summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-10-14 10:56:46 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-10-24 08:08:18 +0200
commite1f357ea3b261265dca2ce0712b265f632079c37 (patch)
tree289ecb097e4eca02b78bbecfb72d93215c173873
parent63e9eb92c6e7a1aee6b682ed885d10a6f6fc699c (diff)
downloadmullvadvpn-e1f357ea3b261265dca2ce0712b265f632079c37.tar.xz
mullvadvpn-e1f357ea3b261265dca2ce0712b265f632079c37.zip
Fetch current location on first start
-rw-r--r--ios/Configurations/Api.xcconfig.template5
-rw-r--r--ios/MullvadMockData/MullvadTypes/URLSessionStub.swift (renamed from ios/MullvadVPNTests/MullvadVPN/Protocols/URLSessionStub.swift)2
-rw-r--r--ios/MullvadREST/ApiHandlers/DefaultLocationService.swift49
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTDefaults.swift3
-rw-r--r--ios/MullvadREST/Info.plist2
-rw-r--r--ios/MullvadREST/Relay/LocationIdentifier.swift10
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Wireguard.swift9
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift16
-rw-r--r--ios/MullvadRESTTests/DefaultLocationServiceTests.swift39
-rw-r--r--ios/MullvadTypes/Protocols/URLSessionProtocol.swift (renamed from ios/MullvadVPN/Protocols/URLSessionProtocol.swift)4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj42
-rw-r--r--ios/MullvadVPN/AppDelegate.swift45
-rw-r--r--ios/MullvadVPN/Classes/AppPreferences.swift14
-rw-r--r--ios/MullvadVPN/Classes/FirstTimeLaunch.swift21
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift1
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift12
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/GeneralAPIs/OutgoingConnectionProxyTests.swift1
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