diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index f8ea478b6f39..f6d36ff95b36 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -708,6 +708,8 @@ EB912B9C22722B6800DF585A /* LockProtected.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB912B9122722A7800DF585A /* LockProtected.swift */; }; EB9407492081353100702E05 /* UXConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB940747208134AF00702E05 /* UXConstants.swift */; }; EB94075320850C9F00702E05 /* photon-colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C49854D206173C800893DAE /* photon-colors.swift */; }; + EB9854FF2422686F0040F24B /* AppDelegate+PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9854FD2422686F0040F24B /* AppDelegate+PushNotifications.swift */; }; + EB98550124226EF70040F24B /* AppDelegate+SyncSentTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB98550024226EF60040F24B /* AppDelegate+SyncSentTabs.swift */; }; EB9A178E20E525DF00B12184 /* ThemeSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A178D20E525DF00B12184 /* ThemeSettingsController.swift */; }; EB9A179B20E69A7F00B12184 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179820E69A7E00B12184 /* ThemeManager.swift */; }; EB9A179C20E69A7F00B12184 /* DarkTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179920E69A7E00B12184 /* DarkTheme.swift */; }; @@ -1847,6 +1849,8 @@ EB912B9022722A7800DF585A /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; EB912B9122722A7800DF585A /* LockProtected.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockProtected.swift; sourceTree = ""; }; EB940747208134AF00702E05 /* UXConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UXConstants.swift; sourceTree = ""; }; + EB9854FD2422686F0040F24B /* AppDelegate+PushNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppDelegate+PushNotifications.swift"; sourceTree = ""; }; + EB98550024226EF60040F24B /* AppDelegate+SyncSentTabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppDelegate+SyncSentTabs.swift"; sourceTree = ""; }; EB9A178D20E525DF00B12184 /* ThemeSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsController.swift; sourceTree = ""; }; EB9A179820E69A7E00B12184 /* ThemeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; EB9A179920E69A7E00B12184 /* DarkTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkTheme.swift; sourceTree = ""; }; @@ -3187,6 +3191,8 @@ isa = PBXGroup; children = ( F84B21E51A0910F600AAB793 /* AppDelegate.swift */, + EB9854FD2422686F0040F24B /* AppDelegate+PushNotifications.swift */, + EB98550024226EF60040F24B /* AppDelegate+SyncSentTabs.swift */, D3BE7B451B054F8600641031 /* TestAppDelegate.swift */, ); name = Delegates; @@ -5160,6 +5166,7 @@ D0EAE0E2228B3762001C875A /* DrawerViewController.swift in Sources */, E689C7301E0C7617008BAADB /* NSAttributedStringExtensions.swift in Sources */, D0C95E0E200FD3B200E4E51C /* PrintHelper.swift in Sources */, + EB9854FF2422686F0040F24B /* AppDelegate+PushNotifications.swift in Sources */, E64ED8FA1BC55AE300DAF864 /* UIAlertControllerExtensions.swift in Sources */, 39CE74F821A83105007AE4F2 /* TranslationSettingsController.swift in Sources */, 282DA4731A68C1E700A406E2 /* OpenSearch.swift in Sources */, @@ -5268,6 +5275,7 @@ C400467C1CF4E43E00B08303 /* BackForwardListViewController.swift in Sources */, D3972BF31C22412B00035B87 /* ShareExtensionHelper.swift in Sources */, F35B8D2D1D6383E9008E3D61 /* SessionRestoreHelper.swift in Sources */, + EB98550124226EF70040F24B /* AppDelegate+SyncSentTabs.swift in Sources */, 744ED5611DBFEB8D00A2B5BE /* MailtoLinkHandler.swift in Sources */, 59A68E0B4ABBF55E14819668 /* BookmarksPanel.swift in Sources */, D04D1B862097859B0074B35F /* DownloadToast.swift in Sources */, diff --git a/Client/Application/AppDelegate+PushNotifications.swift b/Client/Application/AppDelegate+PushNotifications.swift new file mode 100644 index 000000000000..e2bdd6b41941 --- /dev/null +++ b/Client/Application/AppDelegate+PushNotifications.swift @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Shared +import Storage +import Sync +import XCGLogger +import UserNotifications +import Account + +private let log = Logger.browserLogger + +/** + * This exists because the Sync code is extension-safe, and thus doesn't get + * direct access to UIApplication.sharedApplication, which it would need to display a notification. + * This will also likely be the extension point for wipes, resets, and getting access to data sources during a sync. + */ +enum SentTabAction: String { + case view = "TabSendViewAction" + + static let TabSendURLKey = "TabSendURL" + static let TabSendTitleKey = "TabSendTitle" + static let TabSendCategory = "TabSendCategory" + + static func registerActions() { + let viewAction = UNNotificationAction(identifier: SentTabAction.view.rawValue, title: Strings.SentTabViewActionTitle, options: .foreground) + + // Register ourselves to handle the notification category set by NotificationService for APNS notifications + let sentTabCategory = UNNotificationCategory(identifier: "org.mozilla.ios.SentTab.placeholder", actions: [viewAction], intentIdentifiers: [], options: UNNotificationCategoryOptions(rawValue: 0)) + UNUserNotificationCenter.current().setNotificationCategories([sentTabCategory]) + } +} + +extension AppDelegate { + func pushNotificationSetup() { + UNUserNotificationCenter.current().delegate = self + SentTabAction.registerActions() + + NotificationCenter.default.addObserver(forName: .RegisterForPushNotifications, object: nil, queue: .main) { _ in + UNUserNotificationCenter.current().getNotificationSettings { settings in + DispatchQueue.main.async { + if settings.authorizationStatus != .denied { + UIApplication.shared.registerForRemoteNotifications() + } + } + } + } + } + + private func openURLsInNewTabs(_ notification: UNNotification) { + guard let urls = notification.request.content.userInfo["sentTabs"] as? [NSDictionary] else { return } + for sentURL in urls { + if let urlString = sentURL.value(forKey: "url") as? String, let url = URL(string: urlString) { + receivedURLs.append(url) + } + } + + // Check if the app is foregrounded, _also_ verify the BVC is initialized. Most BVC functions depend on viewDidLoad() having run –if not, they will crash. + if UIApplication.shared.applicationState == .active && BrowserViewController.foregroundBVC().isViewLoaded { + BrowserViewController.foregroundBVC().loadQueuedTabs(receivedURLs: receivedURLs) + receivedURLs.removeAll() + } + } +} + +extension AppDelegate: UNUserNotificationCenterDelegate { + // Called when the user taps on a sent-tab notification from the background. + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + openURLsInNewTabs(response.notification) + } + + // Called when the user receives a tab (or any other notification) while in foreground. + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + + if profile?.prefs.boolForKey(PendingAccountDisconnectedKey) ?? false { + FxALoginHelper.sharedInstance.disconnect() + // show the notification + completionHandler([.alert, .sound]) + } else { + openURLsInNewTabs(notification) + } + } +} + +extension AppDelegate { + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + RustFirefoxAccounts.shared.pushNotifications.didRegister(withDeviceToken: deviceToken) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + print("failed to register. \(error)") + Sentry.shared.send(message: "Failed to register for APNS") + } +} diff --git a/Client/Application/AppDelegate+SyncSentTabs.swift b/Client/Application/AppDelegate+SyncSentTabs.swift new file mode 100644 index 000000000000..c583f8c2dd4a --- /dev/null +++ b/Client/Application/AppDelegate+SyncSentTabs.swift @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Shared +import Storage +import Sync +import XCGLogger +import UserNotifications +import Account + +private let log = Logger.browserLogger + +extension UIApplication { + var syncDelegate: SyncDelegate { + return AppSyncDelegate(app: self) + } +} + +/** + Sent tabs can be displayed not only by receiving push notifications, but by sync. + Sync will get the list of sent tabs, and try to display any in that list. + Thus, push notifications are not needed to receive sent tabs, they can be handled + when the app performs a sync. + */ +class AppSyncDelegate: SyncDelegate { + let app: UIApplication + + init(app: UIApplication) { + self.app = app + } + + func displaySentTab(for url: URL, title: String, from deviceName: String?) { + DispatchQueue.main.sync { + if app.applicationState == .active { + BrowserViewController.foregroundBVC().switchToTabForURLOrOpen(url) + return + } + + // check to see what the current notification settings are and only try and send a notification if + // the user has agreed to them + UNUserNotificationCenter.current().getNotificationSettings { settings in + if settings.alertSetting != .enabled { + return + } + if Logger.logPII { + log.info("Displaying notification for URL \(url.absoluteString)") + } + + let notificationContent = UNMutableNotificationContent() + let title: String + if let deviceName = deviceName { + title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName) + } else { + title = Strings.SentTab_TabArrivingNotification_NoDevice_title + } + notificationContent.title = title + notificationContent.body = url.absoluteDisplayExternalString + notificationContent.userInfo = [SentTabAction.TabSendURLKey: url.absoluteString, SentTabAction.TabSendTitleKey: title] + notificationContent.categoryIdentifier = "org.mozilla.ios.SentTab.placeholder" + + // `timeInterval` must be greater than zero + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + + // The identifier for each notification request must be unique in order to be created + let requestIdentifier = "\(SentTabAction.TabSendCategory).\(url.absoluteString)" + let request = UNNotificationRequest(identifier: requestIdentifier, content: notificationContent, trigger: trigger) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + log.error(error.localizedDescription) + } + } + } + } + } +} diff --git a/Client/Application/AppDelegate.swift b/Client/Application/AppDelegate.swift index 78c0f8646f77..b24adb0a1539 100644 --- a/Client/Application/AppDelegate.swift +++ b/Client/Application/AppDelegate.swift @@ -198,8 +198,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati adjustIntegration?.triggerApplicationDidFinishLaunchingWithOptions(launchOptions) - UNUserNotificationCenter.current().delegate = self - SentTabAction.registerActions() UIScrollView.doBadSwizzleStuff() window!.makeKeyAndVisible() @@ -223,15 +221,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati // that is an iOS bug or not. AutocompleteTextField.appearance().semanticContentAttribute = .forceLeftToRight - NotificationCenter.default.addObserver(forName: .RegisterForPushNotifications, object: nil, queue: .main) { _ in - UNUserNotificationCenter.current().getNotificationSettings { settings in - DispatchQueue.main.async { - if settings.authorizationStatus != .denied { - application.registerForRemoteNotifications() - } - } - } - } + pushNotificationSetup() RustFirefoxAccounts.startup() { shared in guard shared.accountManager.hasAccount() else { return } @@ -539,21 +529,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati return false } - fileprivate func openURLsInNewTabs(_ notification: UNNotification) { - guard let urls = notification.request.content.userInfo["sentTabs"] as? [NSDictionary] else { return } - for sentURL in urls { - if let urlString = sentURL.value(forKey: "url") as? String, let url = URL(string: urlString) { - receivedURLs.append(url) - } - } - - // Check if the app is foregrounded, _also_ verify the BVC is initialized. Most BVC functions depend on viewDidLoad() having run –if not, they will crash. - if UIApplication.shared.applicationState == .active && BrowserViewController.foregroundBVC().isViewLoaded { - BrowserViewController.foregroundBVC().loadQueuedTabs(receivedURLs: receivedURLs) - receivedURLs.removeAll() - } - } - func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { let handledShortCutItem = QuickActions.sharedInstance.handleShortCutItem(shortcutItem, withBrowserViewController: BrowserViewController.foregroundBVC()) @@ -612,118 +587,8 @@ extension AppDelegate: MFMailComposeViewControllerDelegate { } } -extension AppDelegate: UNUserNotificationCenterDelegate { - // Called when the user taps on a sent-tab notification from the background. - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - openURLsInNewTabs(response.notification) - } - - // Called when the user receives a tab (or any other notification) while in foreground. - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - - if profile?.prefs.boolForKey(PendingAccountDisconnectedKey) ?? false { - FxALoginHelper.sharedInstance.disconnect() - // show the notification - completionHandler([.alert, .sound]) - } else { - openURLsInNewTabs(notification) - } - } -} - -extension AppDelegate { - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - RustFirefoxAccounts.shared.pushNotifications.didRegister(withDeviceToken: deviceToken) - } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - print("failed to register. \(error)") - Sentry.shared.send(message: "Failed to register for APNS") - } -} - extension UIApplication { - var syncDelegate: SyncDelegate { - return AppSyncDelegate(app: self) - } - static var isInPrivateMode: Bool { return BrowserViewController.foregroundBVC().tabManager.selectedTab?.isPrivate ?? false } } - -class AppSyncDelegate: SyncDelegate { - let app: UIApplication - - init(app: UIApplication) { - self.app = app - } - - open func displaySentTab(for url: URL, title: String, from deviceName: String?) { - DispatchQueue.main.sync { - if app.applicationState == .active { - BrowserViewController.foregroundBVC().switchToTabForURLOrOpen(url) - return - } - - // check to see what the current notification settings are and only try and send a notification if - // the user has agreed to them - UNUserNotificationCenter.current().getNotificationSettings { settings in - if settings.alertSetting == .enabled { - if Logger.logPII { - log.info("Displaying notification for URL \(url.absoluteString)") - } - - let notificationContent = UNMutableNotificationContent() - let title: String - if let deviceName = deviceName { - title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName) - } else { - title = Strings.SentTab_TabArrivingNotification_NoDevice_title - } - notificationContent.title = title - notificationContent.body = url.absoluteDisplayExternalString - notificationContent.userInfo = [SentTabAction.TabSendURLKey: url.absoluteString, SentTabAction.TabSendTitleKey: title] - notificationContent.categoryIdentifier = "org.mozilla.ios.SentTab.placeholder" - - // `timeInterval` must be greater than zero - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) - - // The identifier for each notification request must be unique in order to be created - let requestIdentifier = "\(SentTabAction.TabSendCategory).\(url.absoluteString)" - let request = UNNotificationRequest(identifier: requestIdentifier, content: notificationContent, trigger: trigger) - - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - log.error(error.localizedDescription) - } - } - } - } - } - } -} - -/** - * This exists because the Sync code is extension-safe, and thus doesn't get - * direct access to UIApplication.sharedApplication, which it would need to - * display a notification. - * This will also likely be the extension point for wipes, resets, and - * getting access to data sources during a sync. - */ - -enum SentTabAction: String { - case view = "TabSendViewAction" - - static let TabSendURLKey = "TabSendURL" - static let TabSendTitleKey = "TabSendTitle" - static let TabSendCategory = "TabSendCategory" - - static func registerActions() { - let viewAction = UNNotificationAction(identifier: SentTabAction.view.rawValue, title: Strings.SentTabViewActionTitle, options: .foreground) - - // Register ourselves to handle the notification category set by NotificationService for APNS notifications - let sentTabCategory = UNNotificationCategory(identifier: "org.mozilla.ios.SentTab.placeholder", actions: [viewAction], intentIdentifiers: [], options: UNNotificationCategoryOptions(rawValue: 0)) - UNUserNotificationCenter.current().setNotificationCategories([sentTabCategory]) - } -} diff --git a/RustFxA/FxAWebView.swift b/RustFxA/FxAWebView.swift index bcecb9aff1ef..e6e8d958e596 100755 --- a/RustFxA/FxAWebView.swift +++ b/RustFxA/FxAWebView.swift @@ -32,14 +32,27 @@ fileprivate enum RemoteCommand: String { case deleteAccount = "fxaccounts:delete_account" } +/** + Show the FxA web content for signing in, signing up, or showing FxA settings. + Messaging from the website to native is with WKScriptMessageHandler. + */ class FxAWebView: UIViewController, WKNavigationDelegate { fileprivate let dismissType: DismissType fileprivate var webView: WKWebView fileprivate let pageType: FxAPageType fileprivate var baseURL: URL? - fileprivate var helpBrowser: WKWebView? fileprivate let profile: Profile + /// Used to show a second WKWebView to browse help links. + fileprivate var helpBrowser: WKWebView? + + /** + init() FxAWebView. + + - parameter pageType: Specify login flow or settings page if already logged in. + - parameter profile: a Profile. + - parameter dismissalStyle: depending on how this was presented, it uses modal dismissal, or if part of a UINavigationController stack it will pop to the root. + */ init(pageType: FxAPageType, profile: Profile, dismissalStyle: DismissType) { self.pageType = pageType self.profile = profile @@ -69,8 +82,7 @@ class FxAWebView: UIViewController, WKNavigationDelegate { } override func viewDidLoad() { - // If accountMigrationFailed then the app menu has a caution icon, and at this point the user has taken - // sufficient action to clear the caution. + // If accountMigrationFailed then the app menu has a caution icon, and at this point the user has taken sufficient action to clear the caution. RustFirefoxAccounts.shared.accountMigrationFailed = false super.viewDidLoad() @@ -80,6 +92,9 @@ class FxAWebView: UIViewController, WKNavigationDelegate { let accountManager = RustFirefoxAccounts.shared.accountManager accountManager.getManageAccountURL(entrypoint: "ios_settings_manage") { [weak self] result in guard let self = self else { return } + + // Either show the settings, or the authentication flow. + if self.pageType == .settingsPage, case .success(let url) = result { self.baseURL = url self.webView.load(URLRequest(url: url)) @@ -94,8 +109,22 @@ class FxAWebView: UIViewController, WKNavigationDelegate { } } + /** + Dismiss according the `dismissType`, depending on whether this view was presented modally or on navigation stack. + */ + override func dismiss(animated: Bool, completion: (() -> Void)? = nil) { + if dismissType == .dismiss { + super.dismiss(animated: animated, completion: completion) + } else { + navigationController?.popToRootViewController(animated: true) + completion?() + } + } + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + // Cancel navigation that happens after login to an account, which is when a redirect to `redirectURL` happens. + // The app handles this event fully in native UI. let redirectUrl = RustFirefoxAccounts.shared.redirectURL if let navigationURL = navigationAction.request.url { let expectedRedirectURL = URL(string: redirectUrl)! @@ -133,6 +162,7 @@ extension FxAWebView: WKScriptMessageHandler { } } + /// Send a message to web content using the required message structure. private func runJS(typeId: String, messageId: Int, command: String, data: String = "{}") { let msg = """ var msg = { @@ -149,6 +179,7 @@ extension FxAWebView: WKScriptMessageHandler { webView.evaluateJavaScript(msg) } + /// Respond to the webpage session status notification by either passing signed in user info (for settings), or by passing CWTS setup info (in case the user is signing up for an account). This latter case is also used for the sign-in state. private func onSessionStatus(id: Int) { let cmd = "fxaccounts:fxa_status" let typeId = "account_updates" @@ -186,6 +217,7 @@ extension FxAWebView: WKScriptMessageHandler { UserDefaults.standard.set(declinedSyncEngines, forKey: "fxa.cwts.declinedSyncEngines") } + // Use presence of key `offeredSyncEngines` to determine if this was a new sign-up. if let engines = data["offeredSyncEngines"] as? [String], engines.count > 0 { LeanPlumClient.shared.track(event: .signsUpFxa) } else { @@ -210,11 +242,7 @@ extension FxAWebView: WKScriptMessageHandler { } } - if dismissType == .dismiss { - dismiss(animated: true) - } else { - navigationController?.popToRootViewController(animated: true) - } + dismiss(animated: true) } private func onPasswordChange(data: Any) { @@ -247,10 +275,12 @@ extension FxAWebView: WKScriptMessageHandler { } } -extension FxAWebView{ +extension FxAWebView { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { let hideLongpress = "document.body.style.webkitTouchCallout='none';" webView.evaluateJavaScript(hideLongpress) + + //The helpBrowser shows the current URL in the navbar, the main fxa webview does not. guard webView !== helpBrowser else { let isSecure = webView.hasOnlySecureContent navigationItem.title = (isSecure ? "🔒 " : "") + (webView.url?.host ?? "") @@ -262,7 +292,8 @@ extension FxAWebView{ } extension FxAWebView: WKUIDelegate { - // Blank target links (support links) will create a 2nd webview to browse. + + /// Blank target links (support links) will create a 2nd webview (the `helpBrowser`) to browse. This webview will have a close button in the navigation bar to go back to the main fxa webview. func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { guard helpBrowser == nil else { return nil diff --git a/RustFxA/RustFirefoxAccounts.swift b/RustFxA/RustFirefoxAccounts.swift index 4e0d8299bb8f..0e9ad1eee81e 100644 --- a/RustFxA/RustFirefoxAccounts.swift +++ b/RustFxA/RustFirefoxAccounts.swift @@ -8,6 +8,11 @@ import SwiftKeychainWrapper fileprivate let prefs = NSUserDefaultsPrefs(prefix: "profile") +/** + A singleton that wraps the Rust FxA library. + The singleton design is poor for testability through dependency injection and may need to be changed in future. + */ +// TODO: renamed FirefoxAccounts.swift once the old code is removed fully. open class RustFirefoxAccounts { public static let prefKeyLastDeviceName = "prefKeyLastDeviceName" @@ -15,12 +20,13 @@ open class RustFirefoxAccounts { public let redirectURL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel" public static var shared = RustFirefoxAccounts() public let accountManager: FxAccountManager - public var avatar: Avatar? = nil + public var avatar: Avatar? private static var startupCalled = false public let syncAuthState: SyncAuthState public let pushNotifications = PushNotificationSetup() - + + // This is used so that if a migration failed, show a UI indicator for the user to manually log in to their account. public var accountMigrationFailed: Bool { get { return UserDefaults.standard.bool(forKey: "fxaccount-migration-failed") @@ -30,6 +36,15 @@ open class RustFirefoxAccounts { } } + /** Must be called before this class is fully usable. Until this function is complete, + all methods in this class will behave as if there is no Fx account. + It will be called on app startup, and extensions must call this before using the class. + If it is possible code could access `shared` before initialize() is complete, these callers should also + hook into notifications like `.accountProfileUpdate` to refresh once initialize() is complete. + + The alternative implemention would be to have `shared` as a Deferred. However that + would require a significant rewrite of existing code, for minimal added benefit. + */ public static func startup(completion: ((RustFirefoxAccounts) -> Void)? = nil) { if startupCalled { completion?(shared) @@ -39,15 +54,14 @@ open class RustFirefoxAccounts { shared.accountManager.initialize() { result in let hasAttemptedMigration = UserDefaults.standard.bool(forKey: "hasAttemptedMigration") + + // Note this checks if startup() is called in an app extensions, and if so, do not try account migration if Bundle.main.bundleURL.pathExtension != "appex", let tokens = migrationTokens(), !hasAttemptedMigration { UserDefaults.standard.set(true, forKey: "hasAttemptedMigration") - ["bookmarks", "history", "passwords", "tabs"].forEach { - if let val = prefs.boolForKey("sync.engine.\($0).enabled"), !val { - // is disabled - } - } - + // The client app only needs to trigger this one time. If it fails due to offline state, the rust library + // will automatically re-try until success or permanent failure (notifications accountAuthenticated / accountMigrationFailed respectively). + // See also `init()` use of `.accountAuthenticated` below. shared.accountManager.authenticateViaMigration(sessionToken: tokens.session, kSync: tokens.ksync, kXCS: tokens.kxcs) { _ in } } @@ -73,8 +87,7 @@ open class RustFirefoxAccounts { let config = FxAConfig(server: server, clientId: clientID, redirectUri: redirectURL) let type = UIDevice.current.userInterfaceIdiom == .pad ? DeviceType.tablet : DeviceType.mobile - let deviceConfig = DeviceConfig(name: DeviceInfo.defaultClientName(), type: type, capabilities: [.sendTab]) - + let deviceConfig = DeviceConfig(name: DeviceInfo.defaultClientName(), type: type, capabilities: [.sendTab]) let accessGroupPrefix = Bundle.main.object(forInfoDictionaryKey: "MozDevelopmentTeam") as! String let accessGroupIdentifier = AppInfo.keychainAccessGroupWithPrefix(accessGroupPrefix) @@ -85,7 +98,9 @@ open class RustFirefoxAccounts { withLabel: RustFirefoxAccounts.syncAuthStateUniqueId, factory: syncAuthStateCachefromJSON)) - NotificationCenter.default.addObserver(forName: .accountAuthenticated, object: nil, queue: .main) { [weak self] notification in + // Called when account is logged in for the first time, on every app start when the account is found (even if offline), and when migration of an account is completed. + NotificationCenter.default.addObserver(forName: .accountAuthenticated, object: nil, queue: .main) { [weak self] notification in + // Handle account migration completed successfully. Need to clear the old stored apnsToken and re-register push. if let type = notification.userInfo?["authType"] as? FxaAuthType, case .migrated = type { KeychainWrapper.sharedAppContainerKeychain.removeObject(forKey: "apnsToken", withAccessibility: .afterFirstUnlock) NotificationCenter.default.post(name: .RegisterForPushNotifications, object: nil) @@ -94,7 +109,7 @@ open class RustFirefoxAccounts { self?.update() } - NotificationCenter.default.addObserver(forName: .accountProfileUpdate, object: nil, queue: .main) { [weak self] notification in + NotificationCenter.default.addObserver(forName: .accountProfileUpdate, object: nil, queue: .main) { [weak self] notification in self?.update() } @@ -109,6 +124,7 @@ open class RustFirefoxAccounts { } } + /// When migrating to new rust FxA, grab the old session tokens and try to re-use them. private class func migrationTokens() -> (session: String, ksync: String, kxcs: String)? { // Keychain forKey("profile.account"), return dictionary, from there // forKey("account.state."), guid is dictionary["stateKeyLabel"] @@ -137,29 +153,36 @@ open class RustFirefoxAccounts { return (session: sessionToken, ksync: ksync, kxcs: kxcs) } + /// This is typically used to add a UI indicator that FxA needs attention (usually re-login manually). public var isActionNeeded: Bool { if accountManager.accountMigrationInFlight() || accountMigrationFailed { return true } if !accountManager.hasAccount() { return false } return accountManager.accountNeedsReauth() } + /// Rust FxA notification handlers can call this to update caches and the UI. private func update() { let avatarUrl = accountManager.accountProfile()?.avatar?.url if let str = avatarUrl, let url = URL(string: str) { avatar = Avatar(url: url) } + + // The userProfile (email, display name, etc) and the device name need to be cached for when the app starts in an offline state. Now is a good time to update those caches. + // Accessing the profile will trigger a cache update if needed - let _ = userProfile + _ = userProfile // Update the device name cache if let deviceName = accountManager.deviceConstellation()?.state()?.localDevice?.displayName { UserDefaults.standard.set(deviceName, forKey: RustFirefoxAccounts.prefKeyLastDeviceName) } - + + // The legacy system had both of these notifications for UI updates. Possibly they could be made into a single notification NotificationCenter.default.post(name: .FirefoxAccountProfileChanged, object: self) NotificationCenter.default.post(name: .FirefoxAccountStateChange, object: self) } + /// Cache the user profile (i.e. email, user name) for when the app starts offline. Notice this gets cleared when an account is disconnected. private let prefKeyCachedUserProfile = "prefKeyCachedUserProfile" private var cachedUserProfile: FxAUserProfile? public var userProfile: FxAUserProfile? { @@ -190,6 +213,10 @@ open class RustFirefoxAccounts { } } +/** + Wrap MozillaAppServices.Profile in an easy-to-serialize (and cache) FxAUserProfile. + Caching of this is required for when the app starts offline. + */ public struct FxAUserProfile: Codable, Equatable { public let uid: String public let email: String