diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-10-24 08:13:18 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-10-24 08:13:18 +0200 |
| commit | 3fd0813021ce79818d39996ff089dfb8a5697ad4 (patch) | |
| tree | 289ecb097e4eca02b78bbecfb72d93215c173873 | |
| parent | 63e9eb92c6e7a1aee6b682ed885d10a6f6fc699c (diff) | |
| parent | e1f357ea3b261265dca2ce0712b265f632079c37 (diff) | |
| download | mullvadvpn-3fd0813021ce79818d39996ff089dfb8a5697ad4.tar.xz mullvadvpn-3fd0813021ce79818d39996ff089dfb8a5697ad4.zip | |
Merge branch 'fetch-current-location-asynchronously-upon-first-start-of-ios-1328'
17 files changed, 186 insertions, 89 deletions
diff --git a/ios/Configurations/Api.xcconfig.template b/ios/Configurations/Api.xcconfig.template index 0fb8b09e90..f38cfd22df 100644 --- a/ios/Configurations/Api.xcconfig.template +++ b/ios/Configurations/Api.xcconfig.template @@ -17,3 +17,8 @@ API_ENDPOINT[config=Debug] = 45.83.223.196:443 API_ENDPOINT[config=Release] = 45.83.223.196:443 API_ENDPOINT[config=MockRelease] = 45.83.223.196:443 API_ENDPOINT[config=Staging] = 185.217.116.132:443 + +AM_I_JSON_URL[config=Debug] = https:/${}/am.i.mullvad.net/json +AM_I_JSON_URL[config=Release] = https:/${}/am.i.mullvad.net/json +AM_I_JSON_URL[config=MockRelease] = https:/${}/am.i.mullvad.net/json +AM_I_JSON_URL[config=Staging] = https:/${}/am.i.stagemole.eu/json diff --git a/ios/MullvadVPNTests/MullvadVPN/Protocols/URLSessionStub.swift b/ios/MullvadMockData/MullvadTypes/URLSessionStub.swift index 52c39883a6..44abb2e803 100644 --- a/ios/MullvadVPNTests/MullvadVPN/Protocols/URLSessionStub.swift +++ b/ios/MullvadMockData/MullvadTypes/URLSessionStub.swift @@ -6,7 +6,7 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // -import Foundation +import MullvadTypes class URLSessionStub: URLSessionProtocol { var response: (Data, URLResponse) diff --git a/ios/MullvadREST/ApiHandlers/DefaultLocationService.swift b/ios/MullvadREST/ApiHandlers/DefaultLocationService.swift new file mode 100644 index 0000000000..00d0d56c05 --- /dev/null +++ b/ios/MullvadREST/ApiHandlers/DefaultLocationService.swift @@ -0,0 +1,49 @@ +// +// DefaultLocationService.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-10-13. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import CoreLocation +import MullvadLogging +import MullvadTypes + +public struct DefaultLocationService { + private let urlSession: URLSessionProtocol + private let relayCache: CachedRelays + private let logger = Logger(label: "DefaultLocationService") + + public init(urlSession: URLSessionProtocol, relayCache: CachedRelays) { + self.urlSession = urlSession + self.relayCache = relayCache + } + + public func fetchCurrentLocationIdentifier() async throws -> REST.LocationIdentifier? { + // Safe to unwrap since it's a constant. + let url = URL(string: REST.amIMullvadHostname).unsafelyUnwrapped + + let serverLocation: REST.ServerLocation + do { + let data = try await urlSession.data( + for: URLRequest(url: url, timeoutInterval: REST.defaultAPINetworkTimeout.timeInterval)) + serverLocation = try JSONDecoder().decode(REST.ServerLocation.self, from: data.0) + } catch { + logger.log(level: .error, "Could not fetch server location: \(error.localizedDescription)") + return nil + } + + let mappedRelays = RelayWithLocation.locateRelays( + relays: relayCache.relays.wireguard.relays, + locations: relayCache.relays.locations + ) + + let closestRelay = RelaySelector.WireGuard.closestRelay( + to: CLLocationCoordinate2D(latitude: serverLocation.latitude, longitude: serverLocation.longitude), + using: mappedRelays + ) + + return closestRelay?.location + } +} diff --git a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift index 775f61ffd1..b500826fdb 100644 --- a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift +++ b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift @@ -28,4 +28,7 @@ extension REST { /// Default network timeout for API requests. public static let defaultAPINetworkTimeout: Duration = .seconds(10) + + /// am.i.mullvad.net hostname. + public static let amIMullvadHostname = infoDictionary["AmIMullvad"] as! String } diff --git a/ios/MullvadREST/Info.plist b/ios/MullvadREST/Info.plist index 644beb120a..ec177522a4 100644 --- a/ios/MullvadREST/Info.plist +++ b/ios/MullvadREST/Info.plist @@ -2,6 +2,8 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>AmIMullvad</key> + <string>$(AM_I_JSON_URL)</string> <key>ApiHostName</key> <string>$(API_HOST_NAME)</string> <key>ApiEndpoint</key> diff --git a/ios/MullvadREST/Relay/LocationIdentifier.swift b/ios/MullvadREST/Relay/LocationIdentifier.swift index 53e753277c..cb087c255f 100644 --- a/ios/MullvadREST/Relay/LocationIdentifier.swift +++ b/ios/MullvadREST/Relay/LocationIdentifier.swift @@ -9,19 +9,19 @@ extension REST { // locations are currently always "aa-bbb" for some country code aa and city code bbb. Should this change, this type can be extended. public struct LocationIdentifier: Sendable { - public let country: Substring - public let city: Substring + public let country: String + public let city: String - fileprivate static func parse(_ input: String) -> (Substring, Substring)? { + fileprivate static func parse(_ input: String) -> (String, String)? { let components = input.split(separator: "-") guard components.count == 2 else { return nil } - return (components[0], components[1]) + return (String(components[0]), String(components[1])) } } } extension REST.LocationIdentifier: RawRepresentable { - public var rawValue: String { country.base } + public var rawValue: String { "\(country)-\(city)" } public init?(rawValue: String) { guard let parsed = Self.parse(rawValue) else { return nil } diff --git a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift index dbf3030dab..aab6e73d04 100644 --- a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift +++ b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift @@ -6,6 +6,7 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // +import CoreLocation import MullvadSettings import MullvadTypes @@ -47,7 +48,11 @@ extension RelaySelector { var relayWithLocation: RelayWithLocation<REST.ServerRelay>? if let referenceLocation { - let relay = closestRelay(to: referenceLocation, using: relayWithLocations) + let relay = closestRelay( + to: CLLocationCoordinate2D( + latitude: referenceLocation.latitude, longitude: referenceLocation.longitude), + using: relayWithLocations + ) relayWithLocation = relayWithLocations.first(where: { $0.relay == relay }) } @@ -61,7 +66,7 @@ extension RelaySelector { } public static func closestRelay( - to location: Location, + to location: CLLocationCoordinate2D, using relayWithLocations: [RelayWithLocation<REST.ServerRelay>] ) -> REST.ServerRelay? { let relaysWithDistance = relayWithLocations.map { diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift index dc74d28568..3b13121056 100644 --- a/ios/MullvadREST/Relay/RelaySelector.swift +++ b/ios/MullvadREST/Relay/RelaySelector.swift @@ -131,22 +131,6 @@ public enum RelaySelector { return nil } - private static func makeRelayWithLocationFrom<T: AnyRelay>( - _ serverLocation: REST.ServerLocation, - relay: T - ) -> RelayWithLocation<T>? { - let location = Location( - country: serverLocation.country, - countryCode: String(relay.location.country), - city: serverLocation.city, - cityCode: String(relay.location.city), - latitude: serverLocation.latitude, - longitude: serverLocation.longitude - ) - - return RelayWithLocation(relay: relay, serverLocation: location) - } - private static func filterByActive<T: AnyRelay>( relays: [RelayWithLocation<T>] ) throws -> [RelayWithLocation<T>] { diff --git a/ios/MullvadRESTTests/DefaultLocationServiceTests.swift b/ios/MullvadRESTTests/DefaultLocationServiceTests.swift new file mode 100644 index 0000000000..8d54efd207 --- /dev/null +++ b/ios/MullvadRESTTests/DefaultLocationServiceTests.swift @@ -0,0 +1,39 @@ +// +// DefaultLocationServiceTests.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-10-15. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +@testable import MullvadMockData +@testable import MullvadREST + +class DefaultLocationServiceTests: XCTestCase { + private let encoder = JSONEncoder() + + func testFetchCurrentLocationIdentifier() async throws { + let mockData = try encoder.encode( + REST.ServerLocation( + country: "USA", + city: "Dallas, TX", + latitude: 32.89748, + longitude: -97.040443 + ) + ) + + let locationService = DefaultLocationService( + urlSession: URLSessionStub( + response: (mockData, URLResponse()) + ), + relayCache: try MockRelayCache().read().cachedRelays + ) + + let identifier = try await locationService.fetchCurrentLocationIdentifier() + + XCTAssertEqual(identifier?.country, "us") + XCTAssertEqual(identifier?.city, "dal") + } +} diff --git a/ios/MullvadVPN/Protocols/URLSessionProtocol.swift b/ios/MullvadTypes/Protocols/URLSessionProtocol.swift index b3ca239120..8c64976f69 100644 --- a/ios/MullvadVPN/Protocols/URLSessionProtocol.swift +++ b/ios/MullvadTypes/Protocols/URLSessionProtocol.swift @@ -6,9 +6,7 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // -import Foundation - -protocol URLSessionProtocol { +public protocol URLSessionProtocol { func data(for request: URLRequest) async throws -> (Data, URLResponse) } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 631f52e6bc..e69030542e 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -477,8 +477,6 @@ 7A0EAEA22D033D5D00D3EB8B /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0EAEA12D033D5A00D3EB8B /* BlurView.swift */; }; 7A0EAEA42D06DF8C00D3EB8B /* ConnectionViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */; }; 7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */; }; - 7A12D0762B062D5C00E9602D /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */; }; - 7A12D0772B062D6500E9602D /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */; }; 7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */; }; 7A1A26452A29CEF700B978AA /* RelayFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26442A29CEF700B978AA /* RelayFilterViewController.swift */; }; 7A1A26472A29CF0800B978AA /* RelayFilterDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26462A29CF0800B978AA /* RelayFilterDataSource.swift */; }; @@ -562,7 +560,6 @@ 7A6F2FAF2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */; }; 7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */; }; 7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */; }; - 7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; }; 7A7B3AB62C6DE4DA00D4BCCE /* RestorePurchasesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */; }; 7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; }; 7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */; }; @@ -634,6 +631,10 @@ 7AA1309F2D007B2500640DF9 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */; }; 7AA130A12D01B1E200640DF9 /* SplitMainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */; }; 7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */; }; + 7AA5C3702E9D21DB00B35530 /* DefaultLocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA5C36F2E9D21CD00B35530 /* DefaultLocationService.swift */; }; + 7AA5C3752E9FCD9300B35530 /* DefaultLocationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA5C3742E9FCD5F00B35530 /* DefaultLocationServiceTests.swift */; }; + 7AA5C3792E9FD8BA00B35530 /* URLSessionStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */; }; + 7AA5C37A2E9FDA6C00B35530 /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */; }; 7AA636382D2D3BB0009B2C89 /* View+Conditionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */; }; 7AA7046A2C8EFE2B0045699D /* StoredRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA704682C8EFE050045699D /* StoredRelays.swift */; }; 7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; }; @@ -820,7 +821,6 @@ A9A5F9E42ACB05160083449F /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; }; A9A5F9E52ACB05160083449F /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; A9A5F9E62ACB05160083449F /* DeviceDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58138E60294871C600684F0C /* DeviceDataThrottling.swift */; }; - A9A5F9E72ACB05160083449F /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; }; A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */; }; A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; @@ -1030,7 +1030,6 @@ F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */; }; F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04B62AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift */; }; F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04AF2AE7F83D003D4F89 /* OutgoingConnectionProxy.swift */; }; - F09D04BB2AE95396003D4F89 /* URLSessionStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */; }; F09D04BD2AEBB7C5003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; F09D04C02AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */; }; F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; @@ -2139,7 +2138,6 @@ 7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsInfoButtonItem.swift; sourceTree = "<group>"; }; 7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptibleNavigationController.swift; sourceTree = "<group>"; }; 7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInputFormatter.swift; sourceTree = "<group>"; }; - 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; }; 7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorePurchasesView.swift; sourceTree = "<group>"; }; 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; }; 7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAccessMethodsTests.swift; sourceTree = "<group>"; }; @@ -2205,6 +2203,8 @@ 7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = "<group>"; }; 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitMainButton.swift; sourceTree = "<group>"; }; 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotationTests.swift; sourceTree = "<group>"; }; + 7AA5C36F2E9D21CD00B35530 /* DefaultLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLocationService.swift; sourceTree = "<group>"; }; + 7AA5C3742E9FCD5F00B35530 /* DefaultLocationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLocationServiceTests.swift; sourceTree = "<group>"; }; 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Conditionals.swift"; sourceTree = "<group>"; }; 7AA704682C8EFE050045699D /* StoredRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredRelays.swift; sourceTree = "<group>"; }; 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = "<group>"; }; @@ -2816,7 +2816,6 @@ 440E9EF12BDA940500B1FD11 /* Notifications */, 440E9EF72BDA95AC00B1FD11 /* PacketTunnel */, 44B3C43B2C00CB570079782C /* PacketTunnelCore */, - 440E9F012BDA99FA00B1FD11 /* Protocols */, 440E9EFE2BDA991200B1FD11 /* RelayCacheTracker */, 440E9EFF2BDA995800B1FD11 /* TunnelManager */, 440E9EFC2BDA982200B1FD11 /* View controllers */, @@ -2982,14 +2981,6 @@ path = Extensions; sourceTree = "<group>"; }; - 440E9F012BDA99FA00B1FD11 /* Protocols */ = { - isa = PBXGroup; - children = ( - F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */, - ); - path = Protocols; - sourceTree = "<group>"; - }; 4419AA862D28264D001B13C9 /* ConnectionView */ = { isa = PBXGroup; children = ( @@ -3049,10 +3040,11 @@ 449EBA242B975B7C00DFA4EB /* Protocols */ = { isa = PBXGroup; children = ( - 449EBA252B975B9700DFA4EB /* EphemeralPeerReceiving.swift */, + A97275552CE36CAE00029F15 /* DaitaV2Parameters.swift */, A90C48662C36BC2600DCB94C /* EphemeralPeerReceiver.swift */, + 449EBA252B975B9700DFA4EB /* EphemeralPeerReceiving.swift */, A90C48682C36BF3900DCB94C /* TunnelProvider.swift */, - A97275552CE36CAE00029F15 /* DaitaV2Parameters.swift */, + 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */, ); path = Protocols; sourceTree = "<group>"; @@ -3526,7 +3518,6 @@ 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */, 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */, 58138E60294871C600684F0C /* DeviceDataThrottling.swift */, - 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */, 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */, 7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */, 58DFF7D12B0256A300F864E0 /* MarkdownStylingOptions.swift */, @@ -3589,7 +3580,6 @@ children = ( 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */, 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */, - 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */, ); path = Protocols; sourceTree = "<group>"; @@ -4256,9 +4246,10 @@ 58FBFBE7291622580020E046 /* MullvadRESTTests */ = { isa = PBXGroup; children = ( + 58BDEB9E2A98F6B400F578F2 /* Mocks */, + 7AA5C3742E9FCD5F00B35530 /* DefaultLocationServiceTests.swift */, 58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */, A932D9F22B5EB61100999395 /* HeadRequestTests.swift */, - 58BDEB9E2A98F6B400F578F2 /* Mocks */, F924C4522D706929001F4660 /* MullvadApiTests.swift */, 58B4656F2A98C53300467203 /* RequestExecutorTests.swift */, A91EBED92C1337040004A84D /* RetryStrategyTests.swift */, @@ -4716,6 +4707,7 @@ children = ( 06AC114128F8413A0037AF9A /* AddressCache.swift */, A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */, + 7AA5C36F2E9D21CD00B35530 /* DefaultLocationService.swift */, 06FAE67128F83CA40033DD93 /* HTTP.swift */, 06FAE67228F83CA40033DD93 /* RESTAccessTokenManager.swift */, 06FAE66828F83CA30033DD93 /* RESTAccountsProxy.swift */, @@ -4801,6 +4793,7 @@ 449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */, 449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */, 7AB401862DA53D9B00522E17 /* NewAccountDataMock.swift */, + F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */, ); path = MullvadTypes; sourceTree = "<group>"; @@ -5972,6 +5965,7 @@ 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */, A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */, F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */, + 7AA5C3702E9D21DB00B35530 /* DefaultLocationService.swift in Sources */, F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */, 06799AEA28F98E4800ACD94E /* RESTProxy.swift in Sources */, A90763BC2B2857D50045ADF0 /* Socks5StatusCode.swift in Sources */, @@ -6082,7 +6076,6 @@ A9A5F9E42ACB05160083449F /* AppPreferences.swift in Sources */, A9A5F9E52ACB05160083449F /* CustomDateComponentsFormatting.swift in Sources */, A9A5F9E62ACB05160083449F /* DeviceDataThrottling.swift in Sources */, - A9A5F9E72ACB05160083449F /* FirstTimeLaunch.swift in Sources */, A9B6AC1B2ADEA3AD00F7802A /* MemoryCache.swift in Sources */, 7A9BE5A32B8F89B900E2A7D0 /* LocationNode.swift in Sources */, A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */, @@ -6093,7 +6086,6 @@ A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */, A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */, A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, - 7A12D0772B062D6500E9602D /* URLSessionProtocol.swift in Sources */, 7A9BE5A72B907EEC00E2A7D0 /* AllLocationDataSource.swift in Sources */, A9A5F9EF2ACB05160083449F /* String+AccountFormatting.swift in Sources */, A9A5F9F02ACB05160083449F /* String+FuzzyMatch.swift in Sources */, @@ -6172,7 +6164,6 @@ A9A5FA1C2ACB05160083449F /* Tunnel+Messaging.swift in Sources */, A939661B2CAE6CE1008128CA /* MigrationManagerMultiProcessUpgradeTests.swift in Sources */, 7A9BE5A92B90806800E2A7D0 /* CustomListsRepositoryStub.swift in Sources */, - F09D04BB2AE95396003D4F89 /* URLSessionStub.swift in Sources */, A9A5FA1D2ACB05160083449F /* TunnelBlockObserver.swift in Sources */, A9A5FA1E2ACB05160083449F /* TunnelConfiguration.swift in Sources */, A9A5FA1F2ACB05160083449F /* TunnelInteractor.swift in Sources */, @@ -6448,7 +6439,6 @@ 7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */, 7AA636382D2D3BB0009B2C89 /* View+Conditionals.swift in Sources */, 5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */, - 7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */, F062000C2CB7EB5D002E6DB9 /* UIImage+Helpers.swift in Sources */, F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */, 7A6389EB2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift in Sources */, @@ -6696,7 +6686,6 @@ 58CEB2E92AFBBA4A00E6E088 /* AddAccessMethodCoordinator.swift in Sources */, 58DFF7D02B02560400F864E0 /* NSAttributedString+Extensions.swift in Sources */, 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */, - 7A12D0762B062D5C00E9602D /* URLSessionProtocol.swift in Sources */, 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, 58FF9FEA2B07653800E4C97D /* ButtonCellContentView.swift in Sources */, @@ -6895,6 +6884,7 @@ 58D2240F294C90210029F5F8 /* KeychainError.swift in Sources */, A959E2422D75F3D200F95DDB /* AccessMethodKind.swift in Sources */, 58D22410294C90210029F5F8 /* Location.swift in Sources */, + 7AA5C37A2E9FDA6C00B35530 /* URLSessionProtocol.swift in Sources */, A98207EB2D9190F100654558 /* ShadowsocksConfiguration.swift in Sources */, 58D22411294C90210029F5F8 /* MullvadEndpoint.swift in Sources */, 58D22412294C90210029F5F8 /* RelayConstraint.swift in Sources */, @@ -6937,6 +6927,7 @@ buildActionMask = 2147483647; files = ( 58B465702A98C53300467203 /* RequestExecutorTests.swift in Sources */, + 7AA5C3752E9FCD9300B35530 /* DefaultLocationServiceTests.swift in Sources */, A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */, 58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */, F924C4532D70692E001F4660 /* MullvadApiTests.swift in Sources */, @@ -7088,6 +7079,7 @@ F07F63CE2C63E5790027A351 /* AccessMethodRepository+Stub.swift in Sources */, F0ACE31E2BE4E4F2006D5333 /* AccountsProxy+Stubs.swift in Sources */, F0ACE3202BE4E4F2006D5333 /* AccessTokenManager+Stubs.swift in Sources */, + 7AA5C3792E9FD8BA00B35530 /* URLSessionStub.swift in Sources */, F0ACE32C2BE4E77E006D5333 /* DeviceMock.swift in Sources */, F0ACE3222BE4E4F2006D5333 /* APIProxy+Stubs.swift in Sources */, F0ACE3332BE516F1006D5333 /* RESTRequestExecutor+Stubs.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 4871b07e5a..1072b57a6e 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -48,7 +48,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private var migrationManager: MigrationManager! nonisolated(unsafe) private(set) var accessMethodRepository = AccessMethodRepository() - private(set) var appPreferences = AppPreferences() + nonisolated(unsafe) private(set) var appPreferences = AppPreferences() private(set) var shadowsocksLoader: ShadowsocksLoader! private(set) var ipOverrideRepository = IPOverrideRepository() private(set) var relaySelector: RelaySelectorWrapper! @@ -491,16 +491,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func startInitialization(application: UIApplication) { let wipeSettingsOperation = getWipeSettingsOperation() + let defaultLocationOperation = getDefaultLocationOperation() let loadTunnelStoreOperation = getLoadTunnelStoreOperation() let migrateSettingsOperation = getMigrateSettingsOperation(application: application) let initTunnelManagerOperation = getInitTunnelManagerOperation() + defaultLocationOperation.addDependency(wipeSettingsOperation) migrateSettingsOperation.addDependencies([wipeSettingsOperation, loadTunnelStoreOperation]) initTunnelManagerOperation.addDependency(migrateSettingsOperation) operationQueue.addOperations( [ wipeSettingsOperation, + defaultLocationOperation, loadTunnelStoreOperation, migrateSettingsOperation, initTunnelManagerOperation, @@ -593,8 +596,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// compatible, thus triggering a settings wipe. private func getWipeSettingsOperation() -> AsyncBlockOperation { AsyncBlockOperation { - let appHasNeverBeenLaunched = !FirstTimeLaunch.hasFinished && !SettingsManager.getShouldWipeSettings() - let appWasLaunchedAfterReinstall = !FirstTimeLaunch.hasFinished && SettingsManager.getShouldWipeSettings() + let appHasNeverBeenLaunched = + !self.appPreferences.hasDoneFirstTimeLaunch && !SettingsManager.getShouldWipeSettings() + let appWasLaunchedAfterReinstall = + !self.appPreferences.hasDoneFirstTimeLaunch && SettingsManager.getShouldWipeSettings() if appHasNeverBeenLaunched { try? SettingsManager.writeSettings(LatestTunnelSettings()) @@ -622,11 +627,43 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD try? self.relayCacheTracker.refreshCachedRelays() } - FirstTimeLaunch.setHasFinished() SettingsManager.setShouldWipeSettings() } } + private func getDefaultLocationOperation() -> AsyncBlockOperation { + AsyncBlockOperation { + guard !self.appPreferences.hasDoneFirstTimeLaunch else { + return + } + self.appPreferences.hasDoneFirstTimeLaunch = true + + // No need to keep the handle since we're not waiting or cancelling the completion anyway. + _ = self.relayCacheTracker.updateRelays { _ in + Task { + guard let cachedRelays = try? self.relayCacheTracker.getCachedRelays() else { + return + } + + let locationService = DefaultLocationService( + urlSession: URLSession.shared, relayCache: cachedRelays) + let locationIdentifier = try? await locationService.fetchCurrentLocationIdentifier() + + let constraint = RelayConstraint.only( + UserSelectedRelays( + locations: [.country(locationIdentifier?.country ?? "se")] + )) + + if !self.appPreferences.hasDoneFirstTimeLogin { + self.tunnelManager.updateSettings([ + .relayConstraints(RelayConstraints(entryLocations: constraint, exitLocations: constraint)) + ]) + } + } + } + } + } + // MARK: - StorePaymentManagerDelegate nonisolated func storePaymentManager( diff --git a/ios/MullvadVPN/Classes/AppPreferences.swift b/ios/MullvadVPN/Classes/AppPreferences.swift index 31778ee742..103f17766a 100644 --- a/ios/MullvadVPN/Classes/AppPreferences.swift +++ b/ios/MullvadVPN/Classes/AppPreferences.swift @@ -10,16 +10,28 @@ import Foundation import MullvadSettings protocol AppPreferencesDataSource { + var hasDoneFirstTimeLaunch: Bool { get set } + var hasDoneFirstTimeLogin: Bool { get set } var isShownOnboarding: Bool { get set } var isAgreedToTermsOfService: Bool { get set } var lastSeenChangeLogVersion: String { get set } } enum AppStorageKey: String { - case isShownOnboarding, isAgreedToTermsOfService, lastSeenChangeLogVersion + case hasDoneFirstTimeLaunch = "hasFinishedFirstTimeLaunch" + case hasDoneFirstTimeLogin + case isShownOnboarding + case isAgreedToTermsOfService + case lastSeenChangeLogVersion } final class AppPreferences: AppPreferencesDataSource { + @AppStorage(key: AppStorageKey.hasDoneFirstTimeLaunch.rawValue, container: .standard) + var hasDoneFirstTimeLaunch: Bool = false + + @AppStorage(key: AppStorageKey.hasDoneFirstTimeLogin.rawValue, container: .standard) + var hasDoneFirstTimeLogin: Bool = false + @AppStorage(key: AppStorageKey.isShownOnboarding.rawValue, container: .standard) var isShownOnboarding = true diff --git a/ios/MullvadVPN/Classes/FirstTimeLaunch.swift b/ios/MullvadVPN/Classes/FirstTimeLaunch.swift deleted file mode 100644 index d7ee4ec5fa..0000000000 --- a/ios/MullvadVPN/Classes/FirstTimeLaunch.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// FirstTimeLaunch.swift -// MullvadVPN -// -// Created by Jon Petersson on 2023-04-04. -// Copyright © 2025 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -enum FirstTimeLaunch { - private static let userDefaultsKey = "hasFinishedFirstTimeLaunch" - - static var hasFinished: Bool { - UserDefaults.standard.bool(forKey: userDefaultsKey) - } - - static func setHasFinished() { - UserDefaults.standard.set(true, forKey: userDefaultsKey) - } -} diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index e7121b28ef..f135633a3e 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -466,6 +466,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo coordinator.preferredAccountNumberPublisher = preferredAccountNumberSubject.eraseToAnyPublisher() coordinator.didFinish = { [weak self] _ in + self?.appPreferences.hasDoneFirstTimeLogin = true self?.continueFlow(animated: true) } coordinator.didCreateAccount = { [weak self] in diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift index 9489c773d8..10d2f17341 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift @@ -6,6 +6,7 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // +import CoreLocation import MullvadMockData import MullvadTypes import Network @@ -141,17 +142,8 @@ class RelaySelectorTests: XCTestCase { } let sampleLocation = try XCTUnwrap(sampleRelays.locations["se-got"]) - let location = Location( - country: "Sweden", - countryCode: sampleLocation.country, - city: "Gothenburg", - cityCode: sampleLocation.city, - latitude: sampleLocation.latitude, - longitude: sampleLocation.longitude - ) - let selectedRelay = RelaySelector.WireGuard.closestRelay( - to: location, + to: CLLocationCoordinate2D(latitude: sampleLocation.latitude, longitude: sampleLocation.longitude), using: relayWithLocations ) diff --git a/ios/MullvadVPNTests/MullvadVPN/GeneralAPIs/OutgoingConnectionProxyTests.swift b/ios/MullvadVPNTests/MullvadVPN/GeneralAPIs/OutgoingConnectionProxyTests.swift index 5f582b7817..401a0bc6a0 100644 --- a/ios/MullvadVPNTests/MullvadVPN/GeneralAPIs/OutgoingConnectionProxyTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/GeneralAPIs/OutgoingConnectionProxyTests.swift @@ -6,7 +6,6 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // -import MullvadREST import XCTest @testable import MullvadMockData |
