diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 10f7df5f1cfd..0f58efabc6d4 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -858,6 +858,9 @@ 8AFCE50729DE0CD500B1B253 /* LaunchCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE50629DE0CD500B1B253 /* LaunchCoordinatorTests.swift */; }; 8AFCE50929DE136300B1B253 /* MockLaunchFinishedLoadingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE50829DE136300B1B253 /* MockLaunchFinishedLoadingDelegate.swift */; }; 8AFE4C2127480D0C00B97C65 /* LegacyTabTrayViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFE4C2027480D0B00B97C65 /* LegacyTabTrayViewControllerTests.swift */; }; + 8C19532E2B85E7AE00761B20 /* SelfSizingHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C19532D2B85E7AE00761B20 /* SelfSizingHostingController.swift */; }; + 8C1953302B85E7EC00761B20 /* AutoFillFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C19532F2B85E7EC00761B20 /* AutoFillFooterView.swift */; }; + 8C1953322B85EAB500761B20 /* AutoFillHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C1953312B85EAB500761B20 /* AutoFillHeaderView.swift */; }; 8C29627C2B1F473800571655 /* AdEventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C29627B2B1F473800571655 /* AdEventsResponse.swift */; }; 8C44A9D22A6A99FE009A1AA7 /* ShoppingProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C44A9D12A6A99FE009A1AA7 /* ShoppingProduct.swift */; }; 8C46E1B72B2209F000F56521 /* FakespotAdsEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C46E1B62B2209F000F56521 /* FakespotAdsEvent.swift */; }; @@ -5913,6 +5916,9 @@ 8BF7415AADE68847B78B376B /* mr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mr; path = mr.lproj/3DTouchActions.strings; sourceTree = ""; }; 8BFA4413BB71963DB0E69F82 /* ses */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ses; path = ses.lproj/AuthenticationManager.strings; sourceTree = ""; }; 8BFD47109E07F897D604AEBE /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/ClearPrivateData.strings; sourceTree = ""; }; + 8C19532D2B85E7AE00761B20 /* SelfSizingHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingHostingController.swift; sourceTree = ""; }; + 8C19532F2B85E7EC00761B20 /* AutoFillFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoFillFooterView.swift; sourceTree = ""; }; + 8C1953312B85EAB500761B20 /* AutoFillHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoFillHeaderView.swift; sourceTree = ""; }; 8C264BF5A1C7B2B6378D4DFF /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Today.strings; sourceTree = ""; }; 8C29627B2B1F473800571655 /* AdEventsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdEventsResponse.swift; sourceTree = ""; }; 8C44A9D12A6A99FE009A1AA7 /* ShoppingProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShoppingProduct.swift; sourceTree = ""; }; @@ -8812,6 +8818,9 @@ isa = PBXGroup; children = ( B2981F892B71AD7A00132C1B /* AutofillAccessoryViewButtonItem.swift */, + 8C19532D2B85E7AE00761B20 /* SelfSizingHostingController.swift */, + 8C19532F2B85E7EC00761B20 /* AutoFillFooterView.swift */, + 8C1953312B85EAB500761B20 /* AutoFillHeaderView.swift */, B2FEA6892B460CEC0058E616 /* Address */, 43D00491296FC46E00CB0F31 /* CreditCard */, ); @@ -13489,6 +13498,8 @@ 8C46E1B72B2209F000F56521 /* FakespotAdsEvent.swift in Sources */, 396E38F11EE0C8EC00CC180F /* FxAPushMessageHandler.swift in Sources */, 8A76B01629F6EB3900A82607 /* ScreenshotService.swift in Sources */, + 8C1953322B85EAB500761B20 /* AutoFillHeaderView.swift in Sources */, + 8C19532E2B85E7AE00761B20 /* SelfSizingHostingController.swift in Sources */, E4CD9F6D1A77DD2800318571 /* ReaderModeStyleViewController.swift in Sources */, E13E9AB52AAB0FB5001A0E9D /* FakespotViewModel.swift in Sources */, 8A5D1CBD2A30DC4E005AD35C /* AccountStatusSetting.swift in Sources */, @@ -13986,6 +13997,7 @@ 43F7952525795F69005AEE40 /* SearchTelemetry.swift in Sources */, E65075541E37F6FC006961AC /* LegacyDynamicFontHelper.swift in Sources */, 8ADAE4242A33A126007BF926 /* StudiesToggleSetting.swift in Sources */, + 8C1953302B85E7EC00761B20 /* AutoFillFooterView.swift in Sources */, C82CDD47233E8996002E2743 /* Tab+ChangeUserAgent.swift in Sources */, 81122E212B221AC0003DD9F8 /* SearchScreenState.swift in Sources */, 1DA6F6512B48B42900BB5AD6 /* WindowEventCoordinator.swift in Sources */, @@ -21211,7 +21223,7 @@ repositoryURL = "https://github.com/mozilla/rust-components-swift.git"; requirement = { kind = exactVersion; - version = 125.0.20240216050339; + version = 125.0.20240221050347; }; }; 435C85EE2788F4D00072B526 /* XCRemoteSwiftPackageReference "glean-swift" */ = { diff --git a/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e846ec2e8fa9..7a97545775ab 100644 --- a/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mozilla/rust-components-swift.git", "state" : { - "revision" : "8d6e0c0d2e65e9bddf9e3430162ce570789ea158", - "version" : "125.0.20240216050339" + "revision" : "cf71878bed115ba1fcdf91b24e8cdc181045d4a6", + "version" : "125.0.20240221050347" } }, { diff --git a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift index b74106fb02d8..065561696264 100644 --- a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift +++ b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift @@ -617,5 +617,9 @@ public struct AccessibilityIdentifiers { static let manageCardsButton = "RememberCreditCard.manageCardsButton" static let notNowButton = "RememberCreditCard.notNowButton" } + + enum Autofill { + static let footerPrimaryAction = "Autofill.footerPrimaryAction" + } } // swiftlint:enable line_length diff --git a/firefox-ios/Client/Application/AppDelegate.swift b/firefox-ios/Client/Application/AppDelegate.swift index 16ada5f0660b..d97636d56076 100644 --- a/firefox-ios/Client/Application/AppDelegate.swift +++ b/firefox-ios/Client/Application/AppDelegate.swift @@ -104,7 +104,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { .profileInitialized, .preLaunchDependenciesComplete, .postLaunchDependenciesComplete, - .accountManagerInitialized + .accountManagerInitialized, + .browserIsReady ]) // Then setup dependency container as it's needed for everything else diff --git a/firefox-ios/Client/Application/LaunchSessionProvider.swift b/firefox-ios/Client/Application/LaunchSessionProvider.swift index 720379dcdfb4..78736a5d7953 100644 --- a/firefox-ios/Client/Application/LaunchSessionProvider.swift +++ b/firefox-ios/Client/Application/LaunchSessionProvider.swift @@ -9,16 +9,23 @@ protocol LaunchSessionProviderProtocol { var openedFromExternalSource: Bool { get set } } -class LaunchSessionProvider: LaunchSessionProviderProtocol { - init() { - addObservers() +class LaunchSessionProvider: LaunchSessionProviderProtocol, Notifiable { + private var logger: Logger + var notificationCenter: NotificationProtocol + var openedFromExternalSource = false { + didSet { + guard openedFromExternalSource else { return } + logger.log("openedFromExternalSource was set to true", level: .debug, category: .tabs) + } } - var notificationCenter: NotificationProtocol = NotificationCenter.default - var openedFromExternalSource = false -} + init(notificationCenter: NotificationProtocol = NotificationCenter.default, + logger: Logger = DefaultLogger.shared) { + self.notificationCenter = notificationCenter + self.logger = logger + addObservers() + } -extension LaunchSessionProvider: Notifiable { func addObservers() { setupNotifications(forObserver: self, observing: [UIApplication.willResignActiveNotification, UIScene.willDeactivateNotification]) diff --git a/firefox-ios/Client/Application/SceneDelegate.swift b/firefox-ios/Client/Application/SceneDelegate.swift index 361e002188d4..63bc82fbc611 100644 --- a/firefox-ios/Client/Application/SceneDelegate.swift +++ b/firefox-ios/Client/Application/SceneDelegate.swift @@ -21,6 +21,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var sceneCoordinator: SceneCoordinator? var routeBuilder = RouteBuilder() + var logger: Logger = DefaultLogger.shared // MARK: - Connecting / Disconnecting Scenes @@ -47,10 +48,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let sceneCoordinator = SceneCoordinator(scene: scene) self.sceneCoordinator = sceneCoordinator sceneCoordinator.start() - - AppEventQueue.wait(for: [.startupFlowComplete, .tabRestoration(sceneCoordinator.windowUUID)]) { [weak self] in - self?.handle(connectionOptions: connectionOptions) - } + handle(connectionOptions: connectionOptions) } func sceneDidDisconnect(_ scene: UIScene) { @@ -96,9 +94,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) { guard let url = URLContexts.first?.url, let route = routeBuilder.makeRoute(url: url) else { return } - sceneCoordinator?.findAndHandle(route: route) - - sessionManager.launchSessionProvider.openedFromExternalSource = true + handle(route: route) } // MARK: - Continuing User Activities @@ -106,7 +102,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { /// Use this method to handle Handoff-related data or other activities. func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard let route = routeBuilder.makeRoute(userActivity: userActivity) else { return } - sceneCoordinator?.findAndHandle(route: route) + handle(route: route) } // MARK: - Performing Tasks @@ -124,7 +120,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let route = routeBuilder.makeRoute(shortcutItem: shortcutItem, tabSetting: NewTabAccessors.getNewTabPage(profile.prefs)) else { return } - sceneCoordinator?.findAndHandle(route: route) + handle(route: route) } // MARK: - Misc. Helpers @@ -132,18 +128,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private func handle(connectionOptions: UIScene.ConnectionOptions) { if let context = connectionOptions.urlContexts.first, let route = routeBuilder.makeRoute(url: context.url) { - sceneCoordinator?.findAndHandle(route: route) + handle(route: route) } if let activity = connectionOptions.userActivities.first, let route = routeBuilder.makeRoute(userActivity: activity) { - sceneCoordinator?.findAndHandle(route: route) + handle(route: route) } if let shortcut = connectionOptions.shortcutItem, let route = routeBuilder.makeRoute(shortcutItem: shortcut, tabSetting: NewTabAccessors.getNewTabPage(profile.prefs)) { - sceneCoordinator?.findAndHandle(route: route) + handle(route: route) } // Check if our connection options include a user response to a push @@ -162,7 +158,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let urlString = tab["url"] as? String, let url = URL(string: urlString), let route = routeBuilder.makeRoute(url: url) else { continue } - sceneCoordinator?.findAndHandle(route: route) + handle(route: route) + } + } + + private func handle(route: Route) { + guard let sceneCoordinator = sceneCoordinator else { + logger.log("Scene coordinator should exist", level: .fatal, category: .coordinator) + return + } + + logger.log("Scene coordinator will handle a route", level: .info, category: .coordinator) + sessionManager.launchSessionProvider.openedFromExternalSource = true + + AppEventQueue.wait(for: [.startupFlowComplete, .tabRestoration(sceneCoordinator.windowUUID)]) { + sceneCoordinator.findAndHandle(route: route) } } } diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index e1b65db707e2..f38414b457ad 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -90,6 +90,9 @@ class BrowserCoordinator: BaseCoordinator, // Once launch is done, we check for any saved Route if let savedRoute { + logger.log("Find and handle route called after didFinishLaunch after onboarding", + level: .info, + category: .coordinator) findAndHandle(route: savedRoute) } } @@ -145,13 +148,15 @@ class BrowserCoordinator: BaseCoordinator, if let webviewController = webviewController { webviewController.update(webView: webView, isPrivate: tabManager.selectedTab?.isPrivate ?? false) browserViewController.frontEmbeddedContent(webviewController) + logger.log("Webview content was updated", level: .info, category: .coordinator) } else { let webviewViewController = WebviewViewController( webView: webView, isPrivate: tabManager.selectedTab?.isPrivate ?? false ) webviewController = webviewViewController - _ = browserViewController.embedContent(webviewViewController) + let isEmbedded = browserViewController.embedContent(webviewViewController) + logger.log("Webview controller was created and embedded \(isEmbedded)", level: .info, category: .coordinator) } screenshotService.screenshotableView = webviewController @@ -162,6 +167,9 @@ class BrowserCoordinator: BaseCoordinator, logger.log("Browser has loaded", level: .info, category: .coordinator) if let savedRoute { + logger.log("Find and handle route called after browserHasLoaded", + level: .info, + category: .coordinator) findAndHandle(route: savedRoute) } } @@ -213,6 +221,9 @@ class BrowserCoordinator: BaseCoordinator, override func handle(route: Route) { guard browserIsReady, !tabManager.isRestoringTabs else { + logger.log("Not handling route. Ready? \(browserIsReady), restoring? \(tabManager.isRestoringTabs)", + level: .info, + category: .coordinator) return } @@ -636,6 +647,9 @@ class BrowserCoordinator: BaseCoordinator, func tabManagerDidRestoreTabs(_ tabManager: TabManager) { // Once tab restore is made, if there's any saved route we make sure to call it if let savedRoute { + logger.log("Find and handle route called after tabManagerDidRestoreTabs", + level: .info, + category: .coordinator) findAndHandle(route: savedRoute) } } diff --git a/firefox-ios/Client/Experiments/initial_experiments.json b/firefox-ios/Client/Experiments/initial_experiments.json index 1c446cf8d829..3e95c31cfd9e 100644 --- a/firefox-ios/Client/Experiments/initial_experiments.json +++ b/firefox-ios/Client/Experiments/initial_experiments.json @@ -11,7 +11,7 @@ "channel": "release", "userFacingName": "iOS Onboarding - Search Widget (RERUN)", "userFacingDescription": "Onboarding experiment", - "isEnrollmentPaused": false, + "isEnrollmentPaused": true, "isRollout": false, "bucketConfig": { "randomizationUnit": "nimbus_id", @@ -152,7 +152,7 @@ ], "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('122.!') >= 0) && (language in ['en'])))", "startDate": "2024-02-02", - "enrollmentEndDate": null, + "enrollmentEndDate": "2024-02-20", "endDate": null, "proposedDuration": 25, "proposedEnrollment": 14, @@ -236,7 +236,7 @@ "channel": "release", "userFacingName": "Set to Default reminder notification for iOS", "userFacingDescription": "This experiment will test a notification reminding users to set Firefox as their default browser.", - "isEnrollmentPaused": false, + "isEnrollmentPaused": true, "isRollout": false, "bucketConfig": { "randomizationUnit": "nimbus_id", @@ -328,7 +328,7 @@ ], "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('120.!') >= 0) && (language in ['en'])))", "startDate": "2024-01-23", - "enrollmentEndDate": null, + "enrollmentEndDate": "2024-02-14", "endDate": null, "proposedDuration": 43, "proposedEnrollment": 14, @@ -337,116 +337,6 @@ "localizations": null, "locales": null, "publishedDate": null - }, - { - "schemaVersion": "1.12.0", - "slug": "social-proof-on-ios-default-browser-onboarding-screen", - "id": "social-proof-on-ios-default-browser-onboarding-screen", - "arguments": {}, - "application": "org.mozilla.ios.Firefox", - "appName": "firefox_ios", - "appId": "org.mozilla.ios.Firefox", - "channel": "release", - "userFacingName": "Social Proof on iOS \"Default Browser\" onboarding screen.", - "userFacingDescription": "Testing copy on the default browser screen.", - "isEnrollmentPaused": true, - "isRollout": false, - "bucketConfig": { - "randomizationUnit": "nimbus_id", - "namespace": "ios-onboarding-framework-feature-release-9", - "start": 0, - "count": 10000, - "total": 10000 - }, - "featureIds": [ - "onboarding-framework-feature" - ], - "probeSets": [], - "outcomes": [ - { - "slug": "onboarding", - "priority": "primary" - }, - { - "slug": "default_browser", - "priority": "secondary" - } - ], - "branches": [ - { - "slug": "control", - "ratio": 1, - "feature": { - "featureId": "this-is-included-for-mobile-pre-96-support", - "enabled": false, - "value": {} - }, - "features": [ - { - "featureId": "onboarding-framework-feature", - "enabled": true, - "value": {} - } - ] - }, - { - "slug": "treatment-a", - "ratio": 1, - "feature": { - "featureId": "this-is-included-for-mobile-pre-96-support", - "enabled": false, - "value": {} - }, - "features": [ - { - "featureId": "onboarding-framework-feature", - "enabled": true, - "value": { - "cards": { - "welcome": { - "title": "Find out why millions love Firefox", - "body": "More than 10 million people protect their privacy by choosing a browser that’s backed by a nonprofit." - } - } - } - } - ] - }, - { - "slug": "treatment-b", - "ratio": 1, - "feature": { - "featureId": "this-is-included-for-mobile-pre-96-support", - "enabled": false, - "value": {} - }, - "features": [ - { - "featureId": "onboarding-framework-feature", - "enabled": true, - "value": { - "cards": { - "welcome": { - "title": "Find out why millions love Firefox", - "body": "More than 100 million people protect their privacy by choosing a browser that’s backed by a nonprofit." - } - } - } - } - ] - } - ], - "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('120.!') >= 0) && (language in ['en'])))", - "startDate": "2024-01-09", - "enrollmentEndDate": "2024-01-30", - "endDate": null, - "proposedDuration": 42, - "proposedEnrollment": 14, - "referenceBranch": "control", - "featureValidationOptOut": false, - "localizations": null, - "locales": null, - "publishedDate": null } ] } diff --git a/firefox-ios/Client/Frontend/Autofill/AutoFillFooterView.swift b/firefox-ios/Client/Frontend/Autofill/AutoFillFooterView.swift new file mode 100644 index 000000000000..ced4020912dd --- /dev/null +++ b/firefox-ios/Client/Frontend/Autofill/AutoFillFooterView.swift @@ -0,0 +1,71 @@ +// 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 SwiftUI +import Common + +struct AutoFillFooterView: View { + // Constants for UI layout and styling adapted for LoginAutoFill feature + private enum UX { + static let actionButtonFontSize: CGFloat = 16 + static let actionButtonLeadingSpace: CGFloat = 0 + static let actionButtonTopSpace: CGFloat = 24 + static let actionButtonBottomSpace: CGFloat = 24 + } + + @State private var actionPrimary: Color = .clear + + private let actionButtonTitle: String + private let primaryAction: () -> Void + + init( + title: String, + primaryAction: @escaping () -> Void + ) { + self.actionButtonTitle = title + self.primaryAction = primaryAction + } + + @Environment(\.themeType) + var theme + + var body: some View { + VStack { + Button(action: primaryAction) { + Text(actionButtonTitle) + .font(.system(size: UX.actionButtonFontSize)) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(actionPrimary) + } + .padding([.leading, .trailing], UX.actionButtonLeadingSpace) + .accessibility(identifier: AccessibilityIdentifiers.Autofill.footerPrimaryAction) + } + .onAppear { + applyTheme(theme: theme.theme) + } + .onChange(of: theme) { newThemeValue in + applyTheme(theme: newThemeValue.theme) + } + } + + // MARK: - Theme Application + + /// Applies the theme to the view. + /// - Parameter theme: The theme to be applied. + func applyTheme(theme: Theme) { + let color = theme.colors + actionPrimary = Color(color.actionPrimary) + } +} + +struct AutoFillFooterView_Previews: PreviewProvider { + static var previews: some View { + AutoFillFooterView( + title: "Manage Login Info", + primaryAction: { } + ) + .previewLayout(.sizeThatFits) + .padding() + } +} diff --git a/firefox-ios/Client/Frontend/Autofill/AutoFillHeaderView.swift b/firefox-ios/Client/Frontend/Autofill/AutoFillHeaderView.swift new file mode 100644 index 000000000000..a6a3fabd3f73 --- /dev/null +++ b/firefox-ios/Client/Frontend/Autofill/AutoFillHeaderView.swift @@ -0,0 +1,85 @@ +// 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 SwiftUI +import Common + +struct AutoFillHeaderView: View { + // Constants for UI layout and styling + private struct UX { + static let headerElementsSpacing: CGFloat = 7.0 + static let mainContainerElementsSpacing: CGFloat = 10 + static let bottomSpacing: CGFloat = 24.0 + static let logoSize: CGFloat = 36.0 + static let closeButtonMarginAndWidth: CGFloat = 46.0 + static let buttonSize: CGFloat = 30 + } + + @State private var textPrimary: Color = .clear + @State private var textSecondary: Color = .clear + + @Environment(\.themeType) + var theme + + var title: String + var subtitle: String? + + init(title: String, subtitle: String? = nil) { + self.title = title + self.subtitle = subtitle + } + + var body: some View { + VStack(alignment: .leading, spacing: UX.mainContainerElementsSpacing) { + HStack { + Image(uiImage: UIImage(imageLiteralResourceName: ImageIdentifiers.homeHeaderLogoBall)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: UX.logoSize, height: UX.logoSize) + VStack(alignment: .leading) { + Text(title) + .font(.body) + .fontWeight(.bold) + .foregroundColor(textPrimary) + subtitle.map { + Text($0) + .font(.footnote) + .foregroundColor(textSecondary) + } + } + Spacer() + } + } + .padding([.leading, .trailing], UX.headerElementsSpacing) + .padding(.bottom, UX.bottomSpacing) + + .onAppear { + applyTheme(theme: theme.theme) + } + .onChange(of: theme) { newThemeValue in + applyTheme(theme: newThemeValue.theme) + } + } + + // MARK: - Theme Application + + /// Applies the theme to the view. + /// - Parameter theme: The theme to be applied. + func applyTheme(theme: Theme) { + let color = theme.colors + textPrimary = Color(color.textPrimary) + textSecondary = Color(color.textSecondary) + } +} + +struct AutoFillHeaderView_Previews: PreviewProvider { + static var previews: some View { + AutoFillHeaderView( + title: "Use this login?", + subtitle: "You’ll sign into cnn.com" + ) + .previewLayout(.sizeThatFits) + .padding() + } +} diff --git a/firefox-ios/Client/Frontend/Autofill/SelfSizingHostingController.swift b/firefox-ios/Client/Frontend/Autofill/SelfSizingHostingController.swift new file mode 100644 index 000000000000..b17a0d4bd7ec --- /dev/null +++ b/firefox-ios/Client/Frontend/Autofill/SelfSizingHostingController.swift @@ -0,0 +1,19 @@ +// 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 SwiftUI +import ComponentLibrary + +/// A `UIHostingController` subclass that automatically adjusts its size to fit its SwiftUI `View` content. +/// It also conforms to `BottomSheetChild` for use in bottom sheet contexts, allowing for dismissal handling. +class SelfSizingHostingController: UIHostingController, BottomSheetChild where Content: View { + /// Ensures the view controller dynamically adjusts its size to its content after layout changes. + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + self.view.invalidateIntrinsicContentSize() // Adjusts size based on content. + } + + /// Placeholder for bottom sheet dismissal handling. Override to add custom behavior. + public func willDismiss() {} +} diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 0b5f611e53f7..00b00c7d9daf 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -77,7 +77,6 @@ class BrowserViewController: UIViewController, var urlFromAnotherApp: UrlToOpenModel? var isCrashAlertShowing = false var currentMiddleButtonState: MiddleButtonState? - var openedUrlFromExternalSource = false var passBookHelper: OpenPassBookHelper? var overlayManager: OverlayModeManager var appAuthenticator: AppAuthenticationProtocol @@ -765,6 +764,7 @@ class BrowserViewController: UIViewController, prepareURLOnboardingContextualHint() browserDelegate?.browserHasLoaded() + AppEventQueue.signal(event: .browserIsReady) } private func prepareURLOnboardingContextualHint() { @@ -1579,6 +1579,7 @@ class BrowserViewController: UIViewController, func switchToTabForURLOrOpen(_ url: URL, uuid: String? = nil, isPrivate: Bool = false) { guard !isCrashAlertShowing else { urlFromAnotherApp = UrlToOpenModel(url: url, isPrivate: isPrivate) + logger.log("Saving urlFromAnotherApp since crash alert is showing", level: .debug, category: .tabs) return } popToBVC() @@ -1586,7 +1587,6 @@ class BrowserViewController: UIViewController, tabManager.addTab(URLRequest(url: url), isPrivate: isPrivate) return } - openedUrlFromExternalSource = true if let uuid = uuid, let tab = tabManager.getTabForUUID(uuid: uuid) { tabManager.selectTab(tab) @@ -1607,6 +1607,7 @@ class BrowserViewController: UIViewController, request = URLRequest(url: url) } else { request = nil + logger.log("No request for openURLInNewTab", level: .debug, category: .tabs) } switchToPrivacyMode(isPrivate: isPrivate) @@ -1644,7 +1645,6 @@ class BrowserViewController: UIViewController, tabManager.addTab(nil, isPrivate: isPrivate) return } - openedUrlFromExternalSource = true let freshTab = openURLInNewTab(nil, isPrivate: isPrivate) freshTab.metadataManager?.updateTimerAndObserving(state: .newTab, isPrivate: freshTab.isPrivate) diff --git a/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift b/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift index 6d6192dc2a0e..447306f91480 100644 --- a/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift +++ b/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift @@ -18,6 +18,7 @@ public enum AppEvent: AppEventType { case preLaunchDependenciesComplete case postLaunchDependenciesComplete case accountManagerInitialized + case browserIsReady // Activities: Profile Syncing case profileSyncing diff --git a/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift b/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift index e81b398867af..e6317c2ba4e6 100644 --- a/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift +++ b/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift @@ -889,7 +889,10 @@ class LegacyTabManager: NSObject, FeatureFlaggable, TabManager, TabEventHandler func startAtHomeCheck() -> Bool { let startAtHomeManager = StartAtHomeHelper(prefs: profile.prefs, isRestoringTabs: !tabRestoreHasFinished) - guard !startAtHomeManager.shouldSkipStartHome else { return false } + guard !startAtHomeManager.shouldSkipStartHome else { + logger.log("Skipping start at home", level: .debug, category: .tabs) + return false + } if startAtHomeManager.shouldStartAtHome() { let wasLastSessionPrivate = selectedTab?.isPrivate ?? false diff --git a/firefox-ios/Client/TabManagement/Tab.swift b/firefox-ios/Client/TabManagement/Tab.swift index d5ce68f0643e..a68e27679473 100644 --- a/firefox-ios/Client/TabManagement/Tab.swift +++ b/firefox-ios/Client/TabManagement/Tab.swift @@ -549,6 +549,7 @@ class Tab: NSObject, ThemeApplicable { deinit { webView?.removeObserver(self, forKeyPath: KVOConstants.URL.rawValue) webView?.removeObserver(self, forKeyPath: KVOConstants.title.rawValue) + webView?.removeObserver(self, forKeyPath: KVOConstants.hasOnlySecureContent.rawValue) webView?.navigationDelegate = nil debugTabCount -= 1 @@ -592,6 +593,7 @@ class Tab: NSObject, ThemeApplicable { webView?.removeObserver(self, forKeyPath: KVOConstants.URL.rawValue) webView?.removeObserver(self, forKeyPath: KVOConstants.title.rawValue) + webView?.removeObserver(self, forKeyPath: KVOConstants.hasOnlySecureContent.rawValue) if let webView = webView { tabDelegate?.tab(self, willDeleteWebView: webView) diff --git a/firefox-ios/Client/TabManagement/TabManagerImplementation.swift b/firefox-ios/Client/TabManagement/TabManagerImplementation.swift index a1a5664c67bd..ddc322181f4e 100644 --- a/firefox-ios/Client/TabManagement/TabManagerImplementation.swift +++ b/firefox-ios/Client/TabManagement/TabManagerImplementation.swift @@ -260,10 +260,13 @@ class TabManagerImplementation: LegacyTabManager, Notifiable { logger.log("Tab has empty tab.URL \(logMessage)", level: .debug, category: .tabs) - } else if tab.lastKnownUrl == nil { - logger.log("Tab has empty tab.lastKnownURL \(logMessage)", - level: .fatal, - category: .tabs) + + // lastKnownUrl is the fallback in case tab.url is empty. If this one is empty too then this is a problem + if tab.lastKnownUrl == nil { + logger.log("Tab has empty tab.lastKnownURL \(logMessage)", + level: .fatal, + category: .tabs) + } } return TabData(id: tabId, diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TelemetryWrapperTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TelemetryWrapperTests.swift index 04d84f00b44c..430fed2abcce 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TelemetryWrapperTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TelemetryWrapperTests.swift @@ -14,11 +14,13 @@ class TelemetryWrapperTests: XCTestCase { override func setUp() { super.setUp() Glean.shared.resetGlean(clearStores: true) + Experiments.events.clearEvents() } override func tearDown() { super.tearDown() Glean.shared.resetGlean(clearStores: true) + Experiments.events.clearEvents() } // MARK: - Bookmarks @@ -1322,33 +1324,43 @@ class TelemetryWrapperTests: XCTestCase { // MARK: - Nimbus Calls func test_appForeground_NimbusIsCalled() throws { - throw XCTSkip("Need to be investigated with #12567 so we can enable again") -// TelemetryWrapper.recordEvent( -// category: .action, -// method: .foreground, -// object: .app, -// value: nil -// ) -// XCTAssertTrue( -// try Experiments.shared.createMessageHelper().evalJexl( -// expression: "'app_cycle.foreground'|eventSum('Days', 1, 0) > 0" -// ) -// ) + XCTAssertFalse( + try Experiments.createJexlHelper()!.evalJexl( + expression: "'app_cycle.foreground'|eventSum('Days', 1, 0) > 0" + ) + ) + TelemetryWrapper.recordEvent( + category: .action, + method: .foreground, + object: .app, + value: nil + ) + Experiments.shared.waitForDbQueue() + XCTAssertTrue( + try Experiments.createJexlHelper()!.evalJexl( + expression: "'app_cycle.foreground'|eventSum('Days', 1, 0) > 0" + ) + ) } func test_syncLogin_NimbusIsCalled() throws { - throw XCTSkip("Need to be investigated with #12567 so we can enable again") -// TelemetryWrapper.recordEvent( -// category: .firefoxAccount, -// method: .view, -// object: .fxaLoginCompleteWebpage, -// value: nil -// ) -// XCTAssertTrue( -// try Experiments.shared.createMessageHelper().evalJexl( -// expression: "'sync.login_completed_view'|eventSum('Days', 1, 0) > 0" -// ) -// ) + XCTAssertFalse( + try Experiments.createJexlHelper()!.evalJexl( + expression: "'sync.login_completed_view'|eventSum('Days', 1, 0) > 0" + ) + ) + TelemetryWrapper.recordEvent( + category: .firefoxAccount, + method: .view, + object: .fxaLoginCompleteWebpage, + value: nil + ) + Experiments.shared.waitForDbQueue() + XCTAssertTrue( + try Experiments.createJexlHelper()!.evalJexl( + expression: "'sync.login_completed_view'|eventSum('Days', 1, 0) > 0" + ) + ) } // MARK: - App Errors diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift index 346d8fa79693..47f59581ce33 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift @@ -199,8 +199,9 @@ class ActivityStreamTest: BaseTestCase { navigator.nowAt(NewTabScreen) // Open one of the sites from Topsites and wait until page is loaded // Long tap on apple top site, second cell - waitForExistence(app.collectionViews.cells.element(boundBy: 4), timeout: 3) - app.collectionViews.cells.element(boundBy: 4).press(forDuration: 1) + waitForExistence(app.collectionViews["FxCollectionView"].cells + .staticTexts[defaultTopSite["bookmarkLabel"]!], timeout: 3) + app.collectionViews["FxCollectionView"].cells.staticTexts[defaultTopSite["bookmarkLabel"]!].press(forDuration: 1) selectOptionFromContextMenu(option: "Open in a Private Tab") // Check that two tabs are open and one of them is the default top site one diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/URLValidationTests.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/URLValidationTests.swift index 20d1e845b0a4..5ab2b3ddc04e 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/URLValidationTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/URLValidationTests.swift @@ -16,7 +16,6 @@ class URLValidationTests: BaseTestCase { app.tables.switches["Show Search Suggestions"].tap() scrollToElement(app.tables.switches["FirefoxSuggestShowNonSponsoredSuggestions"]) app.tables.switches["FirefoxSuggestShowNonSponsoredSuggestions"].tap() - app.tables.switches["FirefoxSuggestShowSponsoredSuggestions"].tap() navigator.goto(NewTabScreen) }