summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/SceneDelegate.swift
blob: a28e7c92651ab4099cba59ed81543259654a50bc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//
//  SceneDelegate.swift
//  MullvadVPN
//
//  Created by pronebird on 20/05/2022.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import MullvadLogging
import MullvadREST
import MullvadSettings
import MullvadTypes
import Operations
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate, @preconcurrency SettingsMigrationUIHandler {
    private let logger = Logger(label: "SceneDelegate")

    var window: UIWindow?
    private var privacyOverlayWindow: UIWindow?
    private var isSceneConfigured = false

    private var appCoordinator: ApplicationCoordinator?
    private var accountDataThrottling: AccountDataThrottling?
    private var deviceDataThrottling: DeviceDataThrottling?

    private var tunnelObserver: TunnelObserver?

    private var appDelegate: AppDelegate {
        UIApplication.shared.delegate as! AppDelegate
    }

    private var accessMethodRepository: AccessMethodRepositoryProtocol {
        appDelegate.accessMethodRepository
    }

    private var tunnelManager: TunnelManager {
        appDelegate.tunnelManager
    }

    // MARK: - Private

    private func addTunnelObserver() {
        let tunnelObserver = TunnelBlockObserver(
            didLoadConfiguration: { [weak self] _ in
                self?.configureScene()
            },
            didUpdateDeviceState: { [weak self] _, deviceState, _ in
                self?.deviceStateDidChange(deviceState)
            }
        )

        self.tunnelObserver = tunnelObserver

        tunnelManager.addObserver(tunnelObserver)
    }

    private func configureScene() {
        guard !isSceneConfigured else { return }

        isSceneConfigured = true

        accountDataThrottling = AccountDataThrottling(tunnelManager: tunnelManager)
        deviceDataThrottling = DeviceDataThrottling(tunnelManager: tunnelManager)
        refreshLoginMetadata(forceUpdate: true)

        appCoordinator = ApplicationCoordinator(
            tunnelManager: tunnelManager,
            storePaymentManager: appDelegate.storePaymentManager,
            relayCacheTracker: appDelegate.relayCacheTracker,
            apiProxy: appDelegate.apiProxy,
            devicesProxy: appDelegate.devicesProxy,
            accountsProxy: appDelegate.accountsProxy,
            outgoingConnectionService: OutgoingConnectionService(
                outgoingConnectionProxy: OutgoingConnectionProxy(
                    urlSession: REST.makeURLSession(addressCache: appDelegate.addressCache),
                    hostname: ApplicationConfiguration.hostName
                )
            ),
            appPreferences: appDelegate.appPreferences,
            accessMethodRepository: accessMethodRepository,
            ipOverrideRepository: appDelegate.ipOverrideRepository,
            relaySelectorWrapper: appDelegate.relaySelector
        )

        appCoordinator?.onShowSettings = { [weak self] in
            // Refresh account data and device each time user opens settings
            self?.refreshLoginMetadata(forceUpdate: true)
        }

        appCoordinator?.onShowAccount = { [weak self] in
            // Refresh account data and device each time user opens account controller
            self?.refreshLoginMetadata(forceUpdate: true)
        }

        window?.rootViewController = appCoordinator?.rootViewController
        appCoordinator?.start()
    }

    private func setShowsPrivacyOverlay(_ showOverlay: Bool) {
        if showOverlay {
            privacyOverlayWindow?.isHidden = false
            privacyOverlayWindow?.makeKeyAndVisible()
        } else {
            privacyOverlayWindow?.isHidden = true
            window?.makeKeyAndVisible()
        }
    }

    private func deviceStateDidChange(_ deviceState: DeviceState) {
        switch deviceState {
        case .loggedOut, .revoked:
            resetLoginMetadataThrottling()

        case .loggedIn:
            break
        }
    }

    /**
     Refresh login metadata (account and device data) potentially throttling refresh requests based on recency of
     the last issued request.
    
     Account data is always refreshed when either settings or account are presented on screen, otherwise only when close
     to or past expiry.
    
     Both account and device data are refreshed regardless of other conditions when `forceUpdate` is `true`.
    
     For more information on exact timings used for throttling refresh requests refer to `AccountDataThrottling` and
     `DeviceDataThrottling` types.
     */
    private func refreshLoginMetadata(forceUpdate: Bool) {
        let condition: AccountDataThrottling.Condition

        if forceUpdate {
            condition = .always
        } else {
            let isPresentingSettings = appCoordinator?.isPresentingSettings ?? false
            let isPresentingAccount = appCoordinator?.isPresentingAccount ?? false

            condition = isPresentingSettings || isPresentingAccount ? .always : .whenCloseToExpiryAndBeyond
        }

        accountDataThrottling?.requestUpdate(condition: condition)
        deviceDataThrottling?.requestUpdate(forceUpdate: forceUpdate)
    }

    /**
     Reset throttling for login metadata making a subsequent refresh request execute unthrottled.
     */
    private func resetLoginMetadataThrottling() {
        accountDataThrottling?.reset()
        deviceDataThrottling?.reset()
    }

    // MARK: - UIWindowSceneDelegate

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let windowScene = scene as? UIWindowScene else { return }

        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = LaunchViewController()

        privacyOverlayWindow = UIWindow(windowScene: windowScene)
        privacyOverlayWindow?.rootViewController = LaunchViewController()
        privacyOverlayWindow?.windowLevel = .alert + 1

        window?.makeKeyAndVisible()

        addTunnelObserver()

        if tunnelManager.isConfigurationLoaded {
            configureScene()
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {}

    func sceneDidBecomeActive(_ scene: UIScene) {
        if isSceneConfigured {
            refreshLoginMetadata(forceUpdate: false)
        }

        setShowsPrivacyOverlay(false)
    }

    func sceneWillResignActive(_ scene: UIScene) {
        setShowsPrivacyOverlay(true)
    }

    func sceneWillEnterForeground(_ scene: UIScene) {}

    func sceneDidEnterBackground(_ scene: UIScene) {}

    // MARK: - SettingsMigrationUIHandler

    func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) {
        guard let appCoordinator else {
            completionHandler()
            return
        }

        let presentation = AlertPresentation(
            id: "settings-migration-error-alert",
            title: NSLocalizedString("Settings migration error", comment: ""),
            message: Self.migrationErrorReason(error),
            buttons: [
                AlertAction(
                    title: NSLocalizedString("Got it!", comment: ""),
                    style: .default,
                    handler: {
                        completionHandler()
                    }
                )
            ]
        )

        let presenter = AlertPresenter(context: appCoordinator)
        presenter.showAlert(presentation: presentation, animated: true)
    }

    private static func migrationErrorReason(_ error: Error) -> String {
        if error is UnsupportedSettingsVersionError {
            return NSLocalizedString(
                """
                The version of settings stored on device is unrecognized.\
                Settings will be reset to defaults and the device will be logged out.
                """,
                comment: ""
            )
        } else {
            return NSLocalizedString(
                """
                Internal error occurred. Settings will be reset to defaults and device logged out.
                """,
                comment: ""
            )
        }
    }
}