diff --git a/.swiftlint.yml b/.swiftlint.yml index d31de2b49795..32930ee6bcc2 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -95,9 +95,9 @@ comma: error force_try: warning force_cast: warning - file_header: - required_string: "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */" + required_pattern: \/\* This Source Code Form is subject to the terms of the Mozilla Public.+ + line_length: 1000 reporter: "json" # reporter type (xcode, json, csv, checkstyle) diff --git a/Cartfile b/Cartfile index 31103e3a231c..056e8aff2868 100644 --- a/Cartfile +++ b/Cartfile @@ -15,7 +15,7 @@ github "mozilla-mobile/MappaMundi" "master" github "Leanplum/Leanplum-iOS-SDK" ~> 2.7.2 github "mozilla-services/shavar-prod-lists" "72.0" # Use release version of application-services -github "mozilla/application-services" "v60.0.0" +github "mozilla/application-services" "v61.0.7" github "mozilla/glean" "v31.1.0" # Use interim binary version of application-services diff --git a/Cartfile.resolved b/Cartfile.resolved index 9a7ac512f18b..45d7fd7dd7b7 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -15,6 +15,6 @@ github "kif-framework/KIF" "v3.7.8" github "mozilla-mobile/MappaMundi" "1d17845e4bd6077d790aca5a2b4a468f19567934" github "mozilla-mobile/telemetry-ios" "v1.1.3" github "mozilla-services/shavar-prod-lists" "1f282be9bc7bf86cf4d695e2a63dbcdf2eecfb89" -github "mozilla/application-services" "v60.0.0" +github "mozilla/application-services" "v61.0.7" github "mozilla/glean" "v31.1.0" github "swisspol/GCDWebServer" "3.5.3" diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index eec3de4aeea2..b37f8ffe56ed 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -272,6 +272,7 @@ 59A68D66379CFA85C4EAF00B /* TwoLineCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A68B1F857A8638598A63A0 /* TwoLineCell.swift */; }; 59A68E0B4ABBF55E14819668 /* BookmarksPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A6839879D615FC1C0D71CE /* BookmarksPanel.swift */; }; 59A68FD5260B8D520F890F4A /* ReaderPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A685F4EAD19EDEC854BCA4 /* ReaderPanel.swift */; }; + 5F130D2E2483508E00B0F7D0 /* FxAWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F130D2D2483508E00B0F7D0 /* FxAWebViewModel.swift */; }; 63306D3921103EAE00F25400 /* SavedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63306D3821103EAE00F25400 /* SavedTab.swift */; }; 63306D432110B3CD00F25400 /* TabManagerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63306D422110B3CD00F25400 /* TabManagerStore.swift */; }; 63306D452110BAF000F25400 /* TabManagerStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63306D442110BAF000F25400 /* TabManagerStoreTests.swift */; }; @@ -493,6 +494,7 @@ D4C4BDCE2253725E00986F04 /* LibraryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4BDCD2253725E00986F04 /* LibraryTests.swift */; }; D4F3D789232F960600FBB9AA /* WhatsNewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F3D788232F960600FBB9AA /* WhatsNewTest.swift */; }; D81127D81F84023B0050841D /* PhotonActionSheetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81127D71F84023B0050841D /* PhotonActionSheetTest.swift */; }; + D815A3A824A53F3200AAB221 /* TabToolbarHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D815A3A724A53F3200AAB221 /* TabToolbarHelperTests.swift */; }; D81E45131F82C56D004EFFBA /* NewTabContentSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81E45121F82C56C004EFFBA /* NewTabContentSettingsViewController.swift */; }; D821E90E2141B71C00452C55 /* SiriSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D821E9052141B71C00452C55 /* SiriSettingsViewController.swift */; }; D82ED2641FEB3C420059570B /* DefaultSearchPrefsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82ED2631FEB3C420059570B /* DefaultSearchPrefsTests.swift */; }; @@ -1424,6 +1426,7 @@ 59A685F4EAD19EDEC854BCA4 /* ReaderPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderPanel.swift; sourceTree = ""; }; 59A68B1F857A8638598A63A0 /* TwoLineCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoLineCell.swift; sourceTree = ""; }; 59A68CCB63E2A565CB03F832 /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; + 5F130D2D2483508E00B0F7D0 /* FxAWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAWebViewModel.swift; sourceTree = ""; }; 63306D3821103EAE00F25400 /* SavedTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedTab.swift; sourceTree = ""; }; 63306D422110B3CD00F25400 /* TabManagerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerStore.swift; sourceTree = ""; }; 63306D442110BAF000F25400 /* TabManagerStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerStoreTests.swift; sourceTree = ""; }; @@ -1638,6 +1641,7 @@ D4C4BDCD2253725E00986F04 /* LibraryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryTests.swift; sourceTree = ""; }; D4F3D788232F960600FBB9AA /* WhatsNewTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewTest.swift; sourceTree = ""; }; D81127D71F84023B0050841D /* PhotonActionSheetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotonActionSheetTest.swift; sourceTree = ""; }; + D815A3A724A53F3200AAB221 /* TabToolbarHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabToolbarHelperTests.swift; sourceTree = ""; }; D81E377D2242FF61006AC72D /* Client-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "Client-Bridging-Header.h"; path = "Client/Client-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; D81E45121F82C56C004EFFBA /* NewTabContentSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabContentSettingsViewController.swift; sourceTree = ""; }; D821E9052141B71C00452C55 /* SiriSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiriSettingsViewController.swift; sourceTree = ""; }; @@ -2744,6 +2748,7 @@ children = ( EB07F85E240D695F00924860 /* PushNotificationSetup.swift */, C8E2E80723D20FB3005AACE6 /* FxAWebViewController.swift */, + 5F130D2D2483508E00B0F7D0 /* FxAWebViewModel.swift */, C8E2E80823D20FB3005AACE6 /* Avatar.swift */, C8E2E80923D20FB3005AACE6 /* RustFirefoxAccounts.swift */, CDB3BE8624746787009320EE /* FirefoxAccountSignInViewController.swift */, @@ -3535,6 +3540,7 @@ 63306D442110BAF000F25400 /* TabManagerStoreTests.swift */, E1D8BC7921FF7A0000B100BD /* TPStatsBlocklistsTests.swift */, DACDE995225E537900C8F37F /* VersionSettingTests.swift */, + D815A3A724A53F3200AAB221 /* TabToolbarHelperTests.swift */, ); path = ClientTests; sourceTree = ""; @@ -5097,6 +5103,7 @@ CAA3B7E62497DCB60094E3C1 /* LoginDataSource.swift in Sources */, 396E38F11EE0C8EC00CC180F /* FxAPushMessageHandler.swift in Sources */, E4CD9F6D1A77DD2800318571 /* ReaderModeStyleViewController.swift in Sources */, + 5F130D2E2483508E00B0F7D0 /* FxAWebViewModel.swift in Sources */, D0FCF7F51FE45842004A7995 /* UserScriptManager.swift in Sources */, E4A960061ABB9C450069AD6F /* ReaderModeUtils.swift in Sources */, 435D660323D793DF0046EFA2 /* UpdateModel.swift in Sources */, @@ -5358,6 +5365,7 @@ 3943A81D1E9807C700D4F6DC /* FxAPushMessageTest.swift in Sources */, E60D032A1D5118DB002FE3F6 /* SyncStatusResolverTests.swift in Sources */, E683F0A61E92E0820035D990 /* MockableHistory.swift in Sources */, + D815A3A824A53F3200AAB221 /* TabToolbarHelperTests.swift in Sources */, A83E5B1E1C1DAAAA0026D912 /* UIPasteboardExtensions.swift in Sources */, 0BF42D4F1A7CD09600889E28 /* TestFavicons.swift in Sources */, 7BBFEE741BB405D900A305AA /* TabManagerTests.swift in Sources */, diff --git a/Client/Assets/Images.xcassets/tracking-protection-active-block-dark.imageset/Contents.json b/Client/Assets/Images.xcassets/tracking-protection-active-block-dark.imageset/Contents.json new file mode 100644 index 000000000000..6e7534eafe66 --- /dev/null +++ b/Client/Assets/Images.xcassets/tracking-protection-active-block-dark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "trackingprotection-medium-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Client/Assets/Images.xcassets/tracking-protection-active-block-dark.imageset/trackingprotection-medium-dark.pdf b/Client/Assets/Images.xcassets/tracking-protection-active-block-dark.imageset/trackingprotection-medium-dark.pdf new file mode 100644 index 000000000000..afec0b41e653 Binary files /dev/null and b/Client/Assets/Images.xcassets/tracking-protection-active-block-dark.imageset/trackingprotection-medium-dark.pdf differ diff --git a/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/Contents.json b/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/Contents.json index 5ba835ea311c..3be8c07d5065 100644 --- a/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/Contents.json +++ b/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/Contents.json @@ -1,16 +1,16 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "trackingprotection-medium.pdf" + "filename" : "trackingprotection-medium-light.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { - "template-rendering-intent" : "original", - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" } -} \ No newline at end of file +} diff --git a/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/trackingprotection-medium-light.pdf b/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/trackingprotection-medium-light.pdf new file mode 100644 index 000000000000..b813062179a5 Binary files /dev/null and b/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/trackingprotection-medium-light.pdf differ diff --git a/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/trackingprotection-medium.pdf b/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/trackingprotection-medium.pdf deleted file mode 100644 index e94646d70aa2..000000000000 Binary files a/Client/Assets/Images.xcassets/tracking-protection-active-block.imageset/trackingprotection-medium.pdf and /dev/null differ diff --git a/Client/Frontend/Browser/BrowserViewController.swift b/Client/Frontend/Browser/BrowserViewController.swift index 3c677646894f..803b91dc1852 100644 --- a/Client/Frontend/Browser/BrowserViewController.swift +++ b/Client/Frontend/Browser/BrowserViewController.swift @@ -740,6 +740,7 @@ class BrowserViewController: UIViewController { } }) view.setNeedsUpdateConstraints() + navigationToolbar.updateIsSearchStatus(true) } fileprivate func hideFirefoxHome() { @@ -748,6 +749,7 @@ class BrowserViewController: UIViewController { } self.firefoxHomeViewController = nil + navigationToolbar.updateIsSearchStatus(false) UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: { () -> Void in firefoxHomeViewController.view.alpha = 0 }, completion: { _ in @@ -1157,7 +1159,7 @@ class BrowserViewController: UIViewController { super.traitCollectionDidChange(previousTraitCollection) if #available(iOS 13.0, *) { - if ThemeManager.instance.systemThemeIsOn { + if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection), ThemeManager.instance.systemThemeIsOn { let userInterfaceStyle = traitCollection.userInterfaceStyle ThemeManager.instance.current = userInterfaceStyle == .dark ? DarkTheme() : NormalTheme() } @@ -2387,7 +2389,10 @@ extension BrowserViewController: Themeable { webViews.forEach({ $0.applyTheme() }) let tabs = tabManager.tabs - tabs.forEach { $0.applyTheme() } + tabs.forEach { + $0.applyTheme() + urlBar.locationView.tabDidChangeContentBlocking($0) + } guard let contentScript = self.tabManager.selectedTab?.getContentScript(name: ReaderMode.name()) else { return } appyThemeForPreferences(profile.prefs, contentScript: contentScript) diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+TabToolbarDelegate.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+TabToolbarDelegate.swift index d229a1d73d2a..b1fab57f992c 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+TabToolbarDelegate.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+TabToolbarDelegate.swift @@ -184,5 +184,9 @@ extension BrowserViewController: TabToolbarDelegate, PhotonActionSheetProtocol { self.present(backForwardViewController, animated: true, completion: nil) } } + + func tabToolbarDidPressSearch(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + focusLocationTextField(forTab: tabManager.selectedTab) + } } diff --git a/Client/Frontend/Browser/QRCodeViewController.swift b/Client/Frontend/Browser/QRCodeViewController.swift index e95c0f04a7dc..c95659a5b5ae 100644 --- a/Client/Frontend/Browser/QRCodeViewController.swift +++ b/Client/Frontend/Browser/QRCodeViewController.swift @@ -83,7 +83,9 @@ class QRCodeViewController: UIViewController { self.navigationItem.rightBarButtonItem?.isEnabled = false let alert = UIAlertController(title: "", message: Strings.ScanQRCodePermissionErrorMessage, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: Strings.ScanQRCodeErrorOKButton, style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: Strings.ScanQRCodeErrorOKButton, style: .default, handler: { (action) -> Void in + self.dismiss(animated: true) + })) self.present(alert, animated: true, completion: nil) } diff --git a/Client/Frontend/Browser/TabLocationView.swift b/Client/Frontend/Browser/TabLocationView.swift index b3dfbbac1c00..6c311a8339ff 100644 --- a/Client/Frontend/Browser/TabLocationView.swift +++ b/Client/Frontend/Browser/TabLocationView.swift @@ -394,7 +394,8 @@ extension TabLocationView: TabEventHandler { trackingProtectionButton.alpha = 1.0 switch blocker.status { case .blocking: - trackingProtectionButton.setImage(UIImage(imageLiteralResourceName: "tracking-protection-active-block"), for: .normal) + let blockImageName = ThemeManager.instance.currentName == .dark ? "tracking-protection-active-block-dark" : "tracking-protection-active-block" + trackingProtectionButton.setImage(UIImage(imageLiteralResourceName: blockImageName), for: .normal) case .noBlockedURLs: trackingProtectionButton.setImage(UIImage.templateImageNamed("tracking-protection"), for: .normal) trackingProtectionButton.alpha = 0.5 diff --git a/Client/Frontend/Browser/TabToolbar.swift b/Client/Frontend/Browser/TabToolbar.swift index eeb2eb55aeee..f4067e7d20ad 100644 --- a/Client/Frontend/Browser/TabToolbar.swift +++ b/Client/Frontend/Browser/TabToolbar.swift @@ -20,6 +20,7 @@ protocol TabToolbarProtocol: AnyObject { func updateForwardStatus(_ canGoForward: Bool) func updateReloadStatus(_ isLoading: Bool) func updatePageStatus(_ isWebPage: Bool) + func updateIsSearchStatus(_ isHomePage: Bool) func updateTabCount(_ count: Int, animated: Bool) func privateModeBadge(visible: Bool) func appMenuBadge(setVisible: Bool) @@ -38,6 +39,13 @@ protocol TabToolbarDelegate: AnyObject { func tabToolbarDidPressLibrary(_ tabToolbar: TabToolbarProtocol, button: UIButton) func tabToolbarDidPressTabs(_ tabToolbar: TabToolbarProtocol, button: UIButton) func tabToolbarDidLongPressTabs(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidPressSearch(_ tabToolbar: TabToolbarProtocol, button: UIButton) +} + +fileprivate enum MiddleButtonState { + case reload + case stop + case search } @objcMembers @@ -46,15 +54,40 @@ open class TabToolbarHelper: NSObject { let ImageReload = UIImage.templateImageNamed("nav-refresh") let ImageStop = UIImage.templateImageNamed("nav-stop") + let ImageSearch = UIImage.templateImageNamed("search") - var loading: Bool = false { - didSet { - if loading { + fileprivate func setMiddleButtonState(_ state: MiddleButtonState) { + switch state { + case .reload: + toolbar.stopReloadButton.setImage(ImageReload, for: .normal) + toolbar.stopReloadButton.accessibilityLabel = NSLocalizedString("Reload", comment: "Accessibility Label for the tab toolbar Reload button") + case .stop: toolbar.stopReloadButton.setImage(ImageStop, for: .normal) toolbar.stopReloadButton.accessibilityLabel = NSLocalizedString("Stop", comment: "Accessibility Label for the tab toolbar Stop button") + case .search: + toolbar.stopReloadButton.setImage(ImageSearch, for: .normal) + toolbar.stopReloadButton.accessibilityLabel = NSLocalizedString("Search", comment: "Accessibility Label for the tab toolbar Search button") + } + } + + var loading: Bool = false { + didSet { + if !isSearch { + if loading { + setMiddleButtonState(.stop) + } else { + setMiddleButtonState(.reload) + } + } + } + } + + var isSearch: Bool = false { + didSet { + if isSearch { + setMiddleButtonState(.search) } else { - toolbar.stopReloadButton.setImage(ImageReload, for: .normal) - toolbar.stopReloadButton.accessibilityLabel = NSLocalizedString("Reload", comment: "Accessibility Label for the tab toolbar Reload button") + setMiddleButtonState(.stop) } } } @@ -142,6 +175,8 @@ open class TabToolbarHelper: NSObject { func didClickStopReload() { if loading { toolbar.tabToolbarDelegate?.tabToolbarDidPressStop(toolbar, button: toolbar.stopReloadButton) + } else if isSearch { + toolbar.tabToolbarDelegate?.tabToolbarDidPressSearch(toolbar, button: toolbar.stopReloadButton) } else { toolbar.tabToolbarDelegate?.tabToolbarDidPressReload(toolbar, button: toolbar.stopReloadButton) } @@ -326,12 +361,16 @@ extension TabToolbar: TabToolbarProtocol { } func updatePageStatus(_ isWebPage: Bool) { - stopReloadButton.isEnabled = isWebPage + } func updateTabCount(_ count: Int, animated: Bool) { tabsButton.updateTabCount(count, animated: animated) } + + func updateIsSearchStatus(_ isSearch: Bool) { + helper?.isSearch = isSearch + } } extension TabToolbar: Themeable, PrivateModeUI { diff --git a/Client/Frontend/Browser/URLBarView.swift b/Client/Frontend/Browser/URLBarView.swift index 2cd109e82dbf..3b10acdbea4c 100644 --- a/Client/Frontend/Browser/URLBarView.swift +++ b/Client/Frontend/Browser/URLBarView.swift @@ -616,6 +616,10 @@ extension URLBarView: TabToolbarProtocol { stopReloadButton.isEnabled = isWebPage } + func updateIsSearchStatus(_ isHomePag: Bool) { + + } + var access: [Any]? { get { if inOverlayMode { diff --git a/Client/Frontend/Library/BookmarkDetailPanel.swift b/Client/Frontend/Library/BookmarkDetailPanel.swift index 4633c3eda2f4..05e913b1d2ac 100644 --- a/Client/Frontend/Library/BookmarkDetailPanel.swift +++ b/Client/Frontend/Library/BookmarkDetailPanel.swift @@ -171,6 +171,8 @@ class BookmarkDetailPanel: SiteTableViewController { } override func reloadData() { + // Can be called while app backgrounded and the db closed, don't try to reload the data source in this case + if profile.isShutdown { return } profile.places.getBookmarksTree(rootGUID: BookmarkRoots.RootGUID, recursive: true).uponQueue(.main) { result in guard let rootFolder = result.successValue as? BookmarkFolder else { // TODO: Handle error case? diff --git a/Client/Frontend/Library/BookmarksPanel.swift b/Client/Frontend/Library/BookmarksPanel.swift index d45955252482..5b8d77037f83 100644 --- a/Client/Frontend/Library/BookmarksPanel.swift +++ b/Client/Frontend/Library/BookmarksPanel.swift @@ -163,6 +163,8 @@ class BookmarksPanel: SiteTableViewController, LibraryPanel { } override func reloadData() { + // Can be called while app backgrounded and the db closed, don't try to reload the data source in this case + if profile.isShutdown { return } profile.places.getBookmarksTree(rootGUID: bookmarkFolderGUID, recursive: false).uponQueue(.main) { result in guard let folder = result.successValue as? BookmarkFolder else { diff --git a/Client/Frontend/Library/HistoryPanel.swift b/Client/Frontend/Library/HistoryPanel.swift index 82a5483b3070..c03ce6f5f4e1 100644 --- a/Client/Frontend/Library/HistoryPanel.swift +++ b/Client/Frontend/Library/HistoryPanel.swift @@ -155,6 +155,8 @@ class HistoryPanel: SiteTableViewController, LibraryPanel { // MARK: - Loading data override func reloadData() { + // Can be called while app backgrounded and the db closed, don't try to reload the data source in this case + if profile.isShutdown { return } guard !isFetchInProgress else { return } groupedSites = DateGroupedTableData() diff --git a/Client/Frontend/Settings/ThemeSettingsController.swift b/Client/Frontend/Settings/ThemeSettingsController.swift index 87f3a92a1b9a..3f666dac2fa6 100644 --- a/Client/Frontend/Settings/ThemeSettingsController.swift +++ b/Client/Frontend/Settings/ThemeSettingsController.swift @@ -174,6 +174,8 @@ class ThemeSettingsController: ThemedTableViewController { switch section { case .systemTheme: cell.textLabel?.text = Strings.SystemThemeSectionSwitchTitle + cell.textLabel?.numberOfLines = 0 + cell.textLabel?.lineBreakMode = .byWordWrapping let control = UISwitchThemed() diff --git a/Client/Frontend/Strings.swift b/Client/Frontend/Strings.swift index 850767e4be7f..5bb46289a81f 100644 --- a/Client/Frontend/Strings.swift +++ b/Client/Frontend/Strings.swift @@ -797,3 +797,11 @@ extension Strings { extension String { public static let FxAQRCode_Instructions = NSLocalizedString("fxa.qr-scanning-view.instructions", value: "Scan the QR code shown at firefox.com/pair", comment: "Instructions shown on qr code scanning view") } + +//Today Widget Strings - [New Search - Private Search] +extension String { + public static let NewPrivateTabButtonLabel = NSLocalizedString("TodayWidget.NewPrivateTabButtonLabel", tableName: "Today", value: "Private Search", comment: "New Private Tab button label") + public static let NewTabButtonLabel = NSLocalizedString("TodayWidget.NewTabButtonLabel", tableName: "Today", value: "New Search", comment: "New Tab button label") + public static let GoToCopiedLinkLabel = NSLocalizedString("TodayWidget.GoToCopiedLinkLabel", tableName: "Today", value: "Go to copied link", comment: "Go to link on clipboard") + public static let CopiedLinkLabelFromPasteBoard = NSLocalizedString("TodayWidget.CopiedLinkLabelFromPasteBoard", tableName: "Today", value: "Copied Link from clipboard", comment: "Copied Link from clipboard displayed") +} diff --git a/Client/UserResearch/OnboardingUserResearch.swift b/Client/UserResearch/OnboardingUserResearch.swift index 5646756a2259..3f5a35c5e6eb 100644 --- a/Client/UserResearch/OnboardingUserResearch.swift +++ b/Client/UserResearch/OnboardingUserResearch.swift @@ -85,7 +85,6 @@ class OnboardingUserResearch { lpVariableValue = boolValue ? .versionV1 : .versionV2 self.updateTelemetry() } - self.updatedLPVariable = nil self.onboardingScreenType = lpVariableValue self.updatedLPVariable?() } diff --git a/ClientTests/TabToolbarHelperTests.swift b/ClientTests/TabToolbarHelperTests.swift new file mode 100644 index 000000000000..b5c0981e9dde --- /dev/null +++ b/ClientTests/TabToolbarHelperTests.swift @@ -0,0 +1,143 @@ +/* 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/. */ + +@testable import Client + +import XCTest + +class TabToolbarHelperTests: XCTestCase { + var subject: TabToolbarHelper! + var mockToolbar: MockTabToolbar! + + let refreshButtonImage = UIImage.templateImageNamed("nav-refresh") + let backButtonImage = UIImage.templateImageNamed("nav-back") + let forwardButtonImage = UIImage.templateImageNamed("nav-forward") + let menuButtonImage = UIImage.templateImageNamed("nav-menu") + let libraryButtonImage = UIImage.templateImageNamed("menu-library") + let stopButtonImage = UIImage.templateImageNamed("nav-stop") + let searchButtonImage = UIImage.templateImageNamed("search") + + override func setUp() { + super.setUp() + mockToolbar = MockTabToolbar() + subject = TabToolbarHelper(toolbar: mockToolbar) + } + + func testSetsInitialImages() { + XCTAssertEqual(mockToolbar.stopReloadButton.image(for: .normal), refreshButtonImage) + XCTAssertEqual(mockToolbar.backButton.image(for: .normal), backButtonImage) + XCTAssertEqual(mockToolbar.forwardButton.image(for: .normal), forwardButtonImage) + } + + func testSetLoadingStateImages() { + subject.loading = true + XCTAssertEqual(mockToolbar.stopReloadButton.image(for: .normal), stopButtonImage) + } + + func testSetLoadedStateImages() { + subject.loading = false + XCTAssertEqual(mockToolbar.stopReloadButton.image(for: .normal), refreshButtonImage) + } + + func testSearchStateImages() { + subject.isSearch = true + XCTAssertEqual(mockToolbar.stopReloadButton.image(for: .normal), searchButtonImage) + } + + func testSearchStoppedStateImages() { + subject.isSearch = false + XCTAssertEqual(mockToolbar.stopReloadButton.image(for: .normal), stopButtonImage) + } + + func testLoadingDoesNotOverwriteSearchState() { + subject.isSearch = true + subject.loading = true + XCTAssertEqual(mockToolbar.stopReloadButton.image(for: .normal), searchButtonImage) + } +} + +class MockTabsButton: TabsButton { + init() { + super.init(frame: .zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class MockToolbarButton: ToolbarButton { + init() { + super.init(frame: .zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class MockTabToolbar: TabToolbarProtocol { + var tabToolbarDelegate: TabToolbarDelegate? { + get { return nil } + set { } + } + + var _tabsButton = MockTabsButton() + var tabsButton: TabsButton { + get { _tabsButton } + } + + var _appMenuButton = MockToolbarButton() + var appMenuButton: ToolbarButton { get { _appMenuButton } } + + var _libraryButton = MockToolbarButton() + var libraryButton: ToolbarButton { get { _libraryButton } } + + var _forwardButton = MockToolbarButton() + var forwardButton: ToolbarButton { get { _forwardButton } } + + var _backButton = MockToolbarButton() + var backButton: ToolbarButton { get { _backButton } } + + var _stopReloadButton = MockToolbarButton() + var stopReloadButton: ToolbarButton { get { _stopReloadButton } } + var actionButtons: [Themeable & UIButton] { + get { return [] } + } + + func updateBackStatus(_ canGoBack: Bool) { + + } + + func updateForwardStatus(_ canGoForward: Bool) { + + } + + func updateReloadStatus(_ isLoading: Bool) { + } + + func updatePageStatus(_ isWebPage: Bool) { + + } + + func updateIsSearchStatus(_ isHomePage: Bool) { + + } + + func updateTabCount(_ count: Int, animated: Bool) { + + } + + func privateModeBadge(visible: Bool) { + + } + + func appMenuBadge(setVisible: Bool) { + + } + + func warningMenuBadge(setVisible: Bool) { + + } +} diff --git a/Docs/BUILDING.md b/Docs/BUILDING.md index 845e6ebc6e51..57b926efc3c9 100644 --- a/Docs/BUILDING.md +++ b/Docs/BUILDING.md @@ -50,7 +50,7 @@ Pointing to Local Rust Components (Application Services) Firefox for iOS depends internally on some of the [shared Rust components](https://github.com/mozilla/application-services). Sometimes, you may want to also point to your local Rust components when building locally. You can do so by: -1. First ensure you can [build application-services](https://github.com/mozilla/application-services/blob/master/docs/building.md) locally. +1. First ensure you can [build application-services](https://github.com/mozilla/application-services/blob/main/docs/building.md) locally. 2. Next, `carthage build --no-skip-current --platform iOS --verbose --configuration Debug --cache-builds`. 3. Now back in firefox-ios, after `carthage bootstrap`, replace the application-services library with a symlink: diff --git a/Docs/MMA.md b/Docs/MMA.md index 71c9f0f6cfa2..b9020a547aa3 100644 --- a/Docs/MMA.md +++ b/Docs/MMA.md @@ -23,7 +23,7 @@ Data collection Who will have Leanplum enabled? ====================================================== -Users who have a device locale listed in the following code snippet will have Leanplum enabled: https://github.com/mozilla-mobile/firefox-ios/blob/master/Client/Application/LeanplumIntegration.swift +Users who have a device locale listed in the following code snippet will have Leanplum enabled: https://github.com/mozilla-mobile/firefox-ios/blob/main/Client/Application/LeanplumIntegration.swift Where does data sent to the Leanplum backend go? @@ -109,7 +109,7 @@ Some events are not collected in Mozilla Telemetry. This will be addressed separ There are three elements that are used for each event. They are: event name, value(default: 0.0), and info(default: ""). Default value for event value is 0.0. Default value for event info is empty string. -Here is the list of current Events sent, which can be found here in the code base: https://github.com/mozilla-mobile/firefox-ios/blob/master/Client/Application/LeanplumIntegration.swift#L21 +Here is the list of current Events sent, which can be found here in the code base: https://github.com/mozilla-mobile/firefox-ios/blob/main/Client/Application/LeanplumIntegration.swift#L21 The first launch after install ~~~~ diff --git a/Jenkinsfile b/Jenkinsfile index 43bb058188b0..ac4c1d9f9cfe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,7 +1,7 @@ pipeline { agent any triggers { - cron(env.BRANCH_NAME == 'master' ? 'H 0 * * *' : '') + cron(env.BRANCH_NAME == 'main' ? 'H 0 * * *' : '') } options { timestamps() @@ -9,19 +9,19 @@ pipeline { } stages { stage('checkout') { - when { branch 'master' } + when { branch 'main' } steps { checkout scm } } stage('bootstrap') { - when { branch 'master' } + when { branch 'main' } steps { sh './bootstrap.sh' } } stage('test') { - when { branch 'master' } + when { branch 'main' } steps { dir('SyncIntegrationTests') { sh 'pipenv install' @@ -37,7 +37,7 @@ pipeline { post { always { script { - if (env.BRANCH_NAME == 'master') { + if (env.BRANCH_NAME == 'main') { archiveArtifacts 'SyncIntegrationTests/results/*' junit 'SyncIntegrationTests/results/*.xml' publishHTML(target: [ @@ -53,7 +53,7 @@ pipeline { failure { script { - if (env.BRANCH_NAME == 'master') { + if (env.BRANCH_NAME == 'main') { slackSend( color: 'danger', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})") diff --git a/README.md b/README.md index bf7482bf453c..f1ba9ce25afe 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -Firefox for iOS [![codebeat badge](https://codebeat.co/badges/67e58b6d-bc89-4f22-ba8f-7668a9c15c5a)](https://codebeat.co/projects/github-com-mozilla-firefox-ios) [![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=57bf25c0f096bc01001e21e0&branch=master&build=latest)](https://dashboard.buddybuild.com/apps/57bf25c0f096bc01001e21e0/build/latest) [![codecov](https://codecov.io/gh/mozilla-mobile/firefox-ios/branch/master/graph/badge.svg)](https://codecov.io/gh/mozilla-mobile/firefox-ios/branch/master) +Firefox for iOS [![codebeat badge](https://codebeat.co/badges/67e58b6d-bc89-4f22-ba8f-7668a9c15c5a)](https://codebeat.co/projects/github-com-mozilla-firefox-ios) [![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=57bf25c0f096bc01001e21e0&branch=main&build=latest)](https://dashboard.buddybuild.com/apps/57bf25c0f096bc01001e21e0/build/latest) [![codecov](https://codecov.io/gh/mozilla-mobile/firefox-ios/branch/main/graph/badge.svg)](https://codecov.io/gh/mozilla-mobile/firefox-ios/branch/main) =============== Download on the [App Store](https://itunes.apple.com/app/firefox-web-browser/id989804926). -This branch (master) +This branch (main) ----------- This branch only works with [Xcode 11.5](https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_11.5/Xcode_11.5.xip), Swift 5.2 and supports iOS 12.0 and above. @@ -71,4 +71,4 @@ npm run build ## Contributing -Want to contribute to this repository? Check out [Contributing Guidelines](https://github.com/mozilla-mobile/firefox-ios/blob/master/CONTRIBUTING.md) +Want to contribute to this repository? Check out [Contributing Guidelines](https://github.com/mozilla-mobile/firefox-ios/blob/main/CONTRIBUTING.md) diff --git a/RustFxA/FxAWebViewController.swift b/RustFxA/FxAWebViewController.swift index 651ca4ca6aef..b7d22aa2cb2b 100755 --- a/RustFxA/FxAWebViewController.swift +++ b/RustFxA/FxAWebViewController.swift @@ -3,36 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import WebKit -import UIKit import Account -import MozillaAppServices import Shared -import SwiftKeychainWrapper enum DismissType { case dismiss case popToRootVC } -enum FxAPageType { - case emailLoginFlow - case qrCode(url: String) - case settingsPage -} - -// See https://mozilla.github.io/ecosystem-platform/docs/fxa-engineering/fxa-webchannel-protocol -// For details on message types. -fileprivate enum RemoteCommand: String { - //case canLinkAccount = "can_link_account" - // case loaded = "fxaccounts:loaded" - case status = "fxaccounts:fxa_status" - case login = "fxaccounts:oauth_login" - case changePassword = "fxaccounts:change_password" - case signOut = "fxaccounts:logout" - case deleteAccount = "fxaccounts:delete_account" - case profileChanged = "profile:change" -} - /** Show the FxA web content for signing in, signing up, or showing FxA settings. Messaging from the website to native is with WKScriptMessageHandler. @@ -40,12 +18,9 @@ fileprivate enum RemoteCommand: String { class FxAWebViewController: UIViewController, WKNavigationDelegate { fileprivate let dismissType: DismissType fileprivate var webView: WKWebView - fileprivate let pageType: FxAPageType - fileprivate var baseURL: URL? - fileprivate let profile: Profile /// Used to show a second WKWebView to browse help links. fileprivate var helpBrowser: WKWebView? - fileprivate var deepLinkParams: FxALaunchParams? + fileprivate let viewModel: FxAWebViewModel /** init() FxAWebView. @@ -53,26 +28,23 @@ class FxAWebViewController: UIViewController, WKNavigationDelegate { - 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. - - parameter: deepLinkParams: URL args passed in from deep link that propagate to FxA web view + - parameter deepLinkParams: URL args passed in from deep link that propagate to FxA web view */ init(pageType: FxAPageType, profile: Profile, dismissalStyle: DismissType, deepLinkParams: FxALaunchParams?) { - self.pageType = pageType - self.profile = profile + self.viewModel = FxAWebViewModel(pageType: pageType, profile: profile, deepLinkParams: deepLinkParams) + self.dismissType = dismissalStyle - self.deepLinkParams = deepLinkParams let contentController = WKUserContentController() - if let path = Bundle.main.path(forResource: "FxASignIn", ofType: "js"), let source = try? String(contentsOfFile: path, encoding: .utf8) { - let userScript = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true) - contentController.addUserScript(userScript) - } + viewModel.setupUserScript(for: contentController) + let config = WKWebViewConfiguration() config.userContentController = contentController webView = WKWebView(frame: .zero, configuration: config) webView.allowsLinkPreview = false webView.accessibilityLabel = NSLocalizedString("Web content", comment: "Accessibility label for the main web content view") webView.scrollView.bounces = false // Don't allow overscrolling. - webView.customUserAgent = UserAgent.mobileUserAgent() // This is not shown full-screen, use mobile UA + webView.customUserAgent = FxAWebViewModel.mobileUserAgent super.init(nibName: nil, bundle: nil) contentController.add(self, name: "accountsCommandHandler") @@ -85,58 +57,23 @@ class FxAWebViewController: 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. - RustFirefoxAccounts.shared.accountMigrationFailed = false - super.viewDidLoad() + setup() + } + + private func setup() { webView.navigationDelegate = self view = webView - - func makeRequest(_ url: URL) -> URLRequest { - if let query = deepLinkParams?.query { - let args = query.filter { $0.key.starts(with: "utm_") }.map { - return URLQueryItem(name: $0.key, value: $0.value) - } - - var comp = URLComponents(url: url, resolvingAgainstBaseURL: false) - comp?.queryItems?.append(contentsOf: args) - if let url = comp?.url { - return URLRequest(url: url) - } + + viewModel.setupFirstPage { [weak self] (request, telemetryEventMethod) in + if let method = telemetryEventMethod { + UnifiedTelemetry.recordEvent(category: .firefoxAccount, method: method, object: .accountConnected) } - - return URLRequest(url: url) + self?.webView.load(request) } - - RustFirefoxAccounts.shared.accountManager.uponQueue(.main) { accountManager in - accountManager.getManageAccountURL(entrypoint: "ios_settings_manage") { [weak self] result in - guard let self = self else { return } - - // Handle authentication with either the QR code login flow, email login flow, or settings page flow - switch self.pageType { - case .emailLoginFlow: - accountManager.beginAuthentication { [weak self] result in - if case .success(let url) = result { - self?.baseURL = url - UnifiedTelemetry.recordEvent(category: .firefoxAccount, method: .emailLogin, object: .accountConnected) - self?.webView.load(makeRequest(url)) - } - } - case let .qrCode(url): - accountManager.beginPairingAuthentication(pairingUrl: url) { [weak self] result in - if case .success(let url) = result { - self?.baseURL = url - UnifiedTelemetry.recordEvent(category: .firefoxAccount, method: .qrPairing, object: .accountConnected) - self?.webView.load(makeRequest(url)) - } - } - case .settingsPage: - if case .success(let url) = result { - self.baseURL = url - self.webView.load(makeRequest(url)) - } - } - } + + viewModel.onDismissController = { [weak self] in + self?.dismiss(animated: true) } } @@ -153,164 +90,14 @@ class FxAWebViewController: UIViewController, WKNavigationDelegate { } 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.redirectURL - if let navigationURL = navigationAction.request.url { - let expectedRedirectURL = URL(string: redirectUrl)! - if navigationURL.scheme == expectedRedirectURL.scheme && navigationURL.host == expectedRedirectURL.host && navigationURL.path == expectedRedirectURL.path { - decisionHandler(.cancel) - return - } - } - - decisionHandler(.allow) + let decision = viewModel.shouldAllowRedirectAfterLogIn(basedOn: navigationAction.request.url) + decisionHandler(decision) } } extension FxAWebViewController: WKScriptMessageHandler { - // Handle a message coming from the content server. - private func handleRemote(command rawValue: String, id: Int?, data: Any?) { - if let command = RemoteCommand(rawValue: rawValue) { - switch command { - case .login: - if let data = data { - onLogin(data: data) - } - case .changePassword: - if let data = data { - onPasswordChange(data: data) - } - case .status: - if let id = id { - onSessionStatus(id: id) - } - case .deleteAccount, .signOut: - profile.removeAccount() - dismiss(animated: true) - case .profileChanged: - RustFirefoxAccounts.shared.accountManager.peek()?.refreshProfile(ignoreCache: true) - } - } - } - - /// 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 = { - id: "\(typeId)", - message: { - messageId: \(messageId), - command: "\(command)", - data : \(data) - } - }; - window.dispatchEvent(new CustomEvent('WebChannelMessageToContent', { detail: JSON.stringify(msg) })); - """ - - 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) { - guard let fxa = RustFirefoxAccounts.shared.accountManager.peek() else { return } - let cmd = "fxaccounts:fxa_status" - let typeId = "account_updates" - let data: String - switch pageType { - case .settingsPage: - // Both email and uid are required at this time to properly link the FxA settings session - let email = fxa.accountProfile()?.email ?? "" - let uid = fxa.accountProfile()?.uid ?? "" - let token = (try? fxa.getSessionToken().get()) ?? "" - data = """ - { - capabilities: {}, - signedInUser: { - sessionToken: "\(token)", - email: "\(email)", - uid: "\(uid)", - verified: true, - } - } - """ - case .emailLoginFlow, .qrCode: - data = """ - { capabilities: - { choose_what_to_sync: true, engines: ["bookmarks", "history", "tabs", "passwords"] }, - } - """ - } - - runJS(typeId: typeId, messageId: id, command: cmd, data: data) - } - - private func onLogin(data: Any) { - guard let data = data as? [String: Any], let code = data["code"] as? String, let state = data["state"] as? String else { - return - } - - if let declinedSyncEngines = data["declinedSyncEngines"] as? [String] { - // Stash the declined engines so on first sync we can disable them! - 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 { - LeanPlumClient.shared.track(event: .signsInFxa) - } - LeanPlumClient.shared.set(attributes: [LPAttributeKey.signedInSync: true]) - - let auth = FxaAuthData(code: code, state: state, actionQueryParam: "signin") - RustFirefoxAccounts.shared.accountManager.peek()?.finishAuthentication(authData: auth) { _ in - self.profile.syncManager.onAddedAccount() - - // ask for push notification - KeychainWrapper.sharedAppContainerKeychain.removeObject(forKey: KeychainKey.apnsToken, withAccessibility: .afterFirstUnlock) - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in - guard error == nil else { - return - } - if granted { - NotificationCenter.default.post(name: .RegisterForPushNotifications, object: nil) - } - } - } - - dismiss(animated: true) - } - - private func onPasswordChange(data: Any) { - guard let data = data as? [String: Any], let sessionToken = data["sessionToken"] as? String else { - return - } - - RustFirefoxAccounts.shared.accountManager.peek()?.handlePasswordChanged(newSessionToken: sessionToken) { - NotificationCenter.default.post(name: .RegisterForPushNotifications, object: nil) - } - } - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - guard let url = baseURL else { return } - - let origin = message.frameInfo.securityOrigin - guard origin.`protocol` == url.scheme && origin.host == url.host && origin.port == (url.port ?? 0) else { - print("Ignoring message - \(origin) does not match expected origin: \(url.origin ?? "nil")") - return - } - - guard message.name == "accountsCommandHandler" else { return } - guard let body = message.body as? [String: Any], let detail = body["detail"] as? [String: Any], - let msg = detail["message"] as? [String: Any], let cmd = msg["command"] as? String else { - return - } - - let id = Int(msg["messageId"] as? String ?? "") - handleRemote(command: cmd, id: id, data: msg["data"]) + viewModel.handle(scriptMessage: message) } } @@ -321,8 +108,7 @@ extension FxAWebViewController { //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 ?? "") + navigationItem.title = viewModel.composeTitle(basedOn: webView.url, hasOnlySecureContent: webView.hasOnlySecureContent) return } diff --git a/RustFxA/FxAWebViewModel.swift b/RustFxA/FxAWebViewModel.swift new file mode 100644 index 000000000000..d3b87afe03cd --- /dev/null +++ b/RustFxA/FxAWebViewModel.swift @@ -0,0 +1,282 @@ +/* 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 WebKit +import Foundation +import Account +import MozillaAppServices +import Shared +import SwiftKeychainWrapper + +enum FxAPageType { + case emailLoginFlow + case qrCode(url: String) + case settingsPage +} + +// See https://mozilla.github.io/ecosystem-platform/docs/fxa-engineering/fxa-webchannel-protocol +// For details on message types. +fileprivate enum RemoteCommand: String { + //case canLinkAccount = "can_link_account" + // case loaded = "fxaccounts:loaded" + case status = "fxaccounts:fxa_status" + case login = "fxaccounts:oauth_login" + case changePassword = "fxaccounts:change_password" + case signOut = "fxaccounts:logout" + case deleteAccount = "fxaccounts:delete_account" + case profileChanged = "profile:change" +} + +class FxAWebViewModel { + fileprivate let pageType: FxAPageType + fileprivate let profile: Profile + fileprivate var deepLinkParams: FxALaunchParams? + fileprivate(set) var baseURL: URL? + + // This is not shown full-screen, use mobile UA + static let mobileUserAgent = UserAgent.mobileUserAgent() + + func setupUserScript(for controller: WKUserContentController) { + guard let path = Bundle.main.path(forResource: "FxASignIn", ofType: "js"), let source = try? String(contentsOfFile: path, encoding: .utf8) else { + assert(false) + return + } + let userScript = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true) + controller.addUserScript(userScript) + } + + /** + init() FxAWebViewModel. + - parameter pageType: Specify login flow or settings page if already logged in. + - parameter profile: a Profile. + - parameter deepLinkParams: url parameters that originate from a deep link + */ + required init(pageType: FxAPageType, profile: Profile, deepLinkParams: FxALaunchParams?) { + self.pageType = pageType + self.profile = profile + self.deepLinkParams = deepLinkParams + + // If accountMigrationFailed then the app menu has a caution icon, + // and at this point the user has taken sufficient action to clear the caution. + profile.rustFxA.accountMigrationFailed = false + } + + var onDismissController: (() -> Void)? + + func composeTitle(basedOn url: URL?, hasOnlySecureContent: Bool) -> String { + return (hasOnlySecureContent ? "🔒 " : "") + (url?.host ?? "") + } + + func setupFirstPage(completion: @escaping ((URLRequest, UnifiedTelemetry.EventMethod?) -> Void)) { + profile.rustFxA.accountManager.uponQueue(.main) { accountManager in + accountManager.getManageAccountURL(entrypoint: "ios_settings_manage") { [weak self] result in + guard let self = self else { return } + + // Handle authentication with either the QR code login flow, email login flow, or settings page flow + switch self.pageType { + case .emailLoginFlow: + accountManager.beginAuthentication { [weak self] result in + guard let self = self else { return } + + if case .success(let url) = result { + self.baseURL = url + completion(self.makeRequest(url), .emailLogin) + } + } + case let .qrCode(url): + accountManager.beginPairingAuthentication(pairingUrl: url) { [weak self] result in + guard let self = self else { return } + + if case .success(let url) = result { + self.baseURL = url + completion(self.makeRequest(url), .qrPairing) + } + } + case .settingsPage: + if case .success(let url) = result { + self.baseURL = url + completion(self.makeRequest(url), nil) + } + } + } + } + } + + private func makeRequest(_ url: URL) -> URLRequest { + if let query = deepLinkParams?.query { + let args = query.filter { $0.key.starts(with: "utm_") }.map { + return URLQueryItem(name: $0.key, value: $0.value) + } + + var comp = URLComponents(url: url, resolvingAgainstBaseURL: false) + comp?.queryItems?.append(contentsOf: args) + if let url = comp?.url { + return URLRequest(url: url) + } + } + + return URLRequest(url: url) + } +} + +// MARK: - Commands +extension FxAWebViewModel { + func handle(scriptMessage message: WKScriptMessage) { + guard let url = baseURL, let webView = message.webView else { return } + + let origin = message.frameInfo.securityOrigin + guard origin.`protocol` == url.scheme && origin.host == url.host && origin.port == (url.port ?? 0) else { + print("Ignoring message - \(origin) does not match expected origin: \(url.origin ?? "nil")") + return + } + + guard message.name == "accountsCommandHandler" else { return } + guard let body = message.body as? [String: Any], let detail = body["detail"] as? [String: Any], + let msg = detail["message"] as? [String: Any], let cmd = msg["command"] as? String else { + return + } + + let id = Int(msg["messageId"] as? String ?? "") + handleRemote(command: cmd, id: id, data: msg["data"], webView: webView) + } + + // Handle a message coming from the content server. + private func handleRemote(command rawValue: String, id: Int?, data: Any?, webView: WKWebView) { + if let command = RemoteCommand(rawValue: rawValue) { + switch command { + case .login: + if let data = data { + onLogin(data: data, webView: webView) + } + case .changePassword: + if let data = data { + onPasswordChange(data: data, webView: webView) + } + case .status: + if let id = id { + onSessionStatus(id: id, webView: webView) + } + case .deleteAccount, .signOut: + profile.removeAccount() + onDismissController?() + case .profileChanged: + profile.rustFxA.accountManager.peek()?.refreshProfile(ignoreCache: true) + // dismiss keyboard after changing profile in order to see notification view + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + } + } + + /// Send a message to web content using the required message structure. + private func runJS(webView: WKWebView, typeId: String, messageId: Int, command: String, data: String = "{}") { + let msg = """ + var msg = { + id: "\(typeId)", + message: { + messageId: \(messageId), + command: "\(command)", + data : \(data) + } + }; + window.dispatchEvent(new CustomEvent('WebChannelMessageToContent', { detail: JSON.stringify(msg) })); + """ + + 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, webView: WKWebView) { + guard let fxa = profile.rustFxA.accountManager.peek() else { return } + let cmd = "fxaccounts:fxa_status" + let typeId = "account_updates" + let data: String + switch pageType { + case .settingsPage: + // Both email and uid are required at this time to properly link the FxA settings session + let email = fxa.accountProfile()?.email ?? "" + let uid = fxa.accountProfile()?.uid ?? "" + let token = (try? fxa.getSessionToken().get()) ?? "" + data = """ + { + capabilities: {}, + signedInUser: { + sessionToken: "\(token)", + email: "\(email)", + uid: "\(uid)", + verified: true, + } + } + """ + case .emailLoginFlow, .qrCode: + data = """ + { capabilities: + { choose_what_to_sync: true, engines: ["bookmarks", "history", "tabs", "passwords"] }, + } + """ + } + + runJS(webView: webView, typeId: typeId, messageId: id, command: cmd, data: data) + } + + private func onLogin(data: Any, webView: WKWebView) { + guard let data = data as? [String: Any], let code = data["code"] as? String, let state = data["state"] as? String else { + return + } + + if let declinedSyncEngines = data["declinedSyncEngines"] as? [String] { + // Stash the declined engines so on first sync we can disable them! + 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 { + LeanPlumClient.shared.track(event: .signsInFxa) + } + LeanPlumClient.shared.set(attributes: [LPAttributeKey.signedInSync: true]) + + let auth = FxaAuthData(code: code, state: state, actionQueryParam: "signin") + profile.rustFxA.accountManager.peek()?.finishAuthentication(authData: auth) { _ in + self.profile.syncManager.onAddedAccount() + + // ask for push notification + KeychainWrapper.sharedAppContainerKeychain.removeObject(forKey: KeychainKey.apnsToken, withAccessibility: .afterFirstUnlock) + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in + guard error == nil else { + return + } + if granted { + NotificationCenter.default.post(name: .RegisterForPushNotifications, object: nil) + } + } + } + + onDismissController?() + } + + private func onPasswordChange(data: Any, webView: WKWebView) { + guard let data = data as? [String: Any], let sessionToken = data["sessionToken"] as? String else { + return + } + + profile.rustFxA.accountManager.peek()?.handlePasswordChanged(newSessionToken: sessionToken) { + NotificationCenter.default.post(name: .RegisterForPushNotifications, object: nil) + } + } + + func shouldAllowRedirectAfterLogIn(basedOn navigationURL: URL?) -> WKNavigationActionPolicy { + // 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.redirectURL + if let navigationURL = navigationURL { + let expectedRedirectURL = URL(string: redirectUrl)! + if navigationURL.scheme == expectedRedirectURL.scheme && navigationURL.host == expectedRedirectURL.host && navigationURL.path == expectedRedirectURL.path { + return .cancel + } + } + return .allow + } +} diff --git a/RustFxA/RustFirefoxAccounts.swift b/RustFxA/RustFirefoxAccounts.swift index 074e2203350b..13dccf0fa036 100644 --- a/RustFxA/RustFirefoxAccounts.swift +++ b/RustFxA/RustFirefoxAccounts.swift @@ -102,6 +102,10 @@ open class RustFirefoxAccounts { @discardableResult public static func reconfig(prefs: Prefs) -> Deferred { + if isInitializingAccountManager { + // This func is for reconfiguring a completed FxA init, if FxA init is in-progress, let it complete the init as-is + return shared.accountManager + } isInitializingAccountManager = false shared.accountManager = Deferred() return startup(prefs: prefs) diff --git a/UITests/TrackingProtectionTests.swift b/UITests/TrackingProtectionTests.swift index ebacd1e61bb7..72ed9c2d07ee 100644 --- a/UITests/TrackingProtectionTests.swift +++ b/UITests/TrackingProtectionTests.swift @@ -143,7 +143,7 @@ class TrackingProtectionTests: KIFTestCase, TabEventHandler { EarlGrey.selectElement(with: grey_accessibilityID("Settings.TrackingProtectionOption.BlockListStrict")).perform(grey_tap()) // Accept the warning alert when Strict mode is enabled - tester().waitForAnimationsToFinish() + tester().waitForAnimationsToFinish(withTimeout: 3) tester().tapView(withAccessibilityLabel: "OK, Got It") closeTPSetting() } @@ -164,7 +164,6 @@ class TrackingProtectionTests: KIFTestCase, TabEventHandler { .perform(grey_tap()) checkStrictTrackingProtection(isBlocking: false, isTPDisabled: true) - enableStrictMode() // Now with the TP enabled, the image should be blocked @@ -210,5 +209,8 @@ class TrackingProtectionTests: KIFTestCase, TabEventHandler { ContentBlocker.shared.clearSafelist() { clear1.fulfill() } waitForExpectations(timeout: 10, handler: nil) checkStrictTrackingProtection(isBlocking: true) + openTPSetting() + disableStrictTP() + closeTPSetting() } }