diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 3fa0d8cef2b6..f323ce474783 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 0BB5B30B1AC0AD1F0052877D /* LoginsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB5B30A1AC0AD1F0052877D /* LoginsHelper.swift */; }; 0BF0DB941A8545800039F300 /* URLBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF0DB931A8545800039F300 /* URLBarView.swift */; }; 0BF1B7E31AC60DEA00A7B407 /* InsetButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF1B7E21AC60DEA00A7B407 /* InsetButton.swift */; }; + 12187B502C89B43D00BA88CA /* OnboardingCardNTPExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12187B4F2C89B43D00BA88CA /* OnboardingCardNTPExperiment.swift */; }; + 12187B532C89E15000BA88CA /* NTPOnboardingCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12187B522C89E15000BA88CA /* NTPOnboardingCardCell.swift */; }; 158241282820698B00956B39 /* RustRemoteTabsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 158241272820698B00956B39 /* RustRemoteTabsTests.swift */; }; 15DE98FD27FCED4F00F1ECDB /* RustRemoteTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DE98FC27FCED4F00F1ECDB /* RustRemoteTabs.swift */; }; 1B3D99F1270E89D0006E1264 /* Telemetry in Frameworks */ = {isa = PBXBuildFile; productRef = 1B3D99F0270E89D0006E1264 /* Telemetry */; }; @@ -367,6 +369,9 @@ 2C872A622B8CD7E000B318A0 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE294702B7FC5A6006C22B2 /* VersionTests.swift */; }; 2C872A632B8CD7E000B318A0 /* WhatsNewLocalDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE2946B2B7FC5A5006C22B2 /* WhatsNewLocalDataProviderTests.swift */; }; 2C872A642B8CD7E000B318A0 /* EcosiaHomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C872A562B8CD65100B318A0 /* EcosiaHomeViewModelTests.swift */; }; + 2CA995282CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA995272CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift */; }; + 2CA9952A2CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA995292CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift */; }; + 2CA9952E2CA2EFAA001064CC /* NTPOnboardingCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA9952D2CA2EFAA001064CC /* NTPOnboardingCardViewModel.swift */; }; 2CABD7162C11C9CC00A0750F /* MozillaAppServices in Frameworks */ = {isa = PBXBuildFile; productRef = 2CABD7152C11C9CC00A0750F /* MozillaAppServices */; }; 2CABD7282C12EF1E00A0750F /* PrivateModeButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CABD7272C12EF1E00A0750F /* PrivateModeButtonTests.swift */; }; 2CC8AC342C4F887D000A669A /* MockAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC8AC332C4F887D000A669A /* MockAnalytics.swift */; }; @@ -2003,6 +2008,8 @@ 10CD44F0A402C84BB31E5474 /* gu-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "gu-IN"; path = "gu-IN.lproj/Intro.strings"; sourceTree = ""; }; 11F747589EB8A55A47647C93 /* bn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bn; path = bn.lproj/ClearPrivateData.strings; sourceTree = ""; }; 120F42119EB30F217AB9493E /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/FindInPage.strings; sourceTree = ""; }; + 12187B4F2C89B43D00BA88CA /* OnboardingCardNTPExperiment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCardNTPExperiment.swift; sourceTree = ""; }; + 12187B522C89E15000BA88CA /* NTPOnboardingCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPOnboardingCardCell.swift; sourceTree = ""; }; 123045959E0F295753B4B4DB /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Today.strings; sourceTree = ""; }; 12674A038346A46589A0AC0B /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = "el.lproj/Default Browser.strings"; sourceTree = ""; }; 126A40A4A5AFDFD655B0FDF4 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/ClearHistoryConfirm.strings; sourceTree = ""; }; @@ -2502,6 +2509,9 @@ 2C9144B0B15218D8A0FCD538 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "es-AR.lproj/ClearPrivateData.strings"; sourceTree = ""; }; 2C97EC701E72C80E0092EC18 /* TopTabsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTabsTest.swift; sourceTree = ""; }; 2CA16FDD1E5F089100332277 /* SearchTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTest.swift; sourceTree = ""; }; + 2CA995272CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPConfigurableNudgeCardCell.swift; sourceTree = ""; }; + 2CA995292CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPConfigurableNudgeCardCellViewModel.swift; sourceTree = ""; }; + 2CA9952D2CA2EFAA001064CC /* NTPOnboardingCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPOnboardingCardViewModel.swift; sourceTree = ""; }; 2CABD7272C12EF1E00A0750F /* PrivateModeButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateModeButtonTests.swift; sourceTree = ""; }; 2CAE4511992E91A32AB7D7C7 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Today.strings; sourceTree = ""; }; 2CB1728B2C61336D008551E2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; @@ -7928,6 +7938,15 @@ path = SearchQuickLinksMedium; sourceTree = ""; }; + 12187B512C89D7C900BA88CA /* OnboardingCard */ = { + isa = PBXGroup; + children = ( + 12187B522C89E15000BA88CA /* NTPOnboardingCardCell.swift */, + 2CA9952D2CA2EFAA001064CC /* NTPOnboardingCardViewModel.swift */, + ); + path = OnboardingCard; + sourceTree = ""; + }; 1D7B78952ADF324E0011E9F2 /* Event Queue */ = { isa = PBXGroup; children = ( @@ -8457,6 +8476,7 @@ 2C61889D2B7A8A21006B70D7 /* NTP */ = { isa = PBXGroup; children = ( + 2CA995262CA2C053001064CC /* NudgeCards */, 2C61889E2B7A8A21006B70D7 /* NTPLayout.swift */, 2C61889F2B7A8A21006B70D7 /* DefaultBrowser.swift */, 2C6188A02B7A8A21006B70D7 /* Customization */, @@ -8654,6 +8674,7 @@ 2C4414412BD7B43F00249464 /* BingDistributionExperiment.swift */, 2C6188F72B7A8A22006B70D7 /* EngineShortcutsExperiment.swift */, 2C6188F82B7A8A22006B70D7 /* EngagementServiceExperiment.swift */, + 12187B4F2C89B43D00BA88CA /* OnboardingCardNTPExperiment.swift */, ); path = Unleash; sourceTree = ""; @@ -8831,6 +8852,16 @@ path = Mocks; sourceTree = ""; }; + 2CA995262CA2C053001064CC /* NudgeCards */ = { + isa = PBXGroup; + children = ( + 12187B512C89D7C900BA88CA /* OnboardingCard */, + 2CA995272CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift */, + 2CA995292CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift */, + ); + path = NudgeCards; + sourceTree = ""; + }; 2CABD71A2C11E07300A0750F /* PersistedGenerated */ = { isa = PBXGroup; children = ( @@ -13919,10 +13950,12 @@ 2C6189382B7A8A22006B70D7 /* EmptyBookmarksViewDelegate.swift in Sources */, C8163851268A0899004C7160 /* AddCredentialViewController.swift in Sources */, 2C61898E2B7A8A22006B70D7 /* UIView+maskedCorners.swift in Sources */, + 12187B502C89B43D00BA88CA /* OnboardingCardNTPExperiment.swift in Sources */, 2C61895F2B7A8A22006B70D7 /* ClimateImpactInfo.swift in Sources */, 2C6189612B7A8A22006B70D7 /* NTPAboutEcosiaCellViewModel.swift in Sources */, 8A19ACB22A3290AE001C2147 /* ClearPrivateDataSetting.swift in Sources */, CA520E7A24913C1B00CCAB48 /* PasswordManagerViewModel.swift in Sources */, + 2CA9952A2CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift in Sources */, 8AE1E1CD27B191110024C45E /* SearchBarSettingsViewModel.swift in Sources */, 43D16B8529831EA5009F8279 /* Style.swift in Sources */, E16258EF2A83BE0800522742 /* FakespotLoadingView.swift in Sources */, @@ -13999,6 +14032,7 @@ C81A8F2526D3ED1900EBA539 /* UIWindow+Extension.swift in Sources */, EBC4869E2195F58300CDA48D /* AboutHomeHandler.swift in Sources */, DDA24A431FD84D630098F159 /* DefaultSearchPrefs.swift in Sources */, + 12187B532C89E15000BA88CA /* NTPOnboardingCardCell.swift in Sources */, E65075611E37F77D006961AC /* MenuHelper.swift in Sources */, 8A7A26E529D4C0A800EA76F1 /* IntroScreenManager.swift in Sources */, 8AB8574827D97CD40075C173 /* HomePanelType.swift in Sources */, @@ -14291,6 +14325,7 @@ C82F4C2B29AE2DF1005BD116 /* NotificationsSettingsViewController.swift in Sources */, 2C6189712B7A8A22006B70D7 /* WelcomeTourGreen.swift in Sources */, 59A68B280D62462B85CF57A4 /* HistoryPanel.swift in Sources */, + 2CA995282CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift in Sources */, C400467C1CF4E43E00B08303 /* BackForwardListViewController.swift in Sources */, D5D237782640BBA600326204 /* ExperimentsSettingsViewController.swift in Sources */, D3972BF31C22412B00035B87 /* ShareExtensionHelper.swift in Sources */, @@ -14306,6 +14341,7 @@ BD4B2DE429BB4D9A005FAA50 /* TimerSnackBar.swift in Sources */, 810FF3582B1784E7009F062C /* PrivateModeAction.swift in Sources */, 21EA466A2B04130500AAAB2D /* TabsPanelState.swift in Sources */, + 2CA9952E2CA2EFAA001064CC /* NTPOnboardingCardViewModel.swift in Sources */, D04D1B862097859B0074B35F /* DownloadToast.swift in Sources */, C29B64EE2AD937D500F3244B /* QRCodeNavigationHandler.swift in Sources */, 8A19ACB42A3290D9001C2147 /* ContentBlockerSetting.swift in Sources */, diff --git a/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 98a30c79027c..3d8770c1df65 100644 --- a/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/braze-inc/braze-swift-sdk", "state" : { - "revision" : "d54bd0676c6dcc630793f1c4e3be53bb5b51b67e", - "version" : "10.2.0" + "revision" : "f6b0226e04d19bb79f7fa57cf9f1aa56abe465ff", + "version" : "10.3.1" } }, { @@ -69,7 +69,7 @@ "location" : "https://github.com/ecosia/ios-core.git", "state" : { "branch" : "main", - "revision" : "89180b4f06dc454bbba64d0ff809894ac827edc2" + "revision" : "956f7bacd4c03723079aea6f66cd1d5e552ebce3" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "6d932a79e7173b275b96c600c86c603cf84f153c", - "version" : "1.17.4" + "revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d", + "version" : "1.17.5" } }, { @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "515f79b522918f83483068d99c68daeb5116342d", - "version" : "600.0.0-prerelease-2024-08-20" + "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", + "version" : "600.0.0" } }, { diff --git a/Client/Coordinators/Browser/BrowserCoordinator.swift b/Client/Coordinators/Browser/BrowserCoordinator.swift index ddf106deb3fe..648e5baa5f27 100644 --- a/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -279,9 +279,10 @@ class BrowserCoordinator: BaseCoordinator, browserViewController.presentIntroViewController() } - private func showIntroOnboarding() { + // Ecosia: Add `forceSkipExperiment` - used for `OnboardingCardNTPExperiment` + private func showIntroOnboarding(skipExperiment: Bool = false) { let introManager = IntroScreenManager(prefs: profile.prefs) - let launchType = LaunchType.intro(manager: introManager) + let launchType = LaunchType.intro(manager: introManager, checkExperiment: !skipExperiment) startLaunch(with: launchType) } @@ -583,6 +584,11 @@ class BrowserCoordinator: BaseCoordinator, router.present(navigationController) } + + // Ecosia: Used for `OnboardingCardNTPExperiment` + func showOnboarding() { + showIntroOnboarding(skipExperiment: true) + } // MARK: - ParentCoordinatorDelegate func didFinish(from childCoordinator: Coordinator) { diff --git a/Client/Coordinators/Browser/BrowserNavigationHandler.swift b/Client/Coordinators/Browser/BrowserNavigationHandler.swift index cea3b05b74d4..108b416f3492 100644 --- a/Client/Coordinators/Browser/BrowserNavigationHandler.swift +++ b/Client/Coordinators/Browser/BrowserNavigationHandler.swift @@ -73,6 +73,10 @@ protocol BrowserNavigationHandler: AnyObject, QRCodeNavigationHandler { /// Shows the Tab Tray View Controller. func showTabTray(selectedPanel: TabTrayPanelType) + + // Ecosia: Used for `OnboardingCardNTPExperiment` + /// Present Onboarding. + func showOnboarding() } extension BrowserNavigationHandler { diff --git a/Client/Coordinators/Launch/LaunchCoordinator.swift b/Client/Coordinators/Launch/LaunchCoordinator.swift index bdb1121b295d..853669e3f6d3 100644 --- a/Client/Coordinators/Launch/LaunchCoordinator.swift +++ b/Client/Coordinators/Launch/LaunchCoordinator.swift @@ -29,8 +29,27 @@ class LaunchCoordinator: BaseCoordinator, func start(with launchType: LaunchType) { let isFullScreen = launchType.isFullScreenAvailable(isIphone: isIphone) switch launchType { - case .intro(let manager): + /* Ecosia: Change to support `OnboardingCardNTPExperiment` conditions + case .intro(let manager): presentIntroOnboarding(with: manager, isFullScreen: isFullScreen) + */ + case .intro(let manager, let checkExperiment): + guard checkExperiment else { + presentIntroOnboarding(with: manager, isFullScreen: isFullScreen) + return + } + // TODO: Refactor `FeatureManagement.fetchConfiguration()` pre-condition - maybe a notification from FeatureManagement? + Task { + await FeatureManagement.fetchConfiguration() + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + guard !OnboardingCardNTPExperiment.isEnabled else { + self.parentCoordinator?.didFinishLaunch(from: self) + return + } + self.presentIntroOnboarding(with: manager, isFullScreen: isFullScreen) + } + } case .update(let viewModel): presentUpdateOnboarding(with: viewModel, isFullScreen: isFullScreen) case .defaultBrowser: diff --git a/Client/Coordinators/Launch/LaunchType.swift b/Client/Coordinators/Launch/LaunchType.swift index 8b3a44c68b2c..0b178fee7f5d 100644 --- a/Client/Coordinators/Launch/LaunchType.swift +++ b/Client/Coordinators/Launch/LaunchType.swift @@ -11,8 +11,9 @@ enum LaunchCoordinatorType { } enum LaunchType { + // Ecosia: Add `checkExperiment` - used for `OnboardingCardNTPExperiment` /// Showing the intro onboarding - case intro(manager: IntroScreenManager) + case intro(manager: IntroScreenManager, checkExperiment: Bool = true) /// Show the update onboarding case update(viewModel: UpdateViewModel) diff --git a/Client/Ecosia/Analytics/Analytics.Values.swift b/Client/Ecosia/Analytics/Analytics.Values.swift index 76058f7ff110..ab7f78ec9f77 100644 --- a/Client/Ecosia/Analytics/Analytics.Values.swift +++ b/Client/Ecosia/Analytics/Analytics.Values.swift @@ -45,7 +45,8 @@ extension Analytics { topSites = "top_sites", impact, news, - about + about, + onboardingCard = "onboarding_card" } enum Browser: String { diff --git a/Client/Ecosia/Analytics/Analytics.swift b/Client/Ecosia/Analytics/Analytics.swift index 99e99c90eeee..e56c7119aa11 100644 --- a/Client/Ecosia/Analytics/Analytics.swift +++ b/Client/Ecosia/Analytics/Analytics.swift @@ -82,6 +82,13 @@ final class Analytics: AnalyticsProtocol { .label(label.rawValue)) } + func ntpOnboardingCardExperiment(_ action: Action) { + track(Structured(category: Category.ntp.rawValue, + action: action.rawValue) + .label(Label.NTP.onboardingCard.rawValue) + .property(OnboardingCardNTPExperiment.analyticsProperty)) + } + func navigation(_ action: Action, label: Label.Navigation) { track(Structured(category: Category.navigation.rawValue, action: action.rawValue) @@ -306,7 +313,7 @@ final class Analytics: AnalyticsProtocol { guard let page else { return } - let event = Structured(category: Category.intro.rawValue, + let event = Structured(category: OnboardingCardNTPExperiment.analyticsIntroCategory ?? Category.intro.rawValue, action: Action.display.rawValue) .property(page.rawValue) .value(.init(integerLiteral: index)) @@ -317,7 +324,7 @@ final class Analytics: AnalyticsProtocol { guard let page else { return } - let event = Structured(category: Category.intro.rawValue, + let event = Structured(category: OnboardingCardNTPExperiment.analyticsIntroCategory ?? Category.intro.rawValue, action: Action.click.rawValue) .label(label.rawValue) .property(page.rawValue) @@ -346,7 +353,7 @@ extension Analytics { private func appendTestContextIfNeeded(_ action: Analytics.Action.Activity, _ event: Structured) { switch action { case .resume, .launch: - addABTestContexts(to: event, toggles: [.searchShortcuts, .bingDistribution]) + addABTestContexts(to: event, toggles: [.searchShortcuts, .bingDistribution, .onboardingCardNTP]) addCookieConsentContext(to: event) } } diff --git a/Client/Ecosia/Experiments/Unleash/OnboardingCardNTPExperiment.swift b/Client/Ecosia/Experiments/Unleash/OnboardingCardNTPExperiment.swift new file mode 100644 index 000000000000..117104188fd6 --- /dev/null +++ b/Client/Ecosia/Experiments/Unleash/OnboardingCardNTPExperiment.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 Foundation +import Core + +struct OnboardingCardNTPExperiment { + private enum Variant: String { + case control + case first = "test1" + case second = "test2" + } + + private init() {} + + static var isEnabled: Bool { + Unleash.isEnabled(.onboardingCardNTP) && variant != .control + } + + static private var variant: Variant { + Variant(rawValue: Unleash.getVariant(.onboardingCardNTP).name) ?? .control + } + + // MARK: Analytics + static var analyticsIntroCategory: String? { + isEnabled ? "intro_card" : nil + } + + static var analyticsProperty: String? { + switch variant { + case .first: + return "first_copy" + case .second: + return "second_copy" + default: + return nil + } + } + + /// Send onboarding card view analytics event, but just the first time it's called. + static func trackExperimentImpression() { + let trackExperimentImpressionKey = "onboardingCardNTPExperimentImpression" + guard !UserDefaults.standard.bool(forKey: trackExperimentImpressionKey) else { + return + } + Analytics.shared.ntpOnboardingCardExperiment(.view) + UserDefaults.standard.setValue(true, forKey: trackExperimentImpressionKey) + } + + // MARK: Card dismissed + static private let cardDismissedKey = "onboardingCardNTPExperimentDismissed" + + static var shouldShowCard: Bool { + isEnabled && !UserDefaults.standard.bool(forKey: cardDismissedKey) + } + + static func setCardDismissed() { + UserDefaults.standard.set(true, forKey: cardDismissedKey) + } + + // MARK: Texts + static var title: String { + switch variant { + case .first: + return .localized(.onboardingCardNTPExperimentTitle1) + case .second: + return .localized(.onboardingCardNTPExperimentTitle2) + default: + return "" + } + } + + static var description: String { + switch variant { + case .first: + return .localized(.onboardingCardNTPExperimentDescription1) + case .second: + return .localized(.onboardingCardNTPExperimentDescription2) + default: + return "" + } + } + + static var buttonTitle: String { + switch variant { + case .first: + return .localized(.onboardingCardNTPExperimentButtonText1) + case .second: + return .localized(.onboardingCardNTPExperimentButtonText2) + default: + return "" + } + } +} diff --git a/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift b/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift index 39eb9c4b3b63..7d44859fe616 100644 --- a/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift +++ b/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift @@ -157,6 +157,7 @@ extension AppSettingsTableViewController { ResetSearchCount(settings: self), EngagementServiceIdentifierSetting(settings: self), FasterInactiveTabs(settings: self, settingsDelegate: self), + UnleashOnboardingCardNTPSetting(settings: self), ] return SettingSection(title: NSAttributedString(string: "Debug"), children: hiddenDebugSettings) diff --git a/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift b/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift index fbd8c7d78e68..4047c989bc8c 100644 --- a/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift +++ b/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift @@ -83,6 +83,34 @@ extension HomepageViewController: NTPLibraryDelegate { } } +extension HomepageViewController: NTPConfigurableNudgeCardCellDelegate { + + func nudgeCardRequestToDimiss(for cardType: HomepageSectionType) { + switch cardType { + case .onboardingCard: + OnboardingCardNTPExperiment.setCardDismissed() + Analytics.shared.ntpOnboardingCardExperiment(.dismiss) + default: + return + } + + reloadView() + } + + func nudgeCardRequestToPerformAction(for cardType: HomepageSectionType) { + switch cardType { + case .onboardingCard: + browserNavigationHandler?.showOnboarding() + OnboardingCardNTPExperiment.setCardDismissed() + Analytics.shared.ntpOnboardingCardExperiment(.click) + default: + return + } + + reloadView() + } +} + extension HomepageViewController: NTPImpactCellDelegate { func impactCellButtonClickedWithInfo(_ info: ClimateImpactInfo) { switch info { diff --git a/Client/Ecosia/Frontend/Home/EcosiaHomepageSectionType.swift b/Client/Ecosia/Frontend/Home/EcosiaHomepageSectionType.swift index d40bb4b44e78..114739d74ab5 100644 --- a/Client/Ecosia/Frontend/Home/EcosiaHomepageSectionType.swift +++ b/Client/Ecosia/Frontend/Home/EcosiaHomepageSectionType.swift @@ -11,6 +11,7 @@ import Foundation enum HomepageSectionType: Int, CaseIterable { case logoHeader + case onboardingCard case libraryShortcuts case topSites case impact @@ -21,6 +22,7 @@ enum HomepageSectionType: Int, CaseIterable { var cellIdentifier: String { switch self { case .logoHeader: return NTPLogoCell.cellIdentifier + case .onboardingCard: return NTPOnboardingCardCell.cellIdentifier case .libraryShortcuts: return NTPLibraryCell.cellIdentifier case .topSites: return "" // Top sites has more than 1 cell type, dequeuing is done through FxHomeSectionHandler protocol case .impact: return NTPImpactCell.cellIdentifier @@ -36,6 +38,7 @@ enum HomepageSectionType: Int, CaseIterable { TopSiteItemCell.self, EmptyTopSiteCell.self, NTPLibraryCell.self, + NTPOnboardingCardCell.self, NTPImpactCell.self, NTPNewsCell.self, NTPAboutEcosiaCell.self, @@ -53,7 +56,7 @@ private let MinimumInsets: CGFloat = 16 extension HomepageSectionType { var customizableConfig: CustomizableNTPSettingConfig? { switch self { - case .logoHeader, .libraryShortcuts, .ntpCustomization: return nil + case .logoHeader, .onboardingCard, .libraryShortcuts, .ntpCustomization: return nil case .topSites: return .topSites case .impact: return .climateImpact case .aboutEcosia: return .aboutEcosia @@ -61,9 +64,11 @@ extension HomepageSectionType { } } - func sectionInsets(_ traits: UITraitCollection, bottomSpacing: CGFloat = 32) -> NSDirectionalEdgeInsets { + func sectionInsets(_ traits: UITraitCollection, + topSpacing: CGFloat = 0, + bottomSpacing: CGFloat = 32) -> NSDirectionalEdgeInsets { switch self { - case .libraryShortcuts, .topSites, .impact, .news, .aboutEcosia, .ntpCustomization: + case .libraryShortcuts, .topSites, .impact, .news, .aboutEcosia, .ntpCustomization, .onboardingCard: guard let window = UIApplication.shared.windows.first(where: \.isKeyWindow) else { return NSDirectionalEdgeInsets(top: 0, leading: MinimumInsets, @@ -80,7 +85,7 @@ extension HomepageSectionType { if traits.horizontalSizeClass == .regular || (orientation.isLandscape && traits.userInterfaceIdiom == .phone) { horizontal = window.bounds.width / 4 } - return NSDirectionalEdgeInsets(top: 0, + return NSDirectionalEdgeInsets(top: topSpacing, leading: horizontal, bottom: bottomSpacing, trailing: horizontal) diff --git a/Client/Ecosia/L10N/String.swift b/Client/Ecosia/L10N/String.swift index 3a4d28cb3584..11fc3853ef0e 100644 --- a/Client/Ecosia/L10N/String.swift +++ b/Client/Ecosia/L10N/String.swift @@ -246,5 +246,6 @@ extension String { case onboardingCardNTPExperimentTitle2 = "Wondering how it all works?" case onboardingCardNTPExperimentDescription2 = "Take our welcome tour to learn more about how we contribute to climate action." case onboardingCardNTPExperimentButtonText2 = "Learn more about Ecosia" + case configurableNudgeCardCloseButtonAccessibilityLabel = "Close card button" } } diff --git a/Client/Ecosia/Settings/EcosiaDebugSettings.swift b/Client/Ecosia/Settings/EcosiaDebugSettings.swift index ffa4172a053d..5cc876d150c4 100644 --- a/Client/Ecosia/Settings/EcosiaDebugSettings.swift +++ b/Client/Ecosia/Settings/EcosiaDebugSettings.swift @@ -191,6 +191,16 @@ class UnleashVariantResetSetting: HiddenSetting { } } +final class UnleashOnboardingCardNTPSetting: UnleashVariantResetSetting { + override var titleName: String? { + "Onboarding card NTP" + } + + override var variant: Unleash.Variant? { + Unleash.getVariant(.onboardingCardNTP) + } +} + final class EngagementServiceIdentifierSetting: HiddenSetting { override var title: NSAttributedString? { return NSAttributedString(string: "Debug: Engagement Service Identifier parameter", attributes: [NSAttributedString.Key.foregroundColor: UIColor.legacyTheme.tableView.rowText]) diff --git a/Client/Ecosia/UI/Ecosia.xcassets/closeButton.imageset/Contents.json b/Client/Ecosia/UI/Ecosia.xcassets/closeButtonFilled.imageset/Contents.json similarity index 100% rename from Client/Ecosia/UI/Ecosia.xcassets/closeButton.imageset/Contents.json rename to Client/Ecosia/UI/Ecosia.xcassets/closeButtonFilled.imageset/Contents.json diff --git a/Client/Ecosia/UI/Ecosia.xcassets/closeButton.imageset/closeButton.pdf b/Client/Ecosia/UI/Ecosia.xcassets/closeButtonFilled.imageset/closeButton.pdf similarity index 100% rename from Client/Ecosia/UI/Ecosia.xcassets/closeButton.imageset/closeButton.pdf rename to Client/Ecosia/UI/Ecosia.xcassets/closeButtonFilled.imageset/closeButton.pdf diff --git a/Client/Ecosia/UI/Ecosia.xcassets/closeButtonStandard.imageset/Contents.json b/Client/Ecosia/UI/Ecosia.xcassets/closeButtonStandard.imageset/Contents.json new file mode 100644 index 000000000000..c234ec3a1dc4 --- /dev/null +++ b/Client/Ecosia/UI/Ecosia.xcassets/closeButtonStandard.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "close.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Client/Ecosia/UI/Ecosia.xcassets/closeButtonStandard.imageset/close.pdf b/Client/Ecosia/UI/Ecosia.xcassets/closeButtonStandard.imageset/close.pdf new file mode 100644 index 000000000000..42bc285ba7c2 Binary files /dev/null and b/Client/Ecosia/UI/Ecosia.xcassets/closeButtonStandard.imageset/close.pdf differ diff --git a/Client/Ecosia/UI/Ecosia.xcassets/onboardingExperimentNudgeCardAccessoryImage.imageset/Contents.json b/Client/Ecosia/UI/Ecosia.xcassets/onboardingExperimentNudgeCardAccessoryImage.imageset/Contents.json new file mode 100644 index 000000000000..0385d2e3040c --- /dev/null +++ b/Client/Ecosia/UI/Ecosia.xcassets/onboardingExperimentNudgeCardAccessoryImage.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "nudgeCardDefaultImage.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Client/Ecosia/UI/Ecosia.xcassets/onboardingExperimentNudgeCardAccessoryImage.imageset/nudgeCardDefaultImage.pdf b/Client/Ecosia/UI/Ecosia.xcassets/onboardingExperimentNudgeCardAccessoryImage.imageset/nudgeCardDefaultImage.pdf new file mode 100644 index 000000000000..b9a2193dd50d Binary files /dev/null and b/Client/Ecosia/UI/Ecosia.xcassets/onboardingExperimentNudgeCardAccessoryImage.imageset/nudgeCardDefaultImage.pdf differ diff --git a/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift b/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift index f1f5ded753b5..ba327e22c8b7 100644 --- a/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift +++ b/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift @@ -35,7 +35,7 @@ extension NTPTooltip { } class func highlight(for user: Core.User = User.shared) -> NTPTooltip.Highlight? { - guard !user.firstTime else { return nil } + guard !user.firstTime, !OnboardingCardNTPExperiment.isEnabled else { return nil } if user.referrals.isNewClaim { return .gotClaimed diff --git a/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCell.swift b/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCell.swift new file mode 100644 index 000000000000..2f2ffa09f18e --- /dev/null +++ b/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCell.swift @@ -0,0 +1,203 @@ +// 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 UIKit +import Core +import Common + +/// Reusable Nudge Card Cell that can be configured with any view model. +class NTPConfigurableNudgeCardCell: UICollectionViewCell, Themeable, ReusableCell { + + // MARK: - UX Constants + private enum UX { + static let cornerRadius: CGFloat = 10 + static let closeButtonWidthHeight: CGFloat = 48 + static let insetMargin: CGFloat = 16 + static let textSpacing: CGFloat = 4 + static let mainContainerSpacing: CGFloat = 4 + static let buttonAdditionalSpacing: CGFloat = 8 + static let imageWidthHeight: CGFloat = 48 + } + + // MARK: - UI Components + + private let mainContainerStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.layer.cornerRadius = UX.cornerRadius + stackView.spacing = UX.mainContainerSpacing + stackView.axis = .horizontal + stackView.alignment = .leading + stackView.isLayoutMarginsRelativeArrangement = true + stackView.directionalLayoutMargins = .init(top: UX.insetMargin, + leading: UX.insetMargin, + bottom: UX.insetMargin, + trailing: UX.insetMargin) + stackView.spacing = UX.textSpacing + return stackView + }() + + private let labelsAndActionButtonStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = UX.textSpacing + return stackView + }() + + private let closeButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(.init(named: "closeButtonStandard"), for: .normal) + button.contentMode = .top + button.imageView?.contentMode = .scaleAspectFill + button.addTarget(self, action: #selector(closeAction), for: .touchUpInside) + button.setContentHuggingPriority(.required, for: .horizontal) + return button + }() + + private lazy var imageView: UIImageView = { + let image = UIImageView() + image.translatesAutoresizingMaskIntoConstraints = false + image.contentMode = .scaleAspectFit + return image + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .preferredFont(forTextStyle: .headline).bold() + label.adjustsFontForContentSizeCategory = true + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + return label + }() + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .preferredFont(forTextStyle: .subheadline) + label.adjustsFontForContentSizeCategory = true + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + return label + }() + + private let actionButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.titleLabel?.adjustsFontForContentSizeCategory = true + button.titleLabel?.font = .preferredFont(forTextStyle: .subheadline) + button.setContentCompressionResistancePriority(.required, for: .horizontal) + button.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) + button.setInsets(forContentPadding: .init(top: UX.buttonAdditionalSpacing, left: 0, bottom: 0, right: 0), imageTitlePadding: 0) + return button + }() + + // MARK: - Properties + + private var viewModel: NTPConfigurableNudgeCardCellViewModel? + + // MARK: - Delegate + + weak var delegate: NTPConfigurableNudgeCardCellDelegate? + + // MARK: - Themeable Properties + + var themeManager: ThemeManager { AppContainer.shared.resolve() } + var themeObserver: NSObjectProtocol? + var notificationCenter: NotificationProtocol = NotificationCenter.default + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + private func setup() { + contentView.addSubview(mainContainerStackView) + + labelsAndActionButtonStackView.addArrangedSubview(titleLabel) + labelsAndActionButtonStackView.addArrangedSubview(descriptionLabel) + labelsAndActionButtonStackView.addArrangedSubview(actionButton) + + mainContainerStackView.addArrangedSubview(imageView) + mainContainerStackView.addArrangedSubview(labelsAndActionButtonStackView) + mainContainerStackView.addArrangedSubview(closeButton) + + NSLayoutConstraint.activate([ + mainContainerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + mainContainerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + mainContainerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + mainContainerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + closeButton.widthAnchor.constraint(equalToConstant: UX.closeButtonWidthHeight), + closeButton.heightAnchor.constraint(equalToConstant: UX.closeButtonWidthHeight), + imageView.heightAnchor.constraint(equalToConstant: UX.imageWidthHeight), + imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), + ]) + + applyTheme() + listenForThemeChange(contentView) + } + + // MARK: - Configuration Method + + /// Configures the Nudge Card Cell using the ViewModel. + func configure(with viewModel: NTPConfigurableNudgeCardCellViewModel) { + + titleLabel.text = viewModel.title + descriptionLabel.text = viewModel.description + actionButton.setTitle(viewModel.buttonText, for: .normal) + + if let image = viewModel.image { + imageView.image = image + imageView.isHidden = false + } else { + imageView.isHidden = true + } + + closeButton.isHidden = !viewModel.showsCloseButton + self.viewModel = viewModel + delegate = viewModel.delegate + + // Apply accessibility updates + configureAccessibility() + } + + private func configureAccessibility() { + // Set accessibility labels and traits based on the ViewModel + titleLabel.accessibilityLabel = viewModel?.title + descriptionLabel.accessibilityLabel = viewModel?.description + actionButton.accessibilityLabel = viewModel?.buttonText + closeButton.accessibilityLabel = .localized(.configurableNudgeCardCloseButtonAccessibilityLabel) + } + + // MARK: - Theming + @objc func applyTheme() { + // Apply theming based on the provided theme from the ViewModel + mainContainerStackView.backgroundColor = .legacyTheme.ecosia.secondaryBackground + closeButton.tintColor = .legacyTheme.ecosia.decorativeIcon + titleLabel.textColor = .legacyTheme.ecosia.primaryText + descriptionLabel.textColor = .legacyTheme.ecosia.secondaryText + actionButton.setTitleColor(.legacyTheme.ecosia.primaryButton, for: .normal) + } + + @objc private func closeAction() { + guard let cardSectionType = viewModel?.cardSectionType else { return } + delegate?.nudgeCardRequestToDimiss(for: cardSectionType) + } + + @objc private func actionButtonTapped() { + guard let cardSectionType = viewModel?.cardSectionType else { return } + delegate?.nudgeCardRequestToPerformAction(for: cardSectionType) + } +} diff --git a/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCellViewModel.swift b/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCellViewModel.swift new file mode 100644 index 000000000000..6f4720142db6 --- /dev/null +++ b/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCellViewModel.swift @@ -0,0 +1,100 @@ +// 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 Common + +/// Delegate that forwards events to the Cell to let perform its appropriate actions. +/// The `cardType` corresponds to the section type we will always need to define for each card. +protocol NTPConfigurableNudgeCardCellDelegate: AnyObject { + func nudgeCardRequestToDimiss(for cardType: HomepageSectionType) + func nudgeCardRequestToPerformAction(for cardType: HomepageSectionType) +} + +/// ViewModel for configuring a Nudge Card Cell. +class NTPConfigurableNudgeCardCellViewModel: HomepageViewModelProtocol { + + var title: String + var description: String + var buttonText: String + var image: UIImage? + var showsCloseButton: Bool + var cardSectionType: HomepageSectionType + var identifier: String? + var theme: Theme + weak var delegate: NTPConfigurableNudgeCardCellDelegate? + + /// Initializes the ViewModel with the required properties to configure a card. + /// - Parameters: + /// - title: Title text for the card. + /// - description: Description text for the card. + /// - buttonText: Text to display on the action button. + /// - image: Optional image to display on the card. + /// - showsCloseButton: Boolean to show or hide the close button. + /// - cardType: The associated `HomepageSectionType` for a given card. Used by the NTP to make each card a single section. + /// - identifier: Optional unique identifier for the card. + /// - theme: The current theme for styling the card. + init(title: String, + description: String, + buttonText: String, + image: UIImage? = nil, + showsCloseButton: Bool = true, + cardType: HomepageSectionType, + identifier: String? = nil, + theme: Theme) { + + self.title = title + self.description = description + self.buttonText = buttonText + self.image = image + self.showsCloseButton = showsCloseButton + self.cardSectionType = cardType + self.theme = theme + self.identifier = identifier ?? sectionType.cellIdentifier + } + + func setTheme(theme: Theme) { + self.theme = theme + } + + var sectionType: HomepageSectionType { + cardSectionType + } + + var headerViewModel: LabelButtonHeaderViewModel { + return .emptyHeader + } + + func section(for traitCollection: UITraitCollection, size: CGSize) -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(200)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(200)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1) + + let section = NSCollectionLayoutSection(group: group) + + section.contentInsets = sectionType.sectionInsets(traitCollection, topSpacing: 24) + + return section + } + + func numberOfItemsInSection() -> Int { + return 1 + } + + var isEnabled: Bool { + fatalError("Needs to be implemented") + } +} + +extension NTPConfigurableNudgeCardCellViewModel: HomepageSectionHandler { + + func configure(_ cell: UICollectionViewCell, at indexPath: IndexPath) -> UICollectionViewCell { + (cell as? NTPConfigurableNudgeCardCell)?.configure(with: self) + return cell + } +} + diff --git a/Client/Ecosia/UI/NTP/NudgeCards/OnboardingCard/NTPOnboardingCardCell.swift b/Client/Ecosia/UI/NTP/NudgeCards/OnboardingCard/NTPOnboardingCardCell.swift new file mode 100644 index 000000000000..9adc88227539 --- /dev/null +++ b/Client/Ecosia/UI/NTP/NudgeCards/OnboardingCard/NTPOnboardingCardCell.swift @@ -0,0 +1,9 @@ +// 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 UIKit +import Core +import Common + +final class NTPOnboardingCardCell: NTPConfigurableNudgeCardCell {} diff --git a/Client/Ecosia/UI/NTP/NudgeCards/OnboardingCard/NTPOnboardingCardViewModel.swift b/Client/Ecosia/UI/NTP/NudgeCards/OnboardingCard/NTPOnboardingCardViewModel.swift new file mode 100644 index 000000000000..96356adffaf7 --- /dev/null +++ b/Client/Ecosia/UI/NTP/NudgeCards/OnboardingCard/NTPOnboardingCardViewModel.swift @@ -0,0 +1,12 @@ +// 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 Foundation + +final class NTPOnboardingCardViewModel: NTPConfigurableNudgeCardCellViewModel { + + override var isEnabled: Bool { + OnboardingCardNTPExperiment.shouldShowCard + } +} diff --git a/Client/Ecosia/UI/Onboarding/Welcome.swift b/Client/Ecosia/UI/Onboarding/Welcome.swift index 240c6ad857ad..bc890c6bba63 100644 --- a/Client/Ecosia/UI/Onboarding/Welcome.swift +++ b/Client/Ecosia/UI/Onboarding/Welcome.swift @@ -161,16 +161,18 @@ final class Welcome: UIViewController { stack.addArrangedSubview(UIView()) stack.addArrangedSubview(cta) - let skipButton = UIButton(type: .system) - skipButton.backgroundColor = .clear - skipButton.titleLabel?.font = .preferredFont(forTextStyle: .callout) - skipButton.titleLabel?.adjustsFontForContentSizeCategory = true - skipButton.setTitleColor(.Dark.Text.secondary, for: .normal) - skipButton.setTitle(.localized(.skipWelcomeTour), for: .normal) - skipButton.heightAnchor.constraint(equalToConstant: 50).isActive = true - skipButton.addTarget(self, action: #selector(skip), for: .primaryActionTriggered) - - stack.addArrangedSubview(skipButton) + if !OnboardingCardNTPExperiment.isEnabled { + let skipButton = UIButton(type: .system) + skipButton.backgroundColor = .clear + skipButton.titleLabel?.font = .preferredFont(forTextStyle: .callout) + skipButton.titleLabel?.adjustsFontForContentSizeCategory = true + skipButton.setTitleColor(.Dark.Text.secondary, for: .normal) + skipButton.setTitle(.localized(.skipWelcomeTour), for: .normal) + skipButton.heightAnchor.constraint(equalToConstant: 50).isActive = true + skipButton.addTarget(self, action: #selector(skip), for: .primaryActionTriggered) + + stack.addArrangedSubview(skipButton) + } if view.traitCollection.userInterfaceIdiom == .phone { stack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16).isActive = true diff --git a/Client/Ecosia/UI/Onboarding/WelcomeTour.swift b/Client/Ecosia/UI/Onboarding/WelcomeTour.swift index 6901de5391f3..b48056545350 100644 --- a/Client/Ecosia/UI/Onboarding/WelcomeTour.swift +++ b/Client/Ecosia/UI/Onboarding/WelcomeTour.swift @@ -17,7 +17,7 @@ final class WelcomeTour: UIViewController, Themeable { private weak var titleLabel: UILabel! private weak var subtitleLabel: UILabel! private weak var backButton: UIButton! - private weak var skipButton: UIButton! + private weak var skipButton: UIButton? private weak var pageControl: UIPageControl! private weak var ctaButton: UIButton! private weak var waves: UIImageView! @@ -98,16 +98,24 @@ final class WelcomeTour: UIViewController, Themeable { centerControl.priority = .defaultHigh centerControl.isActive = true - let skipButton = UIButton(type: .system) - skipButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 74).isActive = true - skipButton.addTarget(self, action: #selector(skip), for: .primaryActionTriggered) - skipButton.setContentCompressionResistancePriority(.required, for: .horizontal) - navStack.addArrangedSubview(skipButton) - skipButton.setTitle(.localized(.skip), for: .normal) - skipButton.accessibilityLabel = .localized(.onboardingSkipTourButtonAccessibility) - skipButton.titleLabel?.font = .preferredFont(forTextStyle: .body) - skipButton.titleLabel?.adjustsFontForContentSizeCategory = true - self.skipButton = skipButton + if OnboardingCardNTPExperiment.isEnabled { + let placeholderView = UIButton(type: .system) + placeholderView.widthAnchor.constraint(greaterThanOrEqualToConstant: 74).isActive = true + placeholderView.setContentCompressionResistancePriority(.required, for: .horizontal) + placeholderView.isAccessibilityElement = false + navStack.addArrangedSubview(placeholderView) + } else { + let skipButton = UIButton(type: .system) + skipButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 74).isActive = true + skipButton.addTarget(self, action: #selector(skip), for: .primaryActionTriggered) + skipButton.setContentCompressionResistancePriority(.required, for: .horizontal) + navStack.addArrangedSubview(skipButton) + skipButton.setTitle(.localized(.skip), for: .normal) + skipButton.accessibilityLabel = .localized(.onboardingSkipTourButtonAccessibility) + skipButton.titleLabel?.font = .preferredFont(forTextStyle: .body) + skipButton.titleLabel?.adjustsFontForContentSizeCategory = true + self.skipButton = skipButton + } let waves = UIImageView(image: .init(named: "onboardingWaves")) waves.translatesAutoresizingMaskIntoConstraints = false @@ -360,7 +368,7 @@ final class WelcomeTour: UIViewController, Themeable { waves.tintColor = .legacyTheme.ecosia.welcomeBackground titleLabel.textColor = .legacyTheme.ecosia.primaryText subtitleLabel.textColor = .legacyTheme.ecosia.secondaryText - skipButton.tintColor = .legacyTheme.ecosia.primaryButton + skipButton?.tintColor = .legacyTheme.ecosia.primaryButton backButton.tintColor = .legacyTheme.ecosia.primaryButton pageControl.pageIndicatorTintColor = .legacyTheme.ecosia.disabled pageControl.currentPageIndicatorTintColor = .legacyTheme.ecosia.primaryButton diff --git a/Client/Frontend/Home/HomepageViewController.swift b/Client/Frontend/Home/HomepageViewController.swift index 27ec0cc5b0b8..f5fe147c8429 100644 --- a/Client/Frontend/Home/HomepageViewController.swift +++ b/Client/Frontend/Home/HomepageViewController.swift @@ -700,6 +700,7 @@ private extension HomepageViewController { */ // Ecosia: Adjust HomePageViewController's CollectionView viewModel.libraryViewModel.delegate = self + viewModel.onboardingCardViewModel.delegate = self viewModel.impactViewModel.delegate = self viewModel.newsViewModel.delegate = self viewModel.aboutEcosiaViewModel.delegate = self diff --git a/Client/Frontend/Home/HomepageViewModel.swift b/Client/Frontend/Home/HomepageViewModel.swift index 57203a0964d4..d80190b97c22 100644 --- a/Client/Frontend/Home/HomepageViewModel.swift +++ b/Client/Frontend/Home/HomepageViewModel.swift @@ -117,10 +117,16 @@ class HomepageViewModel: FeatureFlaggable { // Ecosia: Add Ecosia's ViewModels var libraryViewModel: NTPLibraryCellViewModel + var onboardingCardViewModel: NTPOnboardingCardViewModel var impactViewModel: NTPImpactCellViewModel var newsViewModel: NTPNewsCellViewModel var aboutEcosiaViewModel: NTPAboutEcosiaCellViewModel var ntpCustomizationViewModel: NTPCustomizationCellViewModel + /* + Ecosia: Represents the container that stores some of the `HomepageSectionType`s. + The earlier a section type appears in the array, the higher its priority. + */ + private let cardsPrioritySectionTypes: [HomepageSectionType] = [.onboardingCard] // MARK: - Initializers init(profile: Profile, @@ -148,6 +154,12 @@ class HomepageViewModel: FeatureFlaggable { wallpaperManager: wallpaperManager) // Ecosia: Add Ecosia's ViewModels self.libraryViewModel = NTPLibraryCellViewModel(theme: theme) + self.onboardingCardViewModel = NTPOnboardingCardViewModel(title: OnboardingCardNTPExperiment.title, + description: OnboardingCardNTPExperiment.description, + buttonText: OnboardingCardNTPExperiment.buttonTitle, + image: .init(named: "onboardingExperimentNudgeCardAccessoryImage"), + cardType: .onboardingCard, + theme: theme) self.impactViewModel = NTPImpactCellViewModel(referrals: referrals, theme: theme) self.newsViewModel = NTPNewsCellViewModel(theme: theme) self.aboutEcosiaViewModel = NTPAboutEcosiaCellViewModel(theme: theme) @@ -205,7 +217,9 @@ class HomepageViewModel: FeatureFlaggable { customizeButtonViewModel ] */ + // Ecosia: Those models needs to follow strictly the order defined in `enum HomepageSectionType` self.childViewModels = [headerViewModel, + onboardingCardViewModel, libraryViewModel, topSiteViewModel, impactViewModel, @@ -278,10 +292,29 @@ class HomepageViewModel: FeatureFlaggable { func updateEnabledSections() { shownSections.removeAll() - + + /* Ecosia: Handle priority of cards view models + childViewModels.forEach { + if $0.shouldShow { shownSections.append($0.sectionType) } + } + */ + var prioritySectionAdded = false childViewModels.forEach { - if $0.shouldShow { shownSections.append($0.sectionType) } + if $0.shouldShow { + if cardsPrioritySectionTypes.contains($0.sectionType) { + if !prioritySectionAdded { + shownSections.append($0.sectionType) + prioritySectionAdded = true + } + // If a priority section has already been added, skip the rest + } else { + // Non-priority section, add if shouldShow is true + shownSections.append($0.sectionType) + } + } + // If shouldShow is false, skip this viewModel } + logger.log("Homepage amount of sections shown \(shownSections.count)", level: .debug, category: .homepage) diff --git a/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift b/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift index b7746ee5cb6b..ace1269e95eb 100644 --- a/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift +++ b/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift @@ -14,6 +14,9 @@ class EcosiaNTPTooltipHighlightTests: XCTestCase { try? FileManager().removeItem(at: FileManager.user) user = .init() user.firstTime = false + + // Ecosia: Mocking `OnboardingCardNTPExperiment` as disabled + Unleash.model = Unleash.Model() } func testFirstTimeReturnsNil() throws { diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Launch/LaunchCoordinatorTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Launch/LaunchCoordinatorTests.swift index 9aeac810c9cf..6864188ab945 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Launch/LaunchCoordinatorTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Launch/LaunchCoordinatorTests.swift @@ -40,7 +40,7 @@ final class LaunchCoordinatorTests: XCTestCase { func testStart_introNotIphone_present() throws { let introScreenManager = IntroScreenManager(prefs: profile.prefs) let subject = createSubject(isIphone: false) - subject.start(with: .intro(manager: introScreenManager)) + subject.start(with: .intro(manager: introScreenManager, checkExperiment: false)) XCTAssertEqual(mockRouter.presentCalled, 1) XCTAssertEqual(mockRouter.setRootViewControllerCalled, 0) let presentedViewController = try XCTUnwrap(mockRouter.presentedViewController) @@ -52,7 +52,7 @@ final class LaunchCoordinatorTests: XCTestCase { func testStart_introIsIphone_setRootView() throws { let introScreenManager = IntroScreenManager(prefs: profile.prefs) let subject = createSubject(isIphone: true) - subject.start(with: .intro(manager: introScreenManager)) + subject.start(with: .intro(manager: introScreenManager, checkExperiment: false)) XCTAssertEqual(mockRouter.presentCalled, 1) XCTAssertEqual(mockRouter.setRootViewControllerCalled, 0) let pushedVC = try XCTUnwrap(mockRouter.presentedViewController) diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift index 8cab3ffac40c..9a76e4269d66 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift @@ -89,4 +89,7 @@ class MockBrowserCoordinator: BrowserNavigationHandler, ParentCoordinatorDelegat parentViewController: UIViewController) { updateFakespotSidebarCalled += 1 } + + // Ecosia: Used for `OnboardingCardNTPExperiment` + func showOnboarding() { } }