diff --git a/BrowserKit/Sources/Common/Theming/EcosiaThemeColourPalette.swift b/BrowserKit/Sources/Common/Theming/EcosiaThemeColourPalette.swift index da1a41c73f346..bdee034429c0b 100644 --- a/BrowserKit/Sources/Common/Theming/EcosiaThemeColourPalette.swift +++ b/BrowserKit/Sources/Common/Theming/EcosiaThemeColourPalette.swift @@ -60,6 +60,7 @@ public protocol EcosiaSemanticColors { var textPrimary: UIColor { get } var textInversePrimary: UIColor { get } var textSecondary: UIColor { get } + var textStaticLight: UIColor { get } } public protocol EcosiaThemeColourPalette: ThemeColourPalette { @@ -103,4 +104,5 @@ class FakeEcosiaSemanticColors: EcosiaSemanticColors { var textPrimary: UIColor = .systemGray var textInversePrimary: UIColor = .systemGray var textSecondary: UIColor = .systemGray + var textStaticLight: UIColor = .systemGray } diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 0391a001aa714..d456b0a4ba1d1 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -76,6 +76,12 @@ 1214167C2D6375960097788B /* MainFrameAtDocumentEnd.js in Resources */ = {isa = PBXBuildFile; fileRef = 121416792D6375960097788B /* MainFrameAtDocumentEnd.js */; }; 12141E6C2D6877EB0097788B /* EcosiaNetError.html in Resources */ = {isa = PBXBuildFile; fileRef = 12141E682D686C030097788B /* EcosiaNetError.html */; }; 12141E6D2D6877F00097788B /* EcosiaNetError.css in Resources */ = {isa = PBXBuildFile; fileRef = 12141E6A2D686C1C0097788B /* EcosiaNetError.css */; }; + 121C6E182EC1EF870060319A /* BrowserViewController+WelcomeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 121C6E172EC1EF870060319A /* BrowserViewController+WelcomeTransition.swift */; }; + 121C6E1C2EC38B600060319A /* FadeTransitionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 121C6E1B2EC38B600060319A /* FadeTransitionDelegate.swift */; }; + 1230CFF22EBB990900D7AC00 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 126509842CD925B40011BA36 /* BrazeKit */; }; + 1230CFF32EBB990900D7AC00 /* BrazeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 126509862CD925B40011BA36 /* BrazeUI */; }; + 1230D24D2EBCEE9B00D7AC00 /* welcome_background.mov in Resources */ = {isa = PBXBuildFile; fileRef = 1230D24C2EBCEE9B00D7AC00 /* welcome_background.mov */; }; + 123297FC2EC5E6AD004564E9 /* LoopingVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123297FB2EC5E6A5004564E9 /* LoopingVideoPlayer.swift */; }; 123475C32DA6730A0017B0C2 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A6AB4528CA6A4C00EBEBDD /* String+Extension.swift */; }; 123475CC2DA7D6620017B0C2 /* BeforeOrAfterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123475CB2DA7D6580017B0C2 /* BeforeOrAfterView.swift */; }; 12348FFA2DB8D2D40017B0C2 /* PrefsKeys+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12348FF92DB8D2CC0017B0C2 /* PrefsKeys+Ecosia.swift */; }; @@ -84,6 +90,7 @@ 124DAE8A2D3512FA0050104C /* DispatchQueueHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83821FF1FC7961D00303C12 /* DispatchQueueHelper.swift */; }; 1251623C2D59FB30005CB958 /* Ecosia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CFE99662D45329200B25CE0 /* Ecosia.framework */; }; 1251623D2D59FB31005CB958 /* Ecosia.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 2CFE99662D45329200B25CE0 /* Ecosia.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 129F18FE2EC5F04200E870C0 /* Task+Sleep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 129F18FD2EC5F04200E870C0 /* Task+Sleep.swift */; }; 12A536342D3E637800924CB0 /* DownloadHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA8D1C61BA037F500C8AE9E /* DownloadHelper.swift */; }; 12A536352D3E63A200924CB0 /* GlobalTabEventHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F819C51FD70F5D009E31E4 /* GlobalTabEventHandlers.swift */; }; 12A536372D3E89BF00924CB0 /* GCDWebServers in Frameworks */ = {isa = PBXBuildFile; productRef = 12A536362D3E89BF00924CB0 /* GCDWebServers */; }; @@ -95,6 +102,7 @@ 12AEB1282D3A568D0035D7D8 /* SessionRestoreHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12AEB1272D3A568D0035D7D8 /* SessionRestoreHandler.swift */; }; 12AEB1292D3A9AE70035D7D8 /* CreditCardPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2999FF02B194A5800F0FEC1 /* CreditCardPayload.swift */; }; 12AEB12A2D3A9AEB0035D7D8 /* FillCreditCardForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2999FF22B194A8300F0FEC1 /* FillCreditCardForm.swift */; }; + 12B0D7952EA789B00022FECA /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B0D7932EA789B00022FECA /* WelcomeView.swift */; }; 12C11E272D281BCC00E4DDBF /* EcosiaHomepageSectionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12C11E212D281B7200E4DDBF /* EcosiaHomepageSectionType.swift */; }; 12C11E282D281BD100E4DDBF /* EcosiaTopSiteItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12C11E222D281B7A00E4DDBF /* EcosiaTopSiteItemCell.swift */; }; 12C11E8D2D2826A600E4DDBF /* EcosiaDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB289252B07C8F000A8FCB3 /* EcosiaDebugSettings.swift */; }; @@ -351,17 +359,14 @@ 2CFE9FF12D4557EF00B25CE0 /* FxNimbus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFE9FED2D4557EF00B25CE0 /* FxNimbus.swift */; }; 2CFE9FF22D4557EF00B25CE0 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFE9FEB2D4557EF00B25CE0 /* Metrics.swift */; }; 2CFEA0542D455BD500B25CE0 /* MarketsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0062D455BD500B25CE0 /* MarketsController.swift */; }; - 2CFEA0552D455BD500B25CE0 /* WelcomeTourRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0402D455BD500B25CE0 /* WelcomeTourRow.swift */; }; 2CFEA0562D455BD500B25CE0 /* NewsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA02B2D455BD500B25CE0 /* NewsController.swift */; }; 2CFEA0572D455BD500B25CE0 /* NTPLogoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0292D455BD500B25CE0 /* NTPLogoCell.swift */; }; 2CFEA0592D455BD500B25CE0 /* NTPImpactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA01F2D455BD500B25CE0 /* NTPImpactCell.swift */; }; 2CFEA05A2D455BD500B25CE0 /* EcosiaPrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFE9FFC2D455BD500B25CE0 /* EcosiaPrimaryButton.swift */; }; - 2CFEA05B2D455BD500B25CE0 /* WelcomeTourProfit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA03F2D455BD500B25CE0 /* WelcomeTourProfit.swift */; }; 2CFEA05C2D455BD500B25CE0 /* WhatsNewLocalDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA04D2D455BD500B25CE0 /* WhatsNewLocalDataProvider.swift */; }; 2CFEA05D2D455BD500B25CE0 /* NTPTooltipDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0372D455BD500B25CE0 /* NTPTooltipDelegate.swift */; }; 2CFEA05E2D455BD500B25CE0 /* EmptyReadingListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0002D455BD500B25CE0 /* EmptyReadingListView.swift */; }; 2CFEA05F2D455BD500B25CE0 /* NTPImpactRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0222D455BD500B25CE0 /* NTPImpactRowView.swift */; }; - 2CFEA0602D455BD500B25CE0 /* WelcomeTourAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA03D2D455BD500B25CE0 /* WelcomeTourAction.swift */; }; 2CFEA0612D455BD500B25CE0 /* EmptyHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFE9FFF2D455BD500B25CE0 /* EmptyHeader.swift */; }; 2CFEA0622D455BD500B25CE0 /* NTPTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0352D455BD500B25CE0 /* NTPTooltip.swift */; }; 2CFEA0632D455BD500B25CE0 /* NTPLibraryShortcutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0272D455BD500B25CE0 /* NTPLibraryShortcutView.swift */; }; @@ -370,16 +375,14 @@ 2CFEA0672D455BD500B25CE0 /* PageActionsShortcutsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0452D455BD500B25CE0 /* PageActionsShortcutsHeader.swift */; }; 2CFEA0682D455BD500B25CE0 /* NTPImpactDividerFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0212D455BD500B25CE0 /* NTPImpactDividerFooter.swift */; }; 2CFEA0692D455BD500B25CE0 /* EmptyBookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFE9FFD2D455BD500B25CE0 /* EmptyBookmarksView.swift */; }; - 2CFEA06B2D455BD500B25CE0 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0392D455BD500B25CE0 /* Welcome.swift */; }; + 2CFEA06B2D455BD500B25CE0 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0392D455BD500B25CE0 /* WelcomeViewController.swift */; }; 2CFEA06C2D455BD500B25CE0 /* ArcProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0112D455BD500B25CE0 /* ArcProgressView.swift */; }; 2CFEA06D2D455BD500B25CE0 /* Sparkle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0182D455BD500B25CE0 /* Sparkle.swift */; }; 2CFEA06E2D455BD500B25CE0 /* CircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0322D455BD500B25CE0 /* CircleButton.swift */; }; 2CFEA06F2D455BD500B25CE0 /* WhatsNewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0512D455BD500B25CE0 /* WhatsNewViewController.swift */; }; 2CFEA0702D455BD500B25CE0 /* UserDefaultsSeedProgressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA00F2D455BD500B25CE0 /* UserDefaultsSeedProgressManager.swift */; }; 2CFEA0712D455BD500B25CE0 /* NTPSeedCounterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0122D455BD500B25CE0 /* NTPSeedCounterCell.swift */; }; - 2CFEA0722D455BD500B25CE0 /* WelcomeTourGreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA03E2D455BD500B25CE0 /* WelcomeTourGreen.swift */; }; 2CFEA0732D455BD500B25CE0 /* WhatsNewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0522D455BD500B25CE0 /* WhatsNewViewModel.swift */; }; - 2CFEA0742D455BD500B25CE0 /* WelcomeTour.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA03B2D455BD500B25CE0 /* WelcomeTour.swift */; }; 2CFEA0752D455BD500B25CE0 /* NTPTooltip.Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0362D455BD500B25CE0 /* NTPTooltip.Highlight.swift */; }; 2CFEA0762D455BD500B25CE0 /* NTPNewsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA02C2D455BD500B25CE0 /* NTPNewsCell.swift */; }; 2CFEA0772D455BD500B25CE0 /* NTPNewsCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA02D2D455BD500B25CE0 /* NTPNewsCellViewModel.swift */; }; @@ -389,7 +392,6 @@ 2CFEA07B2D455BD500B25CE0 /* WhatsNewDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA04C2D455BD500B25CE0 /* WhatsNewDataProvider.swift */; }; 2CFEA07C2D455BD500B25CE0 /* NTPImpactCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0202D455BD500B25CE0 /* NTPImpactCellViewModel.swift */; }; 2CFEA07D2D455BD500B25CE0 /* EmptyBookmarksViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFE9FFE2D455BD500B25CE0 /* EmptyBookmarksViewDelegate.swift */; }; - 2CFEA07E2D455BD500B25CE0 /* WelcomeTour.Step.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA03C2D455BD500B25CE0 /* WelcomeTour.Step.swift */; }; 2CFEA07F2D455BD500B25CE0 /* DefaultBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0332D455BD500B25CE0 /* DefaultBrowserViewController.swift */; }; 2CFEA0802D455BD500B25CE0 /* SeedCounterHiddenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0152D455BD500B25CE0 /* SeedCounterHiddenSettings.swift */; }; 2CFEA0812D455BD500B25CE0 /* WhatsNewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0502D455BD500B25CE0 /* WhatsNewItem.swift */; }; @@ -397,7 +399,6 @@ 2CFEA0832D455BD500B25CE0 /* NTPConfigurableNudgeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA02F2D455BD500B25CE0 /* NTPConfigurableNudgeCardCell.swift */; }; 2CFEA0842D455BD500B25CE0 /* LoadingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0052D455BD500B25CE0 /* LoadingScreen.swift */; }; 2CFEA0852D455BD500B25CE0 /* NTPCustomizationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA01C2D455BD500B25CE0 /* NTPCustomizationCellViewModel.swift */; }; - 2CFEA0862D455BD500B25CE0 /* WelcomeTourTransparent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0412D455BD500B25CE0 /* WelcomeTourTransparent.swift */; }; 2CFEA0872D455BD500B25CE0 /* NTPCustomizationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA01B2D455BD500B25CE0 /* NTPCustomizationCell.swift */; }; 2CFEA0882D455BD500B25CE0 /* SeedCounterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0142D455BD500B25CE0 /* SeedCounterConfig.swift */; }; 2CFEA0892D455BD500B25CE0 /* NTPSeedCounterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFEA0132D455BD500B25CE0 /* NTPSeedCounterViewModel.swift */; }; @@ -2374,15 +2375,21 @@ 1214167A2D6375960097788B /* MainFrameAtDocumentStart.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = MainFrameAtDocumentStart.js; sourceTree = ""; }; 12141E682D686C030097788B /* EcosiaNetError.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = EcosiaNetError.html; sourceTree = ""; }; 12141E6A2D686C1C0097788B /* EcosiaNetError.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = EcosiaNetError.css; sourceTree = ""; }; + 121C6E172EC1EF870060319A /* BrowserViewController+WelcomeTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+WelcomeTransition.swift"; sourceTree = ""; }; + 121C6E1B2EC38B600060319A /* FadeTransitionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FadeTransitionDelegate.swift; sourceTree = ""; }; 123045959E0F295753B4B4DB /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Today.strings; sourceTree = ""; }; + 1230D24C2EBCEE9B00D7AC00 /* welcome_background.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = welcome_background.mov; sourceTree = ""; }; + 123297FB2EC5E6A5004564E9 /* LoopingVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopingVideoPlayer.swift; sourceTree = ""; }; 123475CB2DA7D6580017B0C2 /* BeforeOrAfterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeforeOrAfterView.swift; sourceTree = ""; }; 12348FF92DB8D2CC0017B0C2 /* PrefsKeys+Ecosia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PrefsKeys+Ecosia.swift"; sourceTree = ""; }; 123CEC4B2E7814A2009EB379 /* FoundersGroteskCondensed-Semibold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FoundersGroteskCondensed-Semibold.ttf"; 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 = ""; }; 126F44CCB14373DC7813DE1F /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/ClearPrivateDataConfirm.strings; sourceTree = ""; }; + 129F18FD2EC5F04200E870C0 /* Task+Sleep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Sleep.swift"; sourceTree = ""; }; 12AEB1252D3A51670035D7D8 /* LegacySessionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySessionData.swift; sourceTree = ""; }; 12AEB1272D3A568D0035D7D8 /* SessionRestoreHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRestoreHandler.swift; sourceTree = ""; }; + 12B0D7932EA789B00022FECA /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 12C11E132D28128000E4DDBF /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Common.xcconfig; path = Configuration/Common.xcconfig; sourceTree = ""; }; 12C11E142D2812B200E4DDBF /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configuration/Debug.xcconfig; sourceTree = ""; }; 12C11E152D2812B200E4DDBF /* EcosiaBeta.ShareTo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = EcosiaBeta.ShareTo.xcconfig; path = Configuration/EcosiaBeta.ShareTo.xcconfig; sourceTree = ""; }; @@ -2867,15 +2874,8 @@ 2CFEA0352D455BD500B25CE0 /* NTPTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPTooltip.swift; sourceTree = ""; }; 2CFEA0362D455BD500B25CE0 /* NTPTooltip.Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPTooltip.Highlight.swift; sourceTree = ""; }; 2CFEA0372D455BD500B25CE0 /* NTPTooltipDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPTooltipDelegate.swift; sourceTree = ""; }; - 2CFEA0392D455BD500B25CE0 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; + 2CFEA0392D455BD500B25CE0 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; 2CFEA03A2D455BD500B25CE0 /* WelcomeNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeNavigation.swift; sourceTree = ""; }; - 2CFEA03B2D455BD500B25CE0 /* WelcomeTour.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTour.swift; sourceTree = ""; }; - 2CFEA03C2D455BD500B25CE0 /* WelcomeTour.Step.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTour.Step.swift; sourceTree = ""; }; - 2CFEA03D2D455BD500B25CE0 /* WelcomeTourAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTourAction.swift; sourceTree = ""; }; - 2CFEA03E2D455BD500B25CE0 /* WelcomeTourGreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTourGreen.swift; sourceTree = ""; }; - 2CFEA03F2D455BD500B25CE0 /* WelcomeTourProfit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTourProfit.swift; sourceTree = ""; }; - 2CFEA0402D455BD500B25CE0 /* WelcomeTourRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTourRow.swift; sourceTree = ""; }; - 2CFEA0412D455BD500B25CE0 /* WelcomeTourTransparent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTourTransparent.swift; sourceTree = ""; }; 2CFEA0432D455BD500B25CE0 /* PageActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageActionMenu.swift; sourceTree = ""; }; 2CFEA0442D455BD500B25CE0 /* PageActionMenuCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageActionMenuCell.swift; sourceTree = ""; }; 2CFEA0452D455BD500B25CE0 /* PageActionsShortcutsHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageActionsShortcutsHeader.swift; sourceTree = ""; }; @@ -9657,6 +9657,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1230CFF32EBB990900D7AC00 /* BrazeUI in Frameworks */, + 1230CFF22EBB990900D7AC00 /* BrazeKit in Frameworks */, 5A70EF0E295DFCCF00790249 /* Common in Frameworks */, 8A08EC6427EBDCAD00E119C7 /* AdServices.framework in Frameworks */, 8A08EC6227EBDCA400E119C7 /* iAd.framework in Frameworks */, @@ -10508,6 +10510,7 @@ 2CF2206E2B72B0530038157D /* AppSettingsTableViewController+Ecosia.swift */, 12C11EA02D2828EA00E4DDBF /* DispatchQueueHelper+BuildChannel.swift */, 12348FF92DB8D2CC0017B0C2 /* PrefsKeys+Ecosia.swift */, + 129F18FD2EC5F04200E870C0 /* Task+Sleep.swift */, ); path = Extensions; sourceTree = ""; @@ -10681,15 +10684,13 @@ 2CFEA0422D455BD500B25CE0 /* Onboarding */ = { isa = PBXGroup; children = ( - 2CFEA0392D455BD500B25CE0 /* Welcome.swift */, + 123297FB2EC5E6A5004564E9 /* LoopingVideoPlayer.swift */, + 121C6E172EC1EF870060319A /* BrowserViewController+WelcomeTransition.swift */, + 121C6E1B2EC38B600060319A /* FadeTransitionDelegate.swift */, + 1230D24C2EBCEE9B00D7AC00 /* welcome_background.mov */, 2CFEA03A2D455BD500B25CE0 /* WelcomeNavigation.swift */, - 2CFEA03B2D455BD500B25CE0 /* WelcomeTour.swift */, - 2CFEA03C2D455BD500B25CE0 /* WelcomeTour.Step.swift */, - 2CFEA03D2D455BD500B25CE0 /* WelcomeTourAction.swift */, - 2CFEA03E2D455BD500B25CE0 /* WelcomeTourGreen.swift */, - 2CFEA03F2D455BD500B25CE0 /* WelcomeTourProfit.swift */, - 2CFEA0402D455BD500B25CE0 /* WelcomeTourRow.swift */, - 2CFEA0412D455BD500B25CE0 /* WelcomeTourTransparent.swift */, + 12B0D7932EA789B00022FECA /* WelcomeView.swift */, + 2CFEA0392D455BD500B25CE0 /* WelcomeViewController.swift */, ); path = Onboarding; sourceTree = ""; @@ -15711,6 +15712,7 @@ 8A5CDEED27E510F500CC60FF /* pocketglobalfeed.json in Resources */, 8A1A93582B757C7C0069C190 /* gradient.json in Resources */, 74821FFE1DB6D3AC00EEEA72 /* MailSchemes.plist in Resources */, + 1230D24D2EBCEE9B00D7AC00 /* welcome_background.mov in Resources */, D308EE561CBF0BF5006843F2 /* CertError.css in Resources */, E1AF3566286DE5F800960045 /* Smoketest1.xctestplan in Resources */, 121416752D6373BA0097788B /* AllFramesAtDocumentEnd.js in Resources */, @@ -16217,6 +16219,7 @@ 8AABBCFC2A0010900089941E /* GleanWrapper.swift in Sources */, 8C92DE912A7128CB0090BD28 /* ProductAnalysisResponse.swift in Sources */, 12C11E282D281BD100E4DDBF /* EcosiaTopSiteItemCell.swift in Sources */, + 123297FC2EC5E6AD004564E9 /* LoopingVideoPlayer.swift in Sources */, F85C7F0F271DD154004BDBA4 /* AppAuthenticator.swift in Sources */, D029A04920A62DB0001DB72F /* TemporaryDocument.swift in Sources */, 2CB5C33F2E45F02D00B1048F /* SearchViewController+Ecosia.swift in Sources */, @@ -16365,6 +16368,7 @@ 8A0727462B4890B50071BB9F /* WebviewTelemetry.swift in Sources */, 8ADC2A212A3399DC00543DAA /* YourRightsSetting.swift in Sources */, DF529E9F2AA86FF4003C5373 /* FakespotReviewQualityCardView.swift in Sources */, + 12B0D7952EA789B00022FECA /* WelcomeView.swift in Sources */, 1D0BA05C24F46A0400D731B5 /* TopSitesProvider.swift in Sources */, 0BF0DB941A8545800039F300 /* URLBarView.swift in Sources */, DFACBF7F277B5F7B003D5F41 /* WallpaperBackgroundView.swift in Sources */, @@ -16639,6 +16643,7 @@ 2CE9E8082E43927000141C6D /* NTPHeader.swift in Sources */, 8A4EA0DD2C0117F200E4E4F1 /* MicrosurveyModel.swift in Sources */, 8C44A9D22A6A99FE009A1AA7 /* ShoppingProduct.swift in Sources */, + 129F18FE2EC5F04200E870C0 /* Task+Sleep.swift in Sources */, 63F7A9AA2C7529ED005846F5 /* NativeErrorPageModel.swift in Sources */, 63F7A9AC2C752BB0005846F5 /* NativeErrorPageViewController.swift in Sources */, C88E7A572A0553360072E638 /* OnboardingButtonInfoModel.swift in Sources */, @@ -16729,6 +16734,7 @@ E14F7DF2288F3F9F00E3722C /* ThemedTableSectionHeaderFooterView.swift in Sources */, 8A3EF7F22A2FCF4000796E3A /* DeleteExportedDataSetting.swift in Sources */, 8ADC2A142A33762900543DAA /* ReferringPage.swift in Sources */, + 121C6E1C2EC38B600060319A /* FadeTransitionDelegate.swift in Sources */, 0E6C1E212C909AD7001A43BB /* PasswordGeneratorAction.swift in Sources */, E1442FBF294782B6003680B0 /* CGRect+Extension.swift in Sources */, 96F8DA49280452CA00E53239 /* GleanPlumbContextProvider.swift in Sources */, @@ -16837,6 +16843,7 @@ A5519CF52B5D57560062BECB /* SearchSettingsState.swift in Sources */, AB936A692C05F2B100600F82 /* TrackingProtectionButton.swift in Sources */, 74F80D342A0A52D700013C3D /* PrivacyPolicyViewController.swift in Sources */, + 121C6E182EC1EF870060319A /* BrowserViewController+WelcomeTransition.swift in Sources */, 274A36CE239EB9EC00A21587 /* LibraryViewController+LibraryPanelDelegate.swift in Sources */, C869912D28917688007ACC5C /* WallpaperImageLoader.swift in Sources */, 96A5F73829928B3700234E5F /* GeneralizedImageFetcher.swift in Sources */, @@ -17080,17 +17087,14 @@ 21EA466A2B04130500AAAB2D /* TabsPanelState.swift in Sources */, D04D1B862097859B0074B35F /* DownloadToast.swift in Sources */, 2CFEA0542D455BD500B25CE0 /* MarketsController.swift in Sources */, - 2CFEA0552D455BD500B25CE0 /* WelcomeTourRow.swift in Sources */, 2CFEA0562D455BD500B25CE0 /* NewsController.swift in Sources */, 2CFEA0572D455BD500B25CE0 /* NTPLogoCell.swift in Sources */, 2CFEA0592D455BD500B25CE0 /* NTPImpactCell.swift in Sources */, 2CFEA05A2D455BD500B25CE0 /* EcosiaPrimaryButton.swift in Sources */, - 2CFEA05B2D455BD500B25CE0 /* WelcomeTourProfit.swift in Sources */, 2CFEA05C2D455BD500B25CE0 /* WhatsNewLocalDataProvider.swift in Sources */, 2CFEA05D2D455BD500B25CE0 /* NTPTooltipDelegate.swift in Sources */, 2CFEA05E2D455BD500B25CE0 /* EmptyReadingListView.swift in Sources */, 2CFEA05F2D455BD500B25CE0 /* NTPImpactRowView.swift in Sources */, - 2CFEA0602D455BD500B25CE0 /* WelcomeTourAction.swift in Sources */, 2CFEA0612D455BD500B25CE0 /* EmptyHeader.swift in Sources */, 2CFEA0622D455BD500B25CE0 /* NTPTooltip.swift in Sources */, 2CFEA0632D455BD500B25CE0 /* NTPLibraryShortcutView.swift in Sources */, @@ -17099,16 +17103,14 @@ 2CFEA0672D455BD500B25CE0 /* PageActionsShortcutsHeader.swift in Sources */, 2CFEA0682D455BD500B25CE0 /* NTPImpactDividerFooter.swift in Sources */, 2CFEA0692D455BD500B25CE0 /* EmptyBookmarksView.swift in Sources */, - 2CFEA06B2D455BD500B25CE0 /* Welcome.swift in Sources */, + 2CFEA06B2D455BD500B25CE0 /* WelcomeViewController.swift in Sources */, 2CFEA06C2D455BD500B25CE0 /* ArcProgressView.swift in Sources */, 2CFEA06D2D455BD500B25CE0 /* Sparkle.swift in Sources */, 2CFEA06E2D455BD500B25CE0 /* CircleButton.swift in Sources */, 2CFEA06F2D455BD500B25CE0 /* WhatsNewViewController.swift in Sources */, 2CFEA0702D455BD500B25CE0 /* UserDefaultsSeedProgressManager.swift in Sources */, 2CFEA0712D455BD500B25CE0 /* NTPSeedCounterCell.swift in Sources */, - 2CFEA0722D455BD500B25CE0 /* WelcomeTourGreen.swift in Sources */, 2CFEA0732D455BD500B25CE0 /* WhatsNewViewModel.swift in Sources */, - 2CFEA0742D455BD500B25CE0 /* WelcomeTour.swift in Sources */, 2CFEA0752D455BD500B25CE0 /* NTPTooltip.Highlight.swift in Sources */, 2CFEA0762D455BD500B25CE0 /* NTPNewsCell.swift in Sources */, 2CFEA0772D455BD500B25CE0 /* NTPNewsCellViewModel.swift in Sources */, @@ -17118,7 +17120,6 @@ 2CFEA07B2D455BD500B25CE0 /* WhatsNewDataProvider.swift in Sources */, 2CFEA07C2D455BD500B25CE0 /* NTPImpactCellViewModel.swift in Sources */, 2CFEA07D2D455BD500B25CE0 /* EmptyBookmarksViewDelegate.swift in Sources */, - 2CFEA07E2D455BD500B25CE0 /* WelcomeTour.Step.swift in Sources */, 2CFEA07F2D455BD500B25CE0 /* DefaultBrowserViewController.swift in Sources */, 2CFEA0802D455BD500B25CE0 /* SeedCounterHiddenSettings.swift in Sources */, 2CFEA0812D455BD500B25CE0 /* WhatsNewItem.swift in Sources */, @@ -17126,7 +17127,6 @@ 2CFEA0832D455BD500B25CE0 /* NTPConfigurableNudgeCardCell.swift in Sources */, 2CFEA0842D455BD500B25CE0 /* LoadingScreen.swift in Sources */, 2CFEA0852D455BD500B25CE0 /* NTPCustomizationCellViewModel.swift in Sources */, - 2CFEA0862D455BD500B25CE0 /* WelcomeTourTransparent.swift in Sources */, 2CFEA0872D455BD500B25CE0 /* NTPCustomizationCell.swift in Sources */, 2CFEA0882D455BD500B25CE0 /* SeedCounterConfig.swift in Sources */, 2CFEA0892D455BD500B25CE0 /* NTPSeedCounterViewModel.swift in Sources */, diff --git a/firefox-ios/Client/Application/AppDelegate.swift b/firefox-ios/Client/Application/AppDelegate.swift index 46ba5e4b4316e..b17b30d37b7ab 100644 --- a/firefox-ios/Client/Application/AppDelegate.swift +++ b/firefox-ios/Client/Application/AppDelegate.swift @@ -84,7 +84,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { .preLaunchDependenciesComplete, .postLaunchDependenciesComplete, .accountManagerInitialized, - .browserIsReady + .browserIsReady, + // Ecosia: Add Feature Management dependency + .featureManagementInitialized ]) // Then setup dependency container as it's needed for everything else @@ -135,7 +137,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { EcosiaInstallType.evaluateCurrentEcosiaInstallType() // Ecosia: Disable BG sync //backgroundSyncUtil = BackgroundSyncUtil(profile: profile, application: application) - /* + /* Ecosia: Feature Management fetch We perform the same configuration retrieval in `applicationDidBecomeActive(:)` and sounds redundant; @@ -148,6 +150,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { */ Task { await FeatureManagement.fetchConfiguration() + // Signal that feature management initialization is complete on main thread + AppEventQueue.signal(event: .featureManagementInitialized) // Ecosia: Braze Service Initialization after feature flags are fetched for conditional initialization BrazeService.shared.initialize() // Ecosia: Lifecycle tracking. Needs to happen after Unleash start so that the flags are correctly added to the analytics context. diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index 5ba2270cecc2a..47d7539f09c5e 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -98,7 +98,12 @@ class BrowserCoordinator: BaseCoordinator, // MARK: - LaunchCoordinatorDelegate func didFinishLaunch(from coordinator: LaunchCoordinator) { + /* Ecosia: Animate transition from welcome screen router.dismiss(animated: true, completion: nil) + */ + router.dismiss(animated: true) { [weak self] in + self?.browserViewController.animateToolbarsIn() + } remove(child: coordinator) // Once launch is done, we check for any saved Route diff --git a/firefox-ios/Client/Coordinators/Launch/LaunchCoordinator.swift b/firefox-ios/Client/Coordinators/Launch/LaunchCoordinator.swift index b2b66f4ca0eda..a7f3f99041bec 100644 --- a/firefox-ios/Client/Coordinators/Launch/LaunchCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Launch/LaunchCoordinator.swift @@ -4,6 +4,7 @@ import Common import Foundation +import Ecosia protocol LaunchCoordinatorDelegate: AnyObject { func didFinishLaunch(from coordinator: LaunchCoordinator) @@ -46,48 +47,56 @@ class LaunchCoordinator: BaseCoordinator, // MARK: - Intro private func presentIntroOnboarding(with manager: IntroScreenManager, isFullScreen: Bool) { - /* Ecosia: Disable old onboarding since outdated. Remove and clean up once properly updated. - - let onboardingModel = NimbusOnboardingFeatureLayer().getOnboardingModel(for: .freshInstall) - let telemetryUtility = OnboardingTelemetryUtility(with: onboardingModel) - let introViewModel = IntroViewModel(introScreenManager: manager, - profile: profile, - model: onboardingModel, - telemetryUtility: telemetryUtility) - /* Ecosia: custom onboarding - let introViewController = IntroViewController(viewModel: introViewModel, windowUUID: windowUUID) - introViewController.qrCodeNavigationHandler = self - introViewController.didFinishFlow = { [weak self] in + // Wait for feature flags to be available since Onboarding is behind an experiment + AppEventQueue.wait(for: .featureManagementInitialized) { [weak self] in guard let self = self else { return } - self.parentCoordinator?.didFinishLaunch(from: self) - } - */ - let introViewController = WelcomeNavigation(rootViewController: Welcome(delegate: self, windowUUID: windowUUID)) - introViewController.isNavigationBarHidden = true - introViewController.edgesForExtendedLayout = UIRectEdge(rawValue: 0) - if isFullScreen { - introViewController.modalPresentationStyle = .fullScreen - router.present(introViewController, animated: false) - } else { - introViewController.preferredContentSize = CGSize( - width: ViewControllerConsts.PreferredSize.IntroViewController.width, - height: ViewControllerConsts.PreferredSize.IntroViewController.height) - introViewController.modalPresentationStyle = .formSheet - // Disables dismissing the view by tapping outside the view, based on - // Nimbus's configuration - if !introViewModel.isDismissable { - introViewController.isModalInPresentation = true + + // Ecosia: Hide onboarding out of experiment + guard OnboardingProductTourExperiment.isEnabled else { + self.parentCoordinator?.didFinishLaunch(from: self) + return } - /* Ecosia: Remove completion - router.present(introViewController, animated: true) { - introViewController.closeOnboarding() + + /* Ecosia: custom onboarding + let onboardingModel = NimbusOnboardingFeatureLayer().getOnboardingModel(for: .freshInstall) + let telemetryUtility = OnboardingTelemetryUtility(with: onboardingModel) + let introViewModel = IntroViewModel(introScreenManager: manager, + profile: profile, + model: onboardingModel, + telemetryUtility: telemetryUtility) + + let introViewController = IntroViewController(viewModel: introViewModel, windowUUID: windowUUID) + introViewController.qrCodeNavigationHandler = self + introViewController.didFinishFlow = { [weak self] in + guard let self = self else { return } + self.parentCoordinator?.didFinishLaunch(from: self) + } + + if isFullScreen { + introViewController.modalPresentationStyle = .fullScreen + router.present(introViewController, animated: false) + } else { + introViewController.preferredContentSize = CGSize( + width: ViewControllerConsts.PreferredSize.IntroViewController.width, + height: ViewControllerConsts.PreferredSize.IntroViewController.height) + introViewController.modalPresentationStyle = .formSheet + // Disables dismissing the view by tapping outside the view, based on + // Nimbus's configuration + if !introViewModel.isDismissable { + introViewController.isModalInPresentation = true + } + router.present(introViewController, animated: true) } - */ - router.present(introViewController, animated: true) + */ + let introViewController = WelcomeNavigation( + rootViewController: WelcomeViewController(delegate: self, windowUUID: self.windowUUID), + windowUUID: self.windowUUID + ) + introViewController.isNavigationBarHidden = true + introViewController.edgesForExtendedLayout = UIRectEdge(rawValue: 0) + introViewController.modalPresentationStyle = .fullScreen + self.router.present(introViewController, animated: false) } - - */ - parentCoordinator?.didFinishLaunch(from: self) } // MARK: - Update @@ -186,7 +195,7 @@ class LaunchCoordinator: BaseCoordinator, // Ecosia: custom onboarding extension LaunchCoordinator: WelcomeDelegate { - func welcomeDidFinish(_ welcome: Welcome) { + func welcomeDidFinish(_ welcome: WelcomeViewController) { self.parentCoordinator?.didFinishLaunch(from: self) } } diff --git a/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift b/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift index 5321bb9bbb01e..fe0434be560a0 100644 --- a/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift @@ -147,8 +147,23 @@ class SceneCoordinator: BaseCoordinator, LaunchCoordinatorDelegate, LaunchFinish // MARK: - LaunchCoordinatorDelegate func didFinishLaunch(from coordinator: LaunchCoordinator) { + /* Ecosia: Custom transition router.dismiss(animated: true) remove(child: coordinator) + */ startBrowser(with: nil) + + // Ecosia: Animate transition from welcome screen + guard let browserCoordinator = childCoordinators.first(where: { $0 is BrowserCoordinator }) as? BrowserCoordinator else { + router.dismiss(animated: true) + remove(child: coordinator) + return + } + + browserCoordinator.browserViewController.prepareToolbarsForWelcomeTransition() + router.dismiss(animated: true) { + browserCoordinator.browserViewController.animateToolbarsIn() + } + remove(child: coordinator) } } diff --git a/firefox-ios/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift b/firefox-ios/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift index d0028405bbfdd..76238fee2c482 100644 --- a/firefox-ios/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift +++ b/firefox-ios/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift @@ -152,7 +152,7 @@ extension AppSettingsTableViewController { OpenFiftyTabsDebugOption(settings: self, settingsDelegate: self), ToggleDefaultBrowserPromo(settings: self), ToggleImpactIntro(settings: self), - ShowTour(settings: self, windowUUID: windowUUID), + ShowWelcomeScreen(settings: self, windowUUID: windowUUID), CreateReferralCode(settings: self), AddReferral(settings: self), AddClaim(settings: self), @@ -164,6 +164,7 @@ extension AppSettingsTableViewController { UnleashBrazeIntegrationSetting(settings: self), UnleashNativeSRPVAnalyticsSetting(settings: self), UnleashAISearchMVPSetting(settings: self), + UnleashOnboardingSetting(settings: self), UnleashIdentifierSetting(settings: self), AnalyticsIdentifierSetting(settings: self) ] diff --git a/firefox-ios/Client/Ecosia/Extensions/Task+Sleep.swift b/firefox-ios/Client/Ecosia/Extensions/Task+Sleep.swift new file mode 100644 index 0000000000000..b85cc2cb8d535 --- /dev/null +++ b/firefox-ios/Client/Ecosia/Extensions/Task+Sleep.swift @@ -0,0 +1,17 @@ +// 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 + +extension Task where Success == Never, Failure == Never { + /// Sleep for a given duration in seconds, with iOS 15 compatibility + /// When iOS 16+ becomes minimum, this can be replaced with Task.sleep(for: .seconds()) + static func sleep(duration: TimeInterval) async throws { + if #available(iOS 16.0, *) { + try await Task.sleep(for: .seconds(duration)) + } else { + try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) + } + } +} diff --git a/firefox-ios/Client/Ecosia/Settings/EcosiaDebugSettings.swift b/firefox-ios/Client/Ecosia/Settings/EcosiaDebugSettings.swift index a76ca3ed352c5..ffd9120b512ff 100644 --- a/firefox-ios/Client/Ecosia/Settings/EcosiaDebugSettings.swift +++ b/firefox-ios/Client/Ecosia/Settings/EcosiaDebugSettings.swift @@ -62,9 +62,9 @@ final class ToggleDefaultBrowserPromo: HiddenSetting { } } -final class ShowTour: HiddenSetting, WelcomeDelegate { +final class ShowWelcomeScreen: HiddenSetting, WelcomeDelegate { override var title: NSAttributedString? { - return NSAttributedString(string: "Debug: Show Intro", attributes: [:]) + return NSAttributedString(string: "Debug: Show Welcome Screen", attributes: [:]) } let windowUUID: WindowUUID @@ -75,7 +75,7 @@ final class ShowTour: HiddenSetting, WelcomeDelegate { var parentPresenter: UIViewController? override func onClick(_ navigationController: UINavigationController?) { - let welcome = Welcome(delegate: self, windowUUID: windowUUID) + let welcome = WelcomeViewController(delegate: self, windowUUID: windowUUID) welcome.modalPresentationStyle = .fullScreen welcome.modalTransitionStyle = .coverVertical let presentingViewController = navigationController?.presentingViewController @@ -84,7 +84,7 @@ final class ShowTour: HiddenSetting, WelcomeDelegate { } } - func welcomeDidFinish(_ welcome: Welcome) { + func welcomeDidFinish(_ welcome: WelcomeViewController) { if let presentedTour = welcome.presentedViewController { presentedTour.dismiss(animated: true) { welcome.dismiss(animated: true) @@ -289,6 +289,20 @@ final class UnleashAISearchMVPSetting: UnleashVariantResetSetting { } } +final class UnleashOnboardingSetting: UnleashVariantResetSetting { + override var titleName: String? { + "Onboarding Product Tour" + } + + override var variant: Unleash.Variant? { + Unleash.getVariant(.onboardingProductTour) + } + + override var unleashEnabled: Bool? { + Unleash.isEnabled(.onboardingProductTour) + } +} + final class AnalyticsIdentifierSetting: HiddenSetting { override var title: NSAttributedString? { return NSAttributedString(string: "Debug: Analytics Identifier", attributes: [:]) diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/Contents.json deleted file mode 100644 index a3b7292a5bac2..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "images" : [ - { - "filename" : "onboardingWaves.pdf", - "idiom" : "iphone" - }, - { - "filename" : "onboardingWaves_iPad.pdf", - "idiom" : "ipad" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/onboardingWaves.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/onboardingWaves.pdf deleted file mode 100644 index f203e4082dac6..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/onboardingWaves.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/onboardingWaves_iPad.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/onboardingWaves_iPad.pdf deleted file mode 100644 index 383d456efdf8b..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/onboardingWaves.imageset/onboardingWaves_iPad.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/Contents.json deleted file mode 100644 index 73c00596a7fca..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 51.png b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 51.png deleted file mode 100644 index 55fcfe8b97e5a..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 51.png and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 52.png b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 52.png deleted file mode 100644 index 0c6a94b7cc0a1..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 52.png and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 53.png b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 53.png deleted file mode 100644 index 9626dfb75ee9a..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/1 53.png and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/Contents.json deleted file mode 100644 index 015418bad6131..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour1.imageset/Contents.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "1x" - }, - { - "filename" : "1 51.png", - "idiom" : "iphone", - "scale" : "2x" - }, - { - "filename" : "1 52.png", - "idiom" : "iphone", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "scale" : "1x" - }, - { - "filename" : "1 53.png", - "idiom" : "ipad", - "scale" : "2x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Contents.json deleted file mode 100644 index b7c65d41737ad..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Contents.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "1x" - }, - { - "filename" : "Group 1434.jpg", - "idiom" : "iphone", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "scale" : "3x" - }, - { - "filename" : "Group 1435.jpg", - "idiom" : "ipad", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "scale" : "2x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Group 1434.jpg b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Group 1434.jpg deleted file mode 100644 index 1dd6fec220c78..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Group 1434.jpg and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Group 1435.jpg b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Group 1435.jpg deleted file mode 100644 index 8ee338a16d1d7..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour2.imageset/Group 1435.jpg and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour3.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour3.imageset/Contents.json deleted file mode 100644 index 2a9d507493dd1..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour3.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "MAP.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour3.imageset/MAP.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour3.imageset/MAP.pdf deleted file mode 100644 index 0899c854a02ee..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour3.imageset/MAP.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Contents.json deleted file mode 100644 index b21b3ae5f5b78..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Contents.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "1x" - }, - { - "filename" : "Group 1580.png", - "idiom" : "iphone", - "scale" : "2x" - }, - { - "filename" : "Group 1581.png", - "idiom" : "iphone", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "scale" : "1x" - }, - { - "filename" : "Group 1582.png", - "idiom" : "ipad", - "scale" : "2x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1580.png b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1580.png deleted file mode 100644 index 6d239354dc0db..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1580.png and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1581.png b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1581.png deleted file mode 100644 index 1a1b2e68ff942..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1581.png and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1582.png b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1582.png deleted file mode 100644 index b8377e4f4d97c..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tour4.imageset/Group 1582.png and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Cards-illustrative 1.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Cards-illustrative 1.pdf deleted file mode 100644 index fa53de1d095de..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Cards-illustrative 1.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Cards-illustrative.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Cards-illustrative.pdf deleted file mode 100644 index ee413008c9550..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Cards-illustrative.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Contents.json deleted file mode 100644 index 039065755d847..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourCounter.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Cards-illustrative.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Cards-illustrative 1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Contents.json deleted file mode 100644 index ffe1374799aea..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Theme=Light, Variant=Green results.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Theme=Dark, Variant=Green results.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Theme=Dark, Variant=Green results.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Theme=Dark, Variant=Green results.pdf deleted file mode 100644 index a9682b169a811..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Theme=Dark, Variant=Green results.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Theme=Light, Variant=Green results.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Theme=Light, Variant=Green results.pdf deleted file mode 100644 index dcf894edefe52..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourGreen.imageset/Theme=Light, Variant=Green results.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Contents.json b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Contents.json deleted file mode 100644 index f2930060c81b6..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Theme=Light, Variant=Search.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Theme=Dark, Variant=Search.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Theme=Dark, Variant=Search.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Theme=Dark, Variant=Search.pdf deleted file mode 100644 index 36041ef02aa07..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Theme=Dark, Variant=Search.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Theme=Light, Variant=Search.pdf b/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Theme=Light, Variant=Search.pdf deleted file mode 100644 index fc1c220f7b37d..0000000000000 Binary files a/firefox-ios/Client/Ecosia/UI/Ecosia.xcassets/tour/tourSearch.imageset/Theme=Light, Variant=Search.pdf and /dev/null differ diff --git a/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreen.xib b/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreen.xib index 76841f10cb0bf..0d90b9bc29c1a 100644 --- a/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreen.xib +++ b/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreen.xib @@ -1,9 +1,9 @@ - + - + @@ -17,7 +17,7 @@ - + diff --git a/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreenView.swift b/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreenView.swift index e0a4d05098c9a..9f384814a7e68 100644 --- a/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreenView.swift +++ b/firefox-ios/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreenView.swift @@ -9,16 +9,8 @@ public class EcosiaLaunchScreenView: UIView { private static let viewName = "EcosiaLaunchScreen" public class func fromNib() -> UIView { - let view = Bundle.main.loadNibNamed(EcosiaLaunchScreenView.viewName, - owner: nil, - options: nil)![0] as! UIView - - // XIB uses systemBackground as fallback since asset catalog colors in XIBs - // can briefly show "Any Appearance" variant before switching to correct appearance - if let color = UIColor(named: "launchScreenBackground") { - view.backgroundColor = color - } - - return view + Bundle.main.loadNibNamed(EcosiaLaunchScreenView.viewName, + owner: nil, + options: nil)![0] as! UIView } } diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/BrowserViewController+WelcomeTransition.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/BrowserViewController+WelcomeTransition.swift new file mode 100644 index 0000000000000..44dbe781861b6 --- /dev/null +++ b/firefox-ios/Client/Ecosia/UI/Onboarding/BrowserViewController+WelcomeTransition.swift @@ -0,0 +1,56 @@ +// 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 Shared + +// MARK: - Welcome Transition Animation +extension BrowserViewController { + private static let welcomeTransitionBackgroundKey = "welcomeTransitionBackground" + + /// Prepares the toolbars to be animated in after welcome dismissal + /// This should be called early in the view lifecycle + func prepareToolbarsForWelcomeTransition() { + // Hide toolbars initially to prevent flash + header.alpha = 0 + bottomContainer.alpha = 0 + + // Hide content and set background color to match NTP + contentStackView.alpha = 0 + let theme = themeManager.getCurrentTheme(for: windowUUID) + // TODO: Investigate colors behind toolbars before transition + // TODO: Smooth out color transitions + view.backgroundColor = theme.colors.ecosia.backgroundPrimaryDecorative + } + + /// Animates the top and bottom toolbars sliding in from the edges + /// This is called after the welcome screen fades out + func animateToolbarsIn() { + let margin: CGFloat = 20 // Additional margin to ensure views are fully hidden + let topOffset = -(header.frame.height + view.safeAreaInsets.top + margin) + let bottomOffset = bottomContainer.frame.height + view.safeAreaInsets.bottom + margin + + // Set initial off-screen positions and make visible + header.alpha = 1.0 + bottomContainer.alpha = 1.0 + header.transform = CGAffineTransform(translationX: 0, y: topOffset) + bottomContainer.transform = CGAffineTransform(translationX: 0, y: bottomOffset) + + // Animate to final positions + UIView.animate( + withDuration: 0.35, + delay: 0, + options: .curveEaseInOut, + animations: { [weak self] in + self?.header.transform = .identity + self?.bottomContainer.transform = .identity + self?.contentStackView.alpha = 1 + } + ) + } + + private static func hash(for key: String) -> Int { + return key.hashValue + } +} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/FadeTransitionDelegate.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/FadeTransitionDelegate.swift new file mode 100644 index 0000000000000..027a51196c361 --- /dev/null +++ b/firefox-ios/Client/Ecosia/UI/Onboarding/FadeTransitionDelegate.swift @@ -0,0 +1,75 @@ +// 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 + +/// A custom transition delegate that provides a fade animation for modal presentation and dismissal +final class FadeTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { + + /// The background color to apply to the underlying view when dismissing. Defaults to nil (no change). + var dismissalBackgroundColor: UIColor? + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return FadeAnimator(isPresenting: false, dismissalBackgroundColor: dismissalBackgroundColor) + } + + func animationController(forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return FadeAnimator(isPresenting: true, dismissalBackgroundColor: nil) + } +} + +private final class FadeAnimator: NSObject, UIViewControllerAnimatedTransitioning { + private let isPresenting: Bool + private let dismissalBackgroundColor: UIColor? + + init(isPresenting: Bool, dismissalBackgroundColor: UIColor?) { + self.isPresenting = isPresenting + self.dismissalBackgroundColor = dismissalBackgroundColor + super.init() + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.2 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let toView = transitionContext.view(forKey: .to), + let fromView = transitionContext.view(forKey: .from) else { + transitionContext.completeTransition(false) + return + } + + let containerView = transitionContext.containerView + let duration = transitionDuration(using: transitionContext) + + if isPresenting { + containerView.addSubview(toView) + toView.alpha = 0 + + UIView.animate(withDuration: duration, + animations: { + toView.alpha = 1 + }, completion: { finished in + transitionContext.completeTransition(finished) + }) + } else { + containerView.insertSubview(toView, at: 0) + toView.alpha = 1 + + if let dismissalBackgroundColor = dismissalBackgroundColor { + toView.backgroundColor = dismissalBackgroundColor + } + + UIView.animate(withDuration: duration, + animations: { + fromView.alpha = 0 + }, completion: { finished in + fromView.removeFromSuperview() + transitionContext.completeTransition(finished) + }) + } + } +} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/LoopingVideoPlayer.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/LoopingVideoPlayer.swift new file mode 100644 index 0000000000000..c599163ee98dd --- /dev/null +++ b/firefox-ios/Client/Ecosia/UI/Onboarding/LoopingVideoPlayer.swift @@ -0,0 +1,124 @@ +// 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 AVKit +import Combine +import SwiftUI + +class VideoPlayerView: UIView { + let playerLayer = AVPlayerLayer() + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } + + override func layoutSubviews() { + super.layoutSubviews() + if let playerLayer = layer as? AVPlayerLayer { + playerLayer.videoGravity = .resizeAspectFill + playerLayer.frame = bounds + } + } +} + +struct LoopingVideoPlayer: UIViewRepresentable { + let videoName: String + var onReady: (() -> Void)? + + func makeUIView(context: Context) -> UIView { + let view = VideoPlayerView() + + // TODO: Check effect on app size and reduce the video + guard let videoURL = Bundle.main.url(forResource: videoName, withExtension: "mov") else { + // Fallback to static image if video not found + let imageView = UIImageView(image: UIImage(named: "forest")) + imageView.contentMode = .scaleAspectFill + imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(imageView) + // Trigger onReady immediately for fallback + DispatchQueue.main.async { + onReady?() + } + return view + } + + let playerItem = AVPlayerItem(url: videoURL) + let player = AVPlayer(playerItem: playerItem) + + if let playerLayer = view.layer as? AVPlayerLayer { + playerLayer.player = player + playerLayer.videoGravity = .resizeAspectFill + } + + // Store references in coordinator + context.coordinator.player = player + context.coordinator.view = view + context.coordinator.onReady = onReady + + // Monitor player status + context.coordinator.statusObserver = playerItem.publisher(for: \.status) + .receive(on: DispatchQueue.main) + .sink { status in + switch status { + case .readyToPlay: + player.play() + context.coordinator.onReady?() + case .failed: + // Call onReady even on failure to prevent animations from never starting + context.coordinator.onReady?() + case .unknown: + break + @unknown default: + break + } + } + + // Monitor buffer status + let bufferObserver = playerItem.publisher(for: \.isPlaybackBufferFull) + .receive(on: DispatchQueue.main) + .sink { _ in + // Buffer monitoring + } + + context.coordinator.bufferObserver = bufferObserver + + // Loop video when it ends + context.coordinator.loopObserver = NotificationCenter.default.addObserver( + forName: .AVPlayerItemDidPlayToEndTime, + object: playerItem, + queue: .main + ) { _ in + player.seek(to: .zero) + player.play() + } + + return view + } + + func updateUIView(_ uiView: UIView, context: Context) { + // View updates handled by layoutSubviews + } + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + class Coordinator { + var player: AVPlayer? + var view: UIView? + var loopObserver: NSObjectProtocol? + var statusObserver: AnyCancellable? + var bufferObserver: AnyCancellable? + var onReady: (() -> Void)? + + deinit { + player?.pause() + if let observer = loopObserver { + NotificationCenter.default.removeObserver(observer) + } + statusObserver?.cancel() + bufferObserver?.cancel() + } + } +} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/Welcome.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/Welcome.swift deleted file mode 100644 index d179b20fba6fb..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/Welcome.swift +++ /dev/null @@ -1,303 +0,0 @@ -// 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 Common -import Ecosia - -protocol WelcomeDelegate: AnyObject { - func welcomeDidFinish(_ welcome: Welcome) -} - -final class Welcome: UIViewController { - private weak var logo: UIImageView! - private weak var background: UIImageView! - private weak var overlay: UIView! - private weak var overlayLogo: UIImageView! - private var maskLayer: CALayer! - private weak var stack: UIStackView! - - private var logoCenterConstraint: NSLayoutConstraint! - private var logoTopConstraint: NSLayoutConstraint! - private var logoHeightConstraint: NSLayoutConstraint! - private var stackBottonConstraint: NSLayoutConstraint! - private var stackTopConstraint: NSLayoutConstraint! - - private lazy var theme: Theme = { - let themeManager: ThemeManager = AppContainer.shared.resolve() - return themeManager.getCurrentTheme(for: windowUUID) - }() - - private var zoomedOut = false - private weak var delegate: WelcomeDelegate? - let windowUUID: WindowUUID - - required init?(coder: NSCoder) { nil } - init(delegate: WelcomeDelegate, windowUUID: WindowUUID) { - self.delegate = delegate - self.windowUUID = windowUUID - super.init(nibName: nil, bundle: nil) - modalPresentationCapturesStatusBarAppearance = true - definesPresentationContext = true - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return zoomedOut ? .lightContent : .darkContent - } - - // MARK: Views - override func viewDidLoad() { - super.viewDidLoad() - - addOverlay() - addBackground() - addStack() - - Task.detached { - // Fetching FinancialReports async as some onboarding steps might use it - try? await FinancialReports.shared.fetchAndUpdate() - } - } - - private var didAppear = false - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - guard !didAppear else { return } - addMask() - fadeIn() - didAppear = true - Analytics.shared.introDisplaying(page: .start) - - MMP.sendEvent(.onboardingStart) - } - - private func addOverlay() { - let overlay = UIView() - overlay.backgroundColor = theme.colors.ecosia.backgroundPrimaryDecorative - overlay.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(overlay) - self.overlay = overlay - - overlay.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - overlay.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true - overlay.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - - let overlayLogo = UIImageView(image: .init(named: "ecosiaLogoLaunch")) - overlayLogo.translatesAutoresizingMaskIntoConstraints = false - overlayLogo.contentMode = .scaleAspectFit - overlay.addSubview(overlayLogo) - self.overlayLogo = overlayLogo - - overlayLogo.centerXAnchor.constraint(equalTo: overlay.centerXAnchor).isActive = true - overlayLogo.centerYAnchor.constraint(equalTo: overlay.centerYAnchor).isActive = true - overlayLogo.heightAnchor.constraint(equalToConstant: 72).isActive = true - } - - private func addBackground() { - let background = UIImageView(image: .init(named: "forest")) - background.translatesAutoresizingMaskIntoConstraints = false - background.contentMode = .scaleAspectFill - view.addSubview(background) - background.alpha = 0 - self.background = background - - background.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - background.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true - background.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - background.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - - let logo = UIImageView(image: .init(named: "ecosiaLogoLaunch")?.withRenderingMode(.alwaysTemplate)) - logo.translatesAutoresizingMaskIntoConstraints = false - logo.contentMode = .scaleAspectFit - logo.tintColor = .white - logo.isAccessibilityElement = true - logo.accessibilityIdentifier = AccessibilityIdentifiers.Ecosia.logo - logo.accessibilityLabel = .localized(.ecosiaLogoAccessibilityLabel) - background.addSubview(logo) - self.logo = logo - - logoCenterConstraint = logo.centerYAnchor.constraint(equalTo: background.centerYAnchor) - logoCenterConstraint.priority = .defaultHigh - logoCenterConstraint.isActive = true - logoTopConstraint = logo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24) - logoTopConstraint.priority = .defaultHigh - logoTopConstraint.isActive = false - logo.centerXAnchor.constraint(equalTo: background.centerXAnchor).isActive = true - - logoHeightConstraint = logo.heightAnchor.constraint(equalToConstant: 72) - logoHeightConstraint.priority = .defaultHigh - logoHeightConstraint.isActive = true - } - - private func addStack() { - let stack = UIStackView() - stack.translatesAutoresizingMaskIntoConstraints = false - stack.isHidden = true - stack.axis = .vertical - stack.distribution = .fill - stack.alignment = .fill - stack.spacing = 10 - view.addSubview(stack) - self.stack = stack - - let label = UILabel() - label.numberOfLines = 0 - label.attributedText = introText - label.accessibilityLabel = simplestWayString.replacingOccurrences(of: "\n", with: "") - label.font = .preferredFont(forTextStyle: .largeTitle).bold() - label.adjustsFontForContentSizeCategory = true - label.textColor = .white - stack.addArrangedSubview(label) - - let cta = UIButton(type: .system) - cta.backgroundColor = EcosiaLightTheme().colors.ecosia.buttonBackgroundSecondary - cta.setTitle(.localized(.getStarted), for: .normal) - cta.titleLabel?.font = .preferredFont(forTextStyle: .callout) - cta.titleLabel?.adjustsFontForContentSizeCategory = true - cta.setTitleColor(EcosiaLightTheme().colors.ecosia.textPrimary, for: .normal) - cta.layer.cornerRadius = 25 - cta.heightAnchor.constraint(equalToConstant: 50).isActive = true - cta.addTarget(self, action: #selector(getStarted), for: .primaryActionTriggered) - - stack.addArrangedSubview(UIView()) - 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(EcosiaDarkTheme().colors.ecosia.textSecondary, 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 - stack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16).isActive = true - } else { - stack.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true - stack.widthAnchor.constraint(equalToConstant: 544).isActive = true - } - stackTopConstraint = stack.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -16) - stackTopConstraint.priority = .defaultHigh - stackTopConstraint.isActive = true - stackBottonConstraint = stack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16) - stackBottonConstraint.priority = .defaultHigh - } - - func addMask() { - let point = CGPoint(x: logo.frame.midX - 25, y: logo.frame.midY - 13) - let mask = CGRect(origin: point, size: .init(width: 32, height: 32)) - - let layer = CALayer() - layer.contents = UIImage(named: "splashMask")?.cgImage - layer.frame = mask - layer.opacity = 0 - layer.contentsGravity = .resizeAspect - - background.layer.mask = layer - maskLayer = layer - background.alpha = 1.0 - } - - // MARK: Animations - private func fadeIn() { - maskLayer.opacity = 1 - - CATransaction.begin() - CATransaction.setAnimationTimingFunction(.init(name: CAMediaTimingFunctionName.easeIn)) - CATransaction.setCompletionBlock { [weak self] in - self?.zoomOut() - } - - let anim = CABasicAnimation(keyPath: "opacity") - anim.fromValue = 0.0 - anim.toValue = 1.0 - anim.duration = 0.3 - maskLayer.add(anim, forKey: "opacity") - - CATransaction.commit() - } - - private func zoomOut() { - zoomedOut = true - - let height = max(view.bounds.height, view.bounds.width) - let targetFrame = self.view.bounds.inset(by: .init(equalInset: -2.5 * height)) - - CATransaction.begin() - CATransaction.setAnimationDuration(1.4) - CATransaction.setAnimationTimingFunction(.init(name: CAMediaTimingFunctionName.easeInEaseOut)) - CATransaction.setCompletionBlock { [weak self] in - self?.showText() - self?.background.layer.mask = nil - } - maskLayer.frame = targetFrame - CATransaction.commit() - } - - private func showText() { - UIView.animate(withDuration: 0.3, delay: 0, options: []) { - self.logoTopConstraint.isActive = true - self.logoCenterConstraint.isActive = false - self.logoHeightConstraint.constant = 48 - self.stack.isHidden = false - self.stackTopConstraint.isActive = false - self.stackBottonConstraint.isActive = true - self.view.layoutIfNeeded() - self.setNeedsStatusBarAppearanceUpdate() - } - } - - // MARK: Helper - private let simplestWayString = String.localized(.theSimplestWay) - private var introText: NSAttributedString { - let raw = simplestWayString - let splits = raw.components(separatedBy: .newlines) - - guard splits.count == 3 else { return NSAttributedString(string: raw) } - - let first = NSMutableAttributedString(string: splits[0]) - let middle = NSMutableAttributedString(string: splits[1]) - let end = NSMutableAttributedString(string: splits[2]) - - let image1Attachment = NSTextAttachment() - image1Attachment.image = UIImage(named: "splashTree1") - let image1String = NSAttributedString(attachment: image1Attachment) - - let image2Attachment = NSTextAttachment() - image2Attachment.image = UIImage(named: "splashTree2") - let image2String = NSAttributedString(attachment: image2Attachment) - - first.append(image1String) - first.append(middle) - first.append(image2String) - first.append(end) - return first - } - - // MARK: Actions - @objc func getStarted() { - let tour = WelcomeTour(delegate: self, windowUUID: windowUUID) - tour.modalTransitionStyle = .crossDissolve - tour.modalPresentationStyle = .overCurrentContext - present(tour, animated: true, completion: nil) - Analytics.shared.introClick(.next, page: .start) - } - - @objc func skip() { - Analytics.shared.introClick(.skip, page: .start) - delegate?.welcomeDidFinish(self) - } -} - -extension Welcome: WelcomeTourDelegate { - func welcomeTourDidFinish(_ tour: WelcomeTour) { - delegate?.welcomeDidFinish(self) - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeNavigation.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeNavigation.swift index 7f8cb442f4fa4..3c61e1b1e0a5a 100644 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeNavigation.swift +++ b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeNavigation.swift @@ -3,9 +3,29 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit +import Common final class WelcomeNavigation: UINavigationController { + private let fadeTransitionDelegate: FadeTransitionDelegate + let windowUUID: WindowUUID + + init(rootViewController: UIViewController, windowUUID: WindowUUID) { + self.windowUUID = windowUUID + + // Transition delegate for fade dismissal + let transition = FadeTransitionDelegate() + let themeManager: ThemeManager = AppContainer.shared.resolve() + let theme = themeManager.getCurrentTheme(for: windowUUID) + // Matches NTP background + transition.dismissalBackgroundColor = theme.colors.ecosia.backgroundPrimaryDecorative + self.fadeTransitionDelegate = transition + + super.init(rootViewController: rootViewController) + transitioningDelegate = fadeTransitionDelegate + } + required init?(coder: NSCoder) { nil } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - topViewController is Welcome ? .portrait : .all + topViewController is WelcomeViewController ? .portrait : .all } } diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTour.Step.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTour.Step.swift deleted file mode 100644 index 6e931b87bf9bd..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTour.Step.swift +++ /dev/null @@ -1,110 +0,0 @@ -// 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 Ecosia - -extension WelcomeTour { - - enum Step { - case green - case profit - case action - case transparent - - static var all: [Step] { - [.green, .profit, .action, .transparent] - } - - var title: String { - switch self { - case .green: - return .localized(.grennestWayToSearch) - case .profit: - return .localized(.hundredPercentOfProfits) - case .action: - return .localized(.collectiveAction) - case .transparent: - return .localized(.realResults) - } - } - var text: String { - switch self { - case .green: - return .localized(.planetFriendlySearch) - case .profit: - return .localized(.weUseAllOurProfits) - case .action: - return .localized(.join15Million) - case .transparent: - return .localized(.shownExactlyHowMuch) - } - } - var background: Background { - switch self { - case .green: - return .init(image: "tour1") - case .profit: - return .init(image: "tour2") - case .action: - return .init(image: "tour3", color: UIColor(rgb: 0x668A7A)) - case .transparent: - return .init(image: "tour4") - } - } - var accessibleDescriptionKey: String.Key { - switch self { - case .green: - return .onboardingIllustrationTour1 - case .profit: - return .onboardingIllustrationTour2 - case .action: - return .onboardingIllustrationTour3 - case .transparent: - return .onboardingIllustrationTour4 - } - } - var content: UIView? { - let view: UIView? - switch self { - case .green: - view = WelcomeTourGreen() - case .profit: - view = WelcomeTourProfit() - case .action: - view = WelcomeTourAction() - case .transparent: - view = WelcomeTourTransparent() - } - view?.isAccessibilityElement = true - view?.accessibilityLabel = .localized(accessibleDescriptionKey) - return view - } - var analyticsValue: Analytics.Property.OnboardingPage { - switch self { - case .profit: - return .profits - case .action: - return .action - case .green: - return .greenSearch - case .transparent: - return .transparentFinances - } - } - } -} - -extension WelcomeTour.Step { - - final class Background { - let image: String - let color: UIColor? - - init(image: String, color: UIColor? = nil) { - self.image = image - self.color = color - } - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTour.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTour.swift deleted file mode 100644 index 22a19938378df..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTour.swift +++ /dev/null @@ -1,374 +0,0 @@ -// 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 Common -import Ecosia - -protocol WelcomeTourDelegate: AnyObject { - func welcomeTourDidFinish(_ tour: WelcomeTour) -} - -final class WelcomeTour: UIViewController, Themeable { - - private weak var navStack: UIStackView! - private weak var labelStack: UIStackView! - private weak var titleLabel: UILabel! - private weak var subtitleLabel: UILabel! - private weak var backButton: UIButton! - private weak var skipButton: UIButton? - private weak var pageControl: UIPageControl! - private weak var ctaButton: UIButton! - private weak var waves: UIImageView! - private weak var container: UIView! - private weak var imageView: UIImageView! - - // references to animated constraints - private weak var labelLeft: NSLayoutConstraint! - private weak var labelRight: NSLayoutConstraint! - private var margin: CGFloat { - return view.traitCollection.userInterfaceIdiom == .phone ? 16 : 112 - } - - // model - private var steps: [Step]! - private var current: Step? - private weak var delegate: WelcomeTourDelegate? - - // MARK: - Themeable Properties - - let windowUUID: WindowUUID - var currentWindowUUID: WindowUUID? { windowUUID } - var themeManager: ThemeManager { AppContainer.shared.resolve() } - var themeObserver: NSObjectProtocol? - var notificationCenter: NotificationProtocol = NotificationCenter.default - - // MARK: - Init - - init(delegate: WelcomeTourDelegate, windowUUID: WindowUUID, startingStep: Step? = nil) { - self.windowUUID = windowUUID - super.init(nibName: nil, bundle: nil) - modalPresentationCapturesStatusBarAppearance = true - self.delegate = delegate - steps = Step.all - current = startingStep - } - - required init?(coder: NSCoder) { return nil } - - override func viewDidLoad() { - super.viewDidLoad() - addStaticViews() - addDynamicViews() - applyTheme() - - listenForThemeChange(self.view) - } - - private func addStaticViews() { - let navStack = UIStackView() - navStack.translatesAutoresizingMaskIntoConstraints = false - navStack.axis = .horizontal - navStack.distribution = .equalCentering - navStack.alignment = .center - view.addSubview(navStack) - self.navStack = navStack - - navStack.heightAnchor.constraint(equalToConstant: 44).isActive = true - navStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16).isActive = true - navStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16).isActive = true - navStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true - - let backButton = UIButton.systemButton(with: .init(named: "backChevron")!, target: self, action: #selector(back)) - navStack.addArrangedSubview(backButton) - backButton.widthAnchor.constraint(equalToConstant: 44).isActive = true - backButton.accessibilityLabel = .localized(.onboardingBackButtonAccessibility) - navStack.addArrangedSubview(backButton) - self.backButton = backButton - - let pageControl = UIPageControl() - pageControl.isUserInteractionEnabled = false - pageControl.numberOfPages = 4 - pageControl.currentPage = 0 - pageControl.setContentHuggingPriority(.defaultLow, for: .horizontal) - pageControl.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - pageControl.accessibilityLabel = .localized(.onboardingPageControlDotsAccessibility) - navStack.addArrangedSubview(pageControl) - self.pageControl = pageControl - - let centerControl = pageControl.centerXAnchor.constraint(equalTo: navStack.centerXAnchor) - 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 - - let waves = UIImageView(image: .init(named: "onboardingWaves")) - waves.translatesAutoresizingMaskIntoConstraints = false - waves.setContentHuggingPriority(.required, for: .vertical) - waves.isAccessibilityElement = false - view.addSubview(waves) - self.waves = waves - - waves.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - waves.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - waves.heightAnchor.constraint(equalToConstant: 37).isActive = true - waves.bottomAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor, constant: 208).isActive = true - let wavesBottom = waves.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 208) - wavesBottom.priority = .defaultHigh - wavesBottom.isActive = true - } - - private func addDynamicViews() { - let labelStack = UIStackView() - labelStack.translatesAutoresizingMaskIntoConstraints = false - labelStack.axis = .vertical - labelStack.distribution = .fill - labelStack.alignment = .leading - labelStack.spacing = 8 - labelStack.alpha = 0 - view.addSubview(labelStack) - self.labelStack = labelStack - - let labelLeft = labelStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin + 48) - labelLeft.priority = .init(rawValue: 999) - labelLeft.isActive = true - self.labelLeft = labelLeft - - let labelRight = labelStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin + 48) - labelRight.priority = .init(rawValue: 999) - labelRight.isActive = true - self.labelRight = labelRight - - labelStack.topAnchor.constraint(equalTo: navStack.bottomAnchor, constant: 24).isActive = true - labelStack.bottomAnchor.constraint(lessThanOrEqualTo: waves.topAnchor).isActive = true - - let titleLabel = UILabel() - titleLabel.text = steps.first?.title - titleLabel.numberOfLines = 0 - titleLabel.font = .preferredFont(forTextStyle: .title2).bold() - titleLabel.adjustsFontForContentSizeCategory = true - titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) - titleLabel.setContentHuggingPriority(.required, for: .vertical) - labelStack.addArrangedSubview(titleLabel) - self.titleLabel = titleLabel - - let subtitleLabel = UILabel() - subtitleLabel.text = steps.first?.text - subtitleLabel.numberOfLines = 3 - subtitleLabel.font = .preferredFont(forTextStyle: .body) - subtitleLabel.adjustsFontForContentSizeCategory = true - subtitleLabel.adjustsFontSizeToFitWidth = true - subtitleLabel.setContentCompressionResistancePriority(.required, for: .vertical) - subtitleLabel.setContentHuggingPriority(.required, for: .vertical) - - labelStack.addArrangedSubview(subtitleLabel) - self.subtitleLabel = subtitleLabel - - let ctaButton = UIButton(type: .system) - ctaButton.setTitle(.localized(.continueMessage), for: .normal) - ctaButton.titleLabel?.font = .preferredFont(forTextStyle: .callout) - ctaButton.titleLabel?.adjustsFontForContentSizeCategory = true - ctaButton.translatesAutoresizingMaskIntoConstraints = false - ctaButton.addTarget(self, action: #selector(forward), for: .primaryActionTriggered) - ctaButton.alpha = 0 - view.addSubview(ctaButton) - self.ctaButton = ctaButton - - ctaButton.layer.cornerRadius = 24 - ctaButton.heightAnchor.constraint(equalToConstant: 48).isActive = true - ctaButton.leadingAnchor.constraint(equalTo: labelStack.leadingAnchor).isActive = true - ctaButton.trailingAnchor.constraint(equalTo: labelStack.trailingAnchor).isActive = true - ctaButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16).isActive = true - - let firstTourImageName = WelcomeTour.Step.all.first?.background.image ?? "tour1" - let imageView = UIImageView(image: .init(named: firstTourImageName)) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFill - imageView.clipsToBounds = true - view.insertSubview(imageView, belowSubview: waves) - self.imageView = imageView - imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - imageView.topAnchor.constraint(equalTo: waves.topAnchor).isActive = true - imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - - let container = UIView() - container.translatesAutoresizingMaskIntoConstraints = false - view.insertSubview(container, belowSubview: waves) - self.container = container - - container.leadingAnchor.constraint(equalTo: labelStack.leadingAnchor).isActive = true - container.trailingAnchor.constraint(equalTo: labelStack.trailingAnchor).isActive = true - container.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - container.topAnchor.constraint(equalTo: waves.topAnchor).isActive = true - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - guard let current else { - startTour() - return - } - display(step: current) - } - - private func startTour() { - let first = steps.first! - display(step: first) - } - - private func display(step: Step) { - current = step - pageControl.currentPage = steps.firstIndex(of: step) ?? 0 - - let title: String = isLastStep() ? .localized(.finishTour) : .localized(.continueMessage) - - // No image transition and "move right" for first step - let duration: CGFloat = isFirstStep() ? 0 : 0.3 - - // Image transition - UIView.transition(with: imageView, duration: duration, options: .transitionCrossDissolve, animations: { - self.imageView.image = UIImage(named: step.background.image) - self.imageView.backgroundColor = step.background.color ?? .clear - if self.traitCollection.userInterfaceIdiom == .phone { - self.imageView.contentMode = step.background.color == nil ? .scaleAspectFill : .scaleAspectFit - } - }) - - // Move and Fade transition - UIView.animate(withDuration: duration) { - self.moveRight() - self.labelStack.alpha = 0 - self.ctaButton.alpha = 0 - self.container.alpha = 0 - self.view.layoutIfNeeded() - } completion: { _ in - - self.fillContainer(with: step.content) - self.ctaButton.setTitle(title, for: .normal) - - UIView.animate(withDuration: 0.3) { - self.moveLeft() - self.titleLabel.text = step.title - self.subtitleLabel.text = step.text - self.labelStack.alpha = 1 - self.ctaButton.alpha = 1 - self.container.alpha = 1 - self.view.layoutIfNeeded() - } - } - - Analytics.shared.introDisplaying(page: current?.analyticsValue) - updateAccessibilityLabels(step: step) - } - - private func updateAccessibilityLabels(step: Step) { - titleLabel.accessibilityLabel = step.title - subtitleLabel.accessibilityLabel = step.text - ctaButton.accessibilityLabel = isLastStep() ? .localized(.onboardingFinishCTAButtonAccessibility) : .localized(.onboardingContinueCTAButtonAccessibility) - } - - private func moveRight() { - labelLeft.constant = margin + 48 - labelRight.constant = -margin + 48 - } - - private func moveLeft() { - labelLeft.constant = margin - labelRight.constant = -margin - } - - private func fillContainer(with content: UIView?) { - container.subviews.forEach({ $0.removeFromSuperview() }) - - guard let content = content else { return } - (content as? ThemeApplicable)?.applyTheme(theme: themeManager.getCurrentTheme(for: windowUUID)) - container.addSubview(content) - content.translatesAutoresizingMaskIntoConstraints = false - content.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true - content.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true - content.topAnchor.constraint(equalTo: container.topAnchor).isActive = true - content.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true - container.setNeedsLayout() - container.layoutIfNeeded() - } - - // MARK: Actions - @objc func back() { - guard !isFirstStep() else { - dismiss(animated: true) { - Analytics.shared.introDisplaying(page: .start) - } - return - } - let displayingStep = currentIndex - 1 - display(step: steps[displayingStep]) - } - - @objc func forward() { - guard !isLastStep() else { - complete() - return - } - Analytics.shared.introClick(.next, page: current?.analyticsValue) - let displayingStep = currentIndex + 1 - display(step: steps[displayingStep]) - UIAccessibility.post(notification: .screenChanged, argument: titleLabel) - } - - private func complete() { - MMP.sendEvent(.onboardingComplete) - delegate?.welcomeTourDidFinish(self) - } - - @objc func skip() { - Analytics.shared.introClick(.skip, page: current?.analyticsValue) - delegate?.welcomeTourDidFinish(self) - } - - // MARK: Helper - private var currentIndex: Int { - guard let current = current else { return 0 } - let index = steps.firstIndex(of: current) ?? 0 - return index - } - - private func isFirstStep() -> Bool { - return currentIndex == 0 - } - - private func isLastStep() -> Bool { - return currentIndex + 1 >= steps.count - } - - // MARK: Theming - func applyTheme() { - let theme = themeManager.getCurrentTheme(for: currentWindowUUID) - view.backgroundColor = theme.colors.ecosia.backgroundPrimaryDecorative - waves.tintColor = theme.colors.ecosia.backgroundPrimaryDecorative - titleLabel.textColor = theme.colors.ecosia.textPrimary - subtitleLabel.textColor = theme.colors.ecosia.textSecondary - skipButton?.tintColor = theme.colors.ecosia.buttonBackgroundPrimary - backButton.tintColor = theme.colors.ecosia.buttonBackgroundPrimary - pageControl.pageIndicatorTintColor = theme.colors.ecosia.stateDisabled - pageControl.currentPageIndicatorTintColor = theme.colors.ecosia.buttonBackgroundPrimary - ctaButton.backgroundColor = EcosiaLightTheme().colors.ecosia.buttonBackgroundSecondary - ctaButton.setTitleColor(EcosiaLightTheme().colors.ecosia.textPrimary, for: .normal) - container.subviews.forEach({ ($0 as? ThemeApplicable)?.applyTheme(theme: theme) }) - - imageView.backgroundColor = current?.background.color ?? .clear - guard let current = current else { return } - imageView.image = .init(named: current.background.image) - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourAction.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourAction.swift deleted file mode 100644 index 5bc5e61808753..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourAction.swift +++ /dev/null @@ -1,77 +0,0 @@ -// 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 Ecosia -import Common - -final class WelcomeTourAction: UIView, ThemeApplicable { - - // MARK: - Properties - - private weak var stack: UIStackView! - - // MARK: - Init - - override init(frame: CGRect) { - super.init(frame: frame) - setup() - updateAccessibilitySettings() - } - - required init?(coder: NSCoder) { nil } - - func setup() { - let stack = UIStackView() - stack.translatesAutoresizingMaskIntoConstraints = false - stack.axis = .vertical - stack.distribution = .fill - stack.alignment = .leading - stack.spacing = 8 - addSubview(stack) - self.stack = stack - - stack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - stack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - stack.topAnchor.constraint(equalTo: topAnchor, constant: 54).isActive = true - let height = heightAnchor.constraint(equalToConstant: 200) - height.priority = .init(rawValue: 500) - height.isActive = true - - let trees = TreesProjection.shared.treesAt(.init()) - let oneMillion = 1000000 - let millionTrees = trees / oneMillion - let multiplesOfFive = millionTrees / 5 - let capped = multiplesOfFive * 5 * oneMillion - let treesPlantedByTheCommunity = NumberFormatter.ecosiaDecimalNumberFormatter().string(from: .init(value: capped)) ?? "150M" - let countries = "30" - let activeProjects = "60" - - let top = WelcomeTourRow(image: "trees", - title: .init(format: .localized(.numberAsStringWithPlusSymbol), treesPlantedByTheCommunity), - text: .localized(.treesPlantedByEcosiaCapitalized)) - stack.addArrangedSubview(top) - - let middle = WelcomeTourRow(image: "hand", - title: .init(format: .localized(.numberAsStringWithPlusSymbol), activeProjects), - text: .localized(.activeProjects)) - stack.addArrangedSubview(middle) - - let bottom = WelcomeTourRow(image: "pins", - title: .init(format: .localized(.numberAsStringWithPlusSymbol), countries), - text: .localized(.countries)) - stack.addArrangedSubview(bottom) - } - - func applyTheme(theme: Theme) { - stack.arrangedSubviews.forEach { view in - (view as? ThemeApplicable)?.applyTheme(theme: theme) - } - } - - func updateAccessibilitySettings() { - isAccessibilityElement = false - shouldGroupAccessibilityChildren = true - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourGreen.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourGreen.swift deleted file mode 100644 index c498d20002dc0..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourGreen.swift +++ /dev/null @@ -1,110 +0,0 @@ -// 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 Common - -final class WelcomeTourGreen: UIView, ThemeApplicable { - private lazy var searchLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = .localized(.sustainableShoes) - label.font = .systemFont(ofSize: 15, weight: .semibold) - label.numberOfLines = 1 - label.textAlignment = .left - return label - }() - private lazy var counterLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = "10" - label.font = .systemFont(ofSize: 17).bold() - label.numberOfLines = 1 - label.textAlignment = .left - return label - }() - private lazy var counterSubtitleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = .localizedPlural(.searches, num: 500) - label.font = .systemFont(ofSize: 13) - label.numberOfLines = 1 - label.textAlignment = .left - return label - }() - - // MARK: - Init - - init(isCounterEnabled: Bool = false) { - super.init(frame: .zero) - setup(isCounterEnabled: isCounterEnabled) - updateAccessibilitySettings() - } - - required init?(coder: NSCoder) { nil } - - func setup(isCounterEnabled: Bool) { - let iPadOffset: CGFloat = traitCollection.userInterfaceIdiom == .pad ? 60 : 0 - - let stack = UIStackView() - stack.translatesAutoresizingMaskIntoConstraints = false - stack.axis = .vertical - stack.distribution = .fill - stack.alignment = .center - stack.spacing = 24 + iPadOffset - addSubview(stack) - - stack.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - stack.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -50 - iPadOffset).isActive = true - - let topImage = UIImageView(image: .init(named: "tourSearch")) - topImage.translatesAutoresizingMaskIntoConstraints = false - stack.addArrangedSubview(topImage) - - topImage.addSubview(searchLabel) - - searchLabel.leadingAnchor.constraint(equalTo: topImage.leadingAnchor, constant: 55).isActive = true - searchLabel.topAnchor.constraint(equalTo: topImage.topAnchor, constant: 35).isActive = true - searchLabel.trailingAnchor.constraint(equalTo: topImage.trailingAnchor, constant: -40).isActive = true - searchLabel.transform = .init(rotationAngle: Double.pi / -33) - - let bottomImage = UIImageView(image: .init(named: isCounterEnabled ? "tourCounter" : "tourGreen")) - bottomImage.translatesAutoresizingMaskIntoConstraints = false - stack.addArrangedSubview(bottomImage) - - if isCounterEnabled { - bottomImage.addSubview(counterLabel) - bottomImage.addSubview(counterSubtitleLabel) - - NSLayoutConstraint.activate([ - counterLabel.topAnchor.constraint(equalTo: bottomImage.topAnchor, constant: 28), - counterLabel.leadingAnchor.constraint(equalTo: bottomImage.leadingAnchor, constant: 65), - counterLabel.trailingAnchor.constraint(equalTo: bottomImage.trailingAnchor, constant: -46), - counterSubtitleLabel.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: 2), - counterSubtitleLabel.leadingAnchor.constraint(equalTo: counterLabel.leadingAnchor), - counterSubtitleLabel.trailingAnchor.constraint(equalTo: counterLabel.trailingAnchor) - ]) - let angle: CGFloat = .pi / 33 - counterLabel.transform = .init(rotationAngle: angle) - counterSubtitleLabel.transform = .init(rotationAngle: angle) - } - - // upscale images for iPad - if traitCollection.userInterfaceIdiom == .pad { - bottomImage.transform = bottomImage.transform.scaledBy(x: 1.5, y: 1.5) - topImage.transform = topImage.transform.scaledBy(x: 1.5, y: 1.5) - } - } - - func applyTheme(theme: Theme) { - searchLabel.textColor = theme.colors.ecosia.textPrimary - counterLabel.textColor = theme.colors.ecosia.textPrimary - counterSubtitleLabel.textColor = theme.colors.ecosia.textSecondary - } - - func updateAccessibilitySettings() { - isAccessibilityElement = false - shouldGroupAccessibilityChildren = true - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourProfit.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourProfit.swift deleted file mode 100644 index 9fd744f35352a..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourProfit.swift +++ /dev/null @@ -1,55 +0,0 @@ -// 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 Common - -final class WelcomeTourProfit: UIView, ThemeApplicable { - struct UX { - static let offsetY: CGFloat = 50 - } - - private lazy var beforeView: BeforeOrAfterView = { - let view = BeforeOrAfterView(type: .before) - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - private lazy var afterView: BeforeOrAfterView = { - let view = BeforeOrAfterView(type: .after) - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - // MARK: - Init - - init() { - super.init(frame: .zero) - setup() - updateAccessibilitySettings() - } - - required init?(coder: NSCoder) { nil } - - func setup() { - addSubview(beforeView) - addSubview(afterView) - - NSLayoutConstraint.activate([ - beforeView.leadingAnchor.constraint(equalTo: leadingAnchor), - beforeView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -UX.offsetY), - afterView.trailingAnchor.constraint(equalTo: trailingAnchor), - afterView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: UX.offsetY) - ]) - } - - func applyTheme(theme: Theme) { - beforeView.applyTheme(theme: theme) - afterView.applyTheme(theme: theme) - } - - func updateAccessibilitySettings() { - isAccessibilityElement = false - shouldGroupAccessibilityChildren = true - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourRow.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourRow.swift deleted file mode 100644 index e87e20904658a..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourRow.swift +++ /dev/null @@ -1,78 +0,0 @@ -// 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 Common -import Ecosia - -final class WelcomeTourRow: UIView, ThemeApplicable { - let image: String - let title: String - let text: String - - weak var titleLabel: UILabel! - weak var textLabel: UILabel! - - // MARK: - Init - - init(image: String, title: String, text: String) { - self.image = image - self.title = title - self.text = text - - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - func setup() { - layer.cornerRadius = 10 - - let stack = UIStackView() - stack.translatesAutoresizingMaskIntoConstraints = false - stack.axis = .horizontal - stack.alignment = .center - stack.spacing = 8 - addSubview(stack) - - stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12).isActive = true - stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12).isActive = true - stack.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true - stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8).isActive = true - - let imageView = UIImageView(image: .init(named: image, in: .ecosia, with: nil)) - imageView.heightAnchor.constraint(equalToConstant: 33).isActive = true - imageView.widthAnchor.constraint(equalToConstant: 33).isActive = true - stack.addArrangedSubview(imageView) - - let trailingStack = UIStackView() - trailingStack.spacing = 5 - trailingStack.axis = .vertical - trailingStack.alignment = .leading - - stack.addArrangedSubview(trailingStack) - - let titleLabel = UILabel() - titleLabel.text = title - titleLabel.font = .preferredFont(forTextStyle: .body).bold() - titleLabel.adjustsFontForContentSizeCategory = true - trailingStack.addArrangedSubview(titleLabel) - self.titleLabel = titleLabel - - let textLabel = UILabel() - textLabel.text = text - textLabel.font = .preferredFont(forTextStyle: .footnote) - textLabel.adjustsFontForContentSizeCategory = true - textLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - trailingStack.addArrangedSubview(textLabel) - self.textLabel = textLabel - } - - func applyTheme(theme: Theme) { - backgroundColor = theme.colors.ecosia.backgroundPrimary - titleLabel.textColor = theme.colors.ecosia.textPrimary - textLabel.textColor = theme.colors.ecosia.textSecondary - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourTransparent.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourTransparent.swift deleted file mode 100644 index 68afd7759848d..0000000000000 --- a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeTourTransparent.swift +++ /dev/null @@ -1,107 +0,0 @@ -// 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 Ecosia -import Common - -final class WelcomeTourTransparent: UIView, ThemeApplicable { - private weak var stack: UIStackView! - private weak var monthView: UIView! - private weak var monthViewLabel: UILabel! - - // MARK: - Init - - override init(frame: CGRect) { - super.init(frame: frame) - setup() - updateAccessibilitySettings() - } - - required init?(coder: NSCoder) { nil } - - func setup() { - let stack = UIStackView() - stack.translatesAutoresizingMaskIntoConstraints = false - stack.axis = .vertical - stack.distribution = .fill - stack.alignment = .leading - stack.spacing = 8 - addSubview(stack) - self.stack = stack - - stack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - stack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - stack.topAnchor.constraint(equalTo: topAnchor, constant: 54).isActive = true - let height = heightAnchor.constraint(equalToConstant: 200) - height.priority = .init(rawValue: 500) - height.isActive = true - - addMonthView(toStack: stack) - - let report = FinancialReports.shared.latestReport - if let totalIncome = NumberFormatter.ecosiaCurrency() - .string(from: .init(value: report.totalIncome)) { - let income = WelcomeTourRow(image: "financialReports", title: totalIncome, text: .localized(.totalIncome)) - stack.addArrangedSubview(income) - } - if let treesFinanced = NumberFormatter.ecosiaDecimalNumberFormatter().string(from: .init(value: report.numberOfTreesFinanced)) { - let trees = WelcomeTourRow(image: "treesUpdate", title: treesFinanced, text: .localized(.treesFinanced)) - stack.addArrangedSubview(trees) - } - } - - func applyTheme(theme: Theme) { - stack.arrangedSubviews.forEach { view in - (view as? ThemeApplicable)?.applyTheme(theme: theme) - } - applyThemeToMonthView(theme: theme) - } - - func updateAccessibilitySettings() { - isAccessibilityElement = false - shouldGroupAccessibilityChildren = true - } - - func addMonthView(toStack parentStack: UIStackView) { - let monthView = UIView() - monthView.translatesAutoresizingMaskIntoConstraints = false - parentStack.addArrangedSubview(monthView) - self.monthView = monthView - - let containerStack = UIStackView() - containerStack.axis = .horizontal - containerStack.alignment = .center - containerStack.spacing = 4 - containerStack.translatesAutoresizingMaskIntoConstraints = false - monthView.addSubview(containerStack) - - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = FinancialReports.shared.localizedMonthAndYear.localizedCapitalized - label.font = .preferredFont(forTextStyle: .footnote) - label.setContentCompressionResistancePriority(.required, for: .horizontal) - containerStack.addArrangedSubview(label) - self.monthViewLabel = label - - NSLayoutConstraint.activate([ - containerStack.topAnchor.constraint(equalTo: monthView.topAnchor, constant: 8), - containerStack.bottomAnchor.constraint(equalTo: monthView.bottomAnchor, constant: -8), - containerStack.leadingAnchor.constraint(equalTo: monthView.leadingAnchor, constant: 12), - containerStack.trailingAnchor.constraint(equalTo: monthView.trailingAnchor, constant: -12) - ]) - - // Force layout to calculate the frame before setting corner radius - monthView.layoutIfNeeded() - monthView.layer.cornerRadius = monthView.frame.height/2 - - // Adding view for extra spacing on parent stack below this specific view - parentStack.addArrangedSubview(UIView(frame: .init(width: 0, height: 8))) - } - - func applyThemeToMonthView(theme: Theme) { - monthView.backgroundColor = theme.colors.ecosia.buttonBackgroundPrimary - monthViewLabel.textColor = theme.colors.ecosia.textInversePrimary - } -} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeView.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeView.swift new file mode 100644 index 0000000000000..5d88362e7a762 --- /dev/null +++ b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeView.swift @@ -0,0 +1,447 @@ +// 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 +import Ecosia + +struct WelcomeView: View { + + // MARK: - UX Constants + private struct UX { + static let logoWidth: CGFloat = 112 + static let logoHeight: CGFloat = 28 + static let logoContainerSpacing: CGFloat = 12 + static let welcomeTextFontSize: CGFloat = 17 + static let logoTopOffsetIPhone: CGFloat = 46 + static let logoTopOffsetIPad: CGFloat = 66 + static let maskInitialHeight: CGFloat = 384 + static let maskInitialWidthMargin: CGFloat = 8 + static let maskCornerRadius: CGFloat = 16 + static let contentMaxWidthIPad: CGFloat = 479 + static let contentPadding: CGFloat = 16 + static let bodyTitleBottomSpacing: CGFloat = 20 + static let bodySubtitleBottomSpacing: CGFloat = 36 + static let buttonHeight: CGFloat = 48 + static let buttonCornerRadius: CGFloat = 24 + static let exitOffset: CGFloat = 50 + + // Gradient dimensions + static let centeredGradientSize: CGFloat = 210 + static let topGradientBottomOffset: CGFloat = 19 + static let bodyGradientTopOffset: CGFloat = 20 + static let bodyGradientBottomOffset: CGFloat = 24 + + // Animation timings (relative delays between phases) + static let initialDelay: TimeInterval = 0.5 + static let phase1Duration: TimeInterval = 0.5 + static let gradientFadeDuration: TimeInterval = 0.2 + static let phase2Delay: TimeInterval = 0.15 + static let phase2Duration: TimeInterval = 0.35 + static let phase3Delay: TimeInterval = 0.5 + static let phase3Duration: TimeInterval = 0.35 + static let exitDuration: TimeInterval = 0.35 + } + + // MARK: - State + + @State private var animationPhase: AnimationPhase = .initial + @State private var transitionMaskScale: CGFloat = 0.0 + @State private var transitionMaskHeight: CGFloat = UX.maskInitialHeight + @State private var transitionMaskWidth: CGFloat = 0.0 + @State private var welcomeTextOpacity: Double = 0.0 + @State private var logoOpacity: Double = 1.0 + @State private var logoColor = Color(uiColor: UIColor.systemBackground) // Will be brandPrimary + @State private var logoOffset: CGFloat = 0.0 + @State private var welcomeTextOffset: CGFloat = 0.0 + @State private var bodyOpacity: Double = 0.0 + @State private var bodyOffset: CGFloat = 0.0 + @State private var showVideoBackground: Bool = false + @State private var backgroundOpacity: Double = 1.0 + @State private var centeredGradientOpacity: Double = 0.0 + @State private var topGradientOpacity: Double = 0.0 + @State private var bodyGradientOpacity: Double = 0.0 + @State private var theme = WelcomeViewTheme() + @State private var isVideoReady = false + @State private var animationTask: Task? + @State private var hasAppeared = false + + private let reduceMotionEnabled = UIAccessibility.isReduceMotionEnabled + + let windowUUID: WindowUUID + let onFinish: () -> Void + + enum AnimationPhase { + case initial + case phase1Complete + case phase2Complete + case phase3Complete + case final + } + + var body: some View { + ZStack(alignment: .center) { + // Matching Launch Screen background + Color(.systemBackground) + .ignoresSafeArea() + .opacity(backgroundOpacity) + + // Video background (clipped to transition mask) + ZStack { + LoopingVideoPlayer(videoName: "welcome_background") { + isVideoReady = true + } + + // Centered radial gradient behind logo + RadialGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0.55), + Color.black.opacity(0) + ]), + center: .center, + startRadius: 0, + endRadius: UX.centeredGradientSize / 2 + ) + .opacity(centeredGradientOpacity) + } + .mask( + RoundedRectangle(cornerRadius: UX.maskCornerRadius) + .frame(height: transitionMaskHeight) + .frame(maxWidth: transitionMaskWidth) + .scaleEffect(transitionMaskScale, anchor: .center) + ) + .ignoresSafeArea(edges: .all) + .opacity(showVideoBackground ? 1 : 0) + + // Top vertical gradient behind logo + if animationPhase == .phase3Complete { + VStack(spacing: 0) { + LinearGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0.38), + Color.black.opacity(0) + ]), + startPoint: .top, + endPoint: .bottom + ) + .frame(height: topGradientHeight) + + Spacer() + } + .ignoresSafeArea() + .opacity(topGradientOpacity) + } + + // Welcome text + Text(verbatim: .localized(.welcomeTo)) + .font(.system(size: UX.welcomeTextFontSize, weight: .semibold)) + .foregroundColor(theme.contentTextColor) + .multilineTextAlignment(.center) + .opacity(welcomeTextOpacity) + .offset(y: welcomeTextOffset) + .frame(maxWidth: transitionMaskWidth) + + // Logo + Image("ecosiaLogoLaunch") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(logoColor) + .frame(width: UX.logoWidth, height: UX.logoHeight) + .opacity(logoOpacity) + .offset(y: logoOffset) + .frame(maxWidth: transitionMaskWidth) + .accessibilityLabel(String.localized(.ecosiaLogoAccessibilityLabel)) + .accessibilityIdentifier(AccessibilityIdentifiers.Ecosia.logo) + + // Content + if animationPhase == .phase3Complete { + VStack { + Spacer() + + VStack(spacing: UX.bodyTitleBottomSpacing) { + Text(verbatim: .localized(.realChangeAtYourFingertips)) + .font(.ecosiaFamilyBrand(size: .ecosia.font._6l)) + .foregroundStyle(theme.contentTextColor) + .multilineTextAlignment(.center) + + Text(verbatim: .localized(.joinMillionsPeople)) + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(theme.contentTextColor) + .multilineTextAlignment(.center) + } + .padding(.top, UX.bodyGradientTopOffset) + .padding(.bottom, UX.bodySubtitleBottomSpacing) + .background( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.black.opacity(0), location: 0.0), + .init(color: Color.black.opacity(0.35), location: 0.3), + .init(color: Color.black.opacity(0.24), location: 0.6), + .init(color: Color.black.opacity(0), location: 1.0) + ]), + startPoint: .top, + endPoint: .bottom + ) + .frame(width: screenWidth) + .opacity(bodyGradientOpacity) + ) + + Button(action: { + Analytics.shared.introWelcome(action: .click) + startExitAnimation() + }) { + Text(verbatim: .localized(.getStarted)) + .font(.body) + .foregroundColor(theme.buttonTextColor) + .frame(maxWidth: .infinity) + .frame(height: UX.buttonHeight) + .background(theme.buttonBackgroundColor) + .cornerRadius(UX.buttonCornerRadius) + } + } + .padding(.horizontal, UX.contentPadding) + .padding(.top, UX.contentPadding) + .padding(.bottom, UX.contentPadding + safeAreaBottom) + .frame(maxWidth: contentMaxWidth) + .opacity(bodyOpacity) + .offset(y: bodyOffset) + } + } + // Needed so initial state matches launch screen + .ignoresSafeArea() + // Theme has to be applied before onAppear for logo color + .ecosiaThemed(windowUUID, $theme) + .onAppear { + guard !hasAppeared else { return } + hasAppeared = true + + Analytics.shared.introWelcome(action: .display) + + logoColor = theme.brandPrimaryColor + + if reduceMotionEnabled { + skipToFinalState() + } else { + // Animation will start when video is ready + } + } + .onChange(of: isVideoReady) { ready in + if ready && !reduceMotionEnabled && hasAppeared { + startAnimationSequence() + } + } + } + + private func skipToFinalState() { + // For reduced motion: skip animations and go directly to final state + showVideoBackground = true + transitionMaskScale = 1.0 + transitionMaskHeight = screenHeight + transitionMaskWidth = screenWidth + logoColor = theme.contentTextColor + welcomeTextOpacity = 1.0 + logoOpacity = 1.0 + logoOffset = phase3LogoOffset + welcomeTextOffset = phase3WelcomeTextOffset + bodyOpacity = 1.0 + topGradientOpacity = 1.0 + bodyGradientOpacity = 1.0 + centeredGradientOpacity = 0.0 + backgroundOpacity = 0.0 + animationPhase = .phase3Complete + } + + private func startAnimationSequence() { + animationTask?.cancel() + animationTask = Task { @MainActor in + // Phase 1: Show centered rounded square mask with logo + try? await Task.sleep(duration: UX.initialDelay) + guard !Task.isCancelled else { return } + + showVideoBackground = true + transitionMaskWidth = screenWidth - UX.maskInitialWidthMargin * 2 + + await animate(duration: UX.phase1Duration) { + transitionMaskScale = 1.0 + logoColor = theme.contentTextColor + } + + guard !Task.isCancelled else { return } + + // Fade in centered gradient directly after phase 1 ends + await animate(duration: UX.gradientFadeDuration) { + centeredGradientOpacity = 1.0 + } + + guard !Task.isCancelled else { return } + + // Phase 2: Animate in welcome text above, move logo down + try? await Task.sleep(duration: UX.phase2Delay) + guard !Task.isCancelled else { return } + + await animate(duration: UX.phase2Duration) { + welcomeTextOpacity = 1.0 + welcomeTextOffset = phase2WelcomeTextOffset + logoOffset = phase2LogoOffset + animationPhase = .phase1Complete + } + + guard !Task.isCancelled else { return } + + // Phase 3: Grow window to full screen, move both to final position, show body + try? await Task.sleep(duration: UX.phase3Delay) + guard !Task.isCancelled else { return } + + await animate(duration: UX.phase3Duration) { + transitionMaskScale = 1.0 // Already at full scale from phase 1 + transitionMaskHeight = screenHeight + transitionMaskWidth = screenWidth + logoOffset = phase3LogoOffset + welcomeTextOffset = phase3WelcomeTextOffset + bodyOpacity = 1.0 + topGradientOpacity = 1.0 + bodyGradientOpacity = 1.0 + centeredGradientOpacity = 0.0 + backgroundOpacity = 0.0 + animationPhase = .phase3Complete + } + } + } + + @MainActor + private func animate(duration: TimeInterval, _ updates: @escaping () -> Void) async { + withAnimation(.easeInOut(duration: duration)) { + updates() + } + try? await Task.sleep(duration: duration) + } + + private func startExitAnimation() { + animationTask?.cancel() + + // Phase 4: Exit transition - move content out while fading + animationTask = Task { @MainActor in + await animate(duration: UX.exitDuration) { + logoOffset = exitLogoOffset + welcomeTextOffset = exitWelcomeTextOffset + logoOpacity = 0.0 + bodyOffset = UX.exitOffset + bodyOpacity = 0.0 + welcomeTextOpacity = 0.0 + backgroundOpacity = 1.0 + animationPhase = .final + } + + guard !Task.isCancelled else { return } + onFinish() + } + } + + private var simplestWayString: String { + .localized(.theSimplestWay) + } +} + +// MARK: - Dynamic Layout Calculations + +extension WelcomeView { + + private var screenHeight: CGFloat { + UIScreen.main.bounds.height + } + + private var screenWidth: CGFloat { + UIScreen.main.bounds.width + } + + private var isIPad: Bool { + UIDevice.current.userInterfaceIdiom == .pad + } + + private var logoTopOffset: CGFloat { + isIPad ? UX.logoTopOffsetIPad : UX.logoTopOffsetIPhone + } + + private var welcomeTextHeight: CGFloat { + let font = UIFont.systemFont(ofSize: UX.welcomeTextFontSize, weight: .semibold) + return font.lineHeight + } + + private var logoContainerHeight: CGFloat { + welcomeTextHeight + UX.logoContainerSpacing + UX.logoHeight + } + + private var safeAreaTop: CGFloat { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + return window.safeAreaInsets.top + } + return 0 + } + + private var safeAreaBottom: CGFloat { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + return window.safeAreaInsets.bottom + } + return 0 + } + + // Logo moves down to make room for welcome text + private var phase2LogoOffset: CGFloat { + (welcomeTextHeight + UX.logoContainerSpacing) / 2 + } + + // Welcome text appears above the logo, maintaining spacing + private var phase2WelcomeTextOffset: CGFloat { + phase2LogoOffset - (UX.logoHeight / 2) - UX.logoContainerSpacing - (welcomeTextHeight / 2) + } + + // Position welcome text at specified offset from top + private var phase3WelcomeTextOffset: CGFloat { + let distanceFromTop = safeAreaTop + logoTopOffset + (welcomeTextHeight / 2) + return distanceFromTop - (screenHeight / 2) + } + + // Logo positioned below welcome text + private var phase3LogoOffset: CGFloat { + phase3WelcomeTextOffset + (welcomeTextHeight / 2) + UX.logoContainerSpacing + (UX.logoHeight / 2) + } + + // Move further up by the exit offset + private var exitLogoOffset: CGFloat { + phase3LogoOffset - UX.exitOffset + } + + private var exitWelcomeTextOffset: CGFloat { + phase3WelcomeTextOffset - UX.exitOffset + } + + private var contentMaxWidth: CGFloat { + isIPad ? UX.contentMaxWidthIPad : .infinity + } + + // Top gradient extends from top of screen to some points below logo + private var topGradientHeight: CGFloat { + let logoBottomY = screenHeight / 2 + phase3LogoOffset + (UX.logoHeight / 2) + return logoBottomY + UX.topGradientBottomOffset + } +} + +// MARK: - WelcomeViewTheme + +struct WelcomeViewTheme: EcosiaThemeable { + var contentTextColor = Color.white + var buttonTextColor = Color.white + var buttonBackgroundColor = Color.green + var brandPrimaryColor = Color.green + + mutating func applyTheme(theme: Theme) { + contentTextColor = Color(theme.colors.ecosia.textStaticLight) + buttonTextColor = Color(theme.colors.ecosia.buttonContentSecondaryStatic) + buttonBackgroundColor = Color(theme.colors.ecosia.buttonBackgroundFeatured) + brandPrimaryColor = Color(theme.colors.ecosia.brandPrimary) + } +} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeViewController.swift b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeViewController.swift new file mode 100644 index 0000000000000..0ce7474eeafef --- /dev/null +++ b/firefox-ios/Client/Ecosia/UI/Onboarding/WelcomeViewController.swift @@ -0,0 +1,57 @@ +// 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 Common +import Ecosia +import SwiftUI + +protocol WelcomeDelegate: AnyObject { + func welcomeDidFinish(_ welcome: WelcomeViewController) +} + +final class WelcomeViewController: UIViewController { + private weak var delegate: WelcomeDelegate? + let windowUUID: WindowUUID + + required init?(coder: NSCoder) { nil } + + init(delegate: WelcomeDelegate, windowUUID: WindowUUID) { + self.delegate = delegate + self.windowUUID = windowUUID + super.init(nibName: nil, bundle: nil) + modalPresentationCapturesStatusBarAppearance = true + definesPresentationContext = true + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override func viewDidLoad() { + super.viewDidLoad() + + let swiftUIView = WelcomeView( + windowUUID: windowUUID, + onFinish: { [weak self] in + guard let self = self else { return } + self.delegate?.welcomeDidFinish(self) + } + ) + + let hostingController = UIHostingController(rootView: swiftUIView) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } +} diff --git a/firefox-ios/Client/Ecosia/UI/Onboarding/welcome_background.mov b/firefox-ios/Client/Ecosia/UI/Onboarding/welcome_background.mov new file mode 100644 index 0000000000000..71327a6ef395a Binary files /dev/null and b/firefox-ios/Client/Ecosia/UI/Onboarding/welcome_background.mov differ diff --git a/firefox-ios/Client/Ecosia/UI/Theme/EcosiaDarkTheme.swift b/firefox-ios/Client/Ecosia/UI/Theme/EcosiaDarkTheme.swift index ae565a5103dae..d1df2738d8af3 100644 --- a/firefox-ios/Client/Ecosia/UI/Theme/EcosiaDarkTheme.swift +++ b/firefox-ios/Client/Ecosia/UI/Theme/EcosiaDarkTheme.swift @@ -55,4 +55,5 @@ private struct EcosiaDarkSemanticColors: EcosiaSemanticColors { var textPrimary: UIColor = EcosiaColor.White var textInversePrimary: UIColor = EcosiaColor.Gray90 var textSecondary: UIColor = EcosiaColor.Gray30 + var textStaticLight: UIColor = EcosiaColor.White } diff --git a/firefox-ios/Client/Ecosia/UI/Theme/EcosiaLightTheme.swift b/firefox-ios/Client/Ecosia/UI/Theme/EcosiaLightTheme.swift index 43eb6c5a87e55..9e672bd8bc287 100644 --- a/firefox-ios/Client/Ecosia/UI/Theme/EcosiaLightTheme.swift +++ b/firefox-ios/Client/Ecosia/UI/Theme/EcosiaLightTheme.swift @@ -145,4 +145,5 @@ private struct EcosiaLightSemanticColors: EcosiaSemanticColors { var textPrimary: UIColor = EcosiaColor.Gray70 var textInversePrimary: UIColor = EcosiaColor.White var textSecondary: UIColor = EcosiaColor.Gray50 + var textStaticLight: UIColor = EcosiaColor.White } diff --git a/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift b/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift index 188bf7ea200b1..b8d52e1c6775e 100644 --- a/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift +++ b/firefox-ios/Client/Frontend/Browser/Event Queue/AppEvent.swift @@ -21,6 +21,8 @@ public enum AppEvent: AppEventType { case postLaunchDependenciesComplete case accountManagerInitialized case browserIsReady + // Ecosia: Add Feature Management event + case featureManagementInitialized // Activities: Profile Syncing case profileSyncing diff --git a/firefox-ios/Ecosia/Analytics/Analytics.Values.swift b/firefox-ios/Ecosia/Analytics/Analytics.Values.swift index 4de4903434737..487499315916c 100644 --- a/firefox-ios/Ecosia/Analytics/Analytics.Values.swift +++ b/firefox-ios/Ecosia/Analytics/Analytics.Values.swift @@ -98,8 +98,7 @@ extension Analytics { public enum Onboarding: String { case - next, - skip + welcome = "welcome_screen" } public enum Referral: String { @@ -211,6 +210,12 @@ extension Analytics { remove, unpin } + + public enum Welcome: String { + case + click, + display + } } public enum Property: String { diff --git a/firefox-ios/Ecosia/Analytics/Analytics.swift b/firefox-ios/Ecosia/Analytics/Analytics.swift index 55ed5d70a2d67..180bb08149f0c 100644 --- a/firefox-ios/Ecosia/Analytics/Analytics.swift +++ b/firefox-ios/Ecosia/Analytics/Analytics.swift @@ -243,24 +243,10 @@ open class Analytics { } // MARK: Onboarding - public func introDisplaying(page: Property.OnboardingPage?) { - guard let page else { - return - } + public func introWelcome(action: Action.Welcome) { let event = Structured(category: Category.intro.rawValue, - action: Action.display.rawValue) - .property(page.rawValue) - track(event) - } - - public func introClick(_ label: Label.Onboarding, page: Property.OnboardingPage?) { - guard let page else { - return - } - let event = Structured(category: Category.intro.rawValue, - action: Action.click.rawValue) - .label(label.rawValue) - .property(page.rawValue) + action: action.rawValue) + .label(Label.Onboarding.welcome.rawValue) track(event) } diff --git a/firefox-ios/Ecosia/Braze/APNConsent.swift b/firefox-ios/Ecosia/Braze/APNConsent.swift index c051e2577897c..64e596d18480d 100644 --- a/firefox-ios/Ecosia/Braze/APNConsent.swift +++ b/firefox-ios/Ecosia/Braze/APNConsent.swift @@ -8,6 +8,10 @@ public struct APNConsent { private init() {} public static func requestIfNeeded() async { + // Do not ask for push consent during Onboarding experiment + guard !OnboardingProductTourExperiment.isEnabled else { + return + } guard BrazeService.shared.notificationAuthorizationStatus == .notDetermined else { return } diff --git a/firefox-ios/Ecosia/Core/FeatureManagement/Unleash/Unleash.Model.swift b/firefox-ios/Ecosia/Core/FeatureManagement/Unleash/Unleash.Model.swift index cfe4678dafe3b..e209bcc82c8ff 100644 --- a/firefox-ios/Ecosia/Core/FeatureManagement/Unleash/Unleash.Model.swift +++ b/firefox-ios/Ecosia/Core/FeatureManagement/Unleash/Unleash.Model.swift @@ -20,12 +20,13 @@ extension Unleash { public struct Toggle: Codable, Hashable { public enum Name: String { + case aiSearchMVP = "ai2-67-ai-search-mvp" case brazeIntegration = "mob_ios_braze_integration" case configTest = "mob_ios_staging_config" case seedCounterNTP = "mob_ios_seed_counter_ntp" case nativeSRPVAnalytics = "mob_ios_native_srpv_analytics" case newsletterCard = "mob_ios_newsletter_card" - case aiSearchMVP = "ai2-67-ai-search-mvp" + case onboardingProductTour = "mob_ios_onboarding_product_tour" } public let name: String diff --git a/firefox-ios/Ecosia/Core/MMP/MMP.swift b/firefox-ios/Ecosia/Core/MMP/MMP.swift index 535a54eb89568..a721131a69f60 100644 --- a/firefox-ios/Ecosia/Core/MMP/MMP.swift +++ b/firefox-ios/Ecosia/Core/MMP/MMP.swift @@ -11,8 +11,6 @@ import AdServices import Common public enum MMPEvent: String { - case onboardingStart = "onboarding_start" - case onboardingComplete = "onboarding_complete" case firstSearch = "first_search" case fifthSearch = "fifth_search" case tenthSearch = "tenth_search" diff --git a/firefox-ios/Ecosia/Experiments/Unleash/OnboardingProductTourExperiment.swift b/firefox-ios/Ecosia/Experiments/Unleash/OnboardingProductTourExperiment.swift new file mode 100644 index 0000000000000..b505512397951 --- /dev/null +++ b/firefox-ios/Ecosia/Experiments/Unleash/OnboardingProductTourExperiment.swift @@ -0,0 +1,23 @@ +// 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 + +public struct OnboardingProductTourExperiment { + + private init() {} + + public static var isEnabled: Bool { + Unleash.isEnabled(.onboardingProductTour) && !isControl + } + + private static var variant: Unleash.Variant { + Unleash.getVariant(.onboardingProductTour) + } + + private static let controlVariantName: String = "control" + public static var isControl: Bool { + variant.name == controlVariantName + } +} diff --git a/firefox-ios/Ecosia/L10N/String.swift b/firefox-ios/Ecosia/L10N/String.swift index 62b7fa0f8dc15..4c28db064150e 100644 --- a/firefox-ios/Ecosia/L10N/String.swift +++ b/firefox-ios/Ecosia/L10N/String.swift @@ -44,6 +44,9 @@ extension String { case getStarted = "Get started" case home = "Home" case homepage = "Homepage" + case welcomeTo = "Welcome to" + case realChangeAtYourFingertips = "Real change\nat your fingertips" + case joinMillionsPeople = "Join 20 million people making a difference every day" case invalidReferralLink = "Invalid referral link!" case invalidReferralLinkMessage = "Your referral link is wrong or not valid for you. Please check it and try again." case invertColors = "Invert website colors" @@ -139,15 +142,6 @@ extension String { case tapLinkToConfirm = "If you’re using an iPhone or iPad, tap here to confirm you’ve joined:" case seeTheCollectiveImpact = "See the collective impact you are having with the Ecosia community" case theSimplestWay = "The simplest way to be \n climate-active every day while \n browsing the web" - case skipWelcomeTour = "Skip welcome tour" - case grennestWayToSearch = "The greenest way to search" - case planetFriendlySearch = "Ecosia is the world's most planet-friendly way to search - and it's free." - case hundredPercentOfProfits = "100% of profits for the planet" - case weUseAllOurProfits = "All our profits go directly to reforestation and renewable energy around the world." - case collectiveAction = "Collective action starts here" - case join15Million = "Join 15 million people growing the right trees in the right places." - case realResults = "Real results, transparent finances" - case shownExactlyHowMuch = "You're shown exactly how much we earn and invest in trees and climate action." case totalIncome = "Total income" case treesFinanced = "Trees financed" case skip = "Skip" @@ -161,7 +155,6 @@ extension String { case dedicatedToClimateAction = "dedicated to climate action" case activeProjects = "Active projects" case countries = "Countries" - case finishTour = "Start Planting" case treesPlantedPlural = "Tree(s) planted" case howItWorks = "How it works" case notNow = "Not now" @@ -205,15 +198,6 @@ extension String { case sendUsageDataSettingsDescription = "To improve our browser apps, we collect usage statistics from your device. These are anonymous and protect your privacy." case impactSectionAccessibilityHint = "Open the Your Impact section" case impactSectionAccessibilityLabel = "Your Impact section, highlithing the trees planted by yourself and the Ecosia community overall. You have contributed to plant %@ trees. The total number of trees planted by the Ecosia community has reached %@" - case onboardingPageControlDotsAccessibility = "Page control dots" - case onboardingBackButtonAccessibility = "Back" - case onboardingSkipTourButtonAccessibility = "Skip the onboarding" - case onboardingContinueCTAButtonAccessibility = "Continue to the next onboarding page" - case onboardingFinishCTAButtonAccessibility = "Finish onboarding and start contributing to Ecosia" - case onboardingIllustrationTour1 = "This onboarding illustration shows how by performing searches via the Ecosia app, you are leveling up your planed-friendly lifestyle. A small search input field screenshot and result example containing the green icon is shown. A forest can be seen on the background." - case onboardingIllustrationTour2 = "This onboarding illustration shows briefly an example of a before and after comparision of trees planted in a land. The image is a screenshot from the satellite view." - case onboardingIllustrationTour3 = "This onboarding illustration shows a few numbers like the projects Ecosia is involved in, the total number of trees planted by the Ecosia community, alongisde the number of countries Ecosia is active. A small map of the planisphere with trees pins in few geographic location, background." - case onboardingIllustrationTour4 = "This onboarding illustration shows the latest financial reports of Ecosia. On the background there is an image of a person caring for tree seedlings" case whatsNewViewTitle = "What's new" case whatsNewFirstItemTitle9_0_0 = "Collective action" case whatsNewFirstItemDescription9_0_0 = "See the climate impact you are having together with the rest of the Ecosia community." diff --git a/firefox-ios/Ecosia/L10N/de.lproj/Ecosia.strings b/firefox-ios/Ecosia/L10N/de.lproj/Ecosia.strings index 241a9e8f56c7c..c3153fbb091c3 100644 Binary files a/firefox-ios/Ecosia/L10N/de.lproj/Ecosia.strings and b/firefox-ios/Ecosia/L10N/de.lproj/Ecosia.strings differ diff --git a/firefox-ios/Ecosia/L10N/en.lproj/Ecosia.strings b/firefox-ios/Ecosia/L10N/en.lproj/Ecosia.strings index eb4003817acd0..fbea16245d373 100644 Binary files a/firefox-ios/Ecosia/L10N/en.lproj/Ecosia.strings and b/firefox-ios/Ecosia/L10N/en.lproj/Ecosia.strings differ diff --git a/firefox-ios/Ecosia/L10N/es.lproj/Ecosia.strings b/firefox-ios/Ecosia/L10N/es.lproj/Ecosia.strings index a8fe2d1f4d2d1..ec1869b171d40 100644 Binary files a/firefox-ios/Ecosia/L10N/es.lproj/Ecosia.strings and b/firefox-ios/Ecosia/L10N/es.lproj/Ecosia.strings differ diff --git a/firefox-ios/Ecosia/L10N/fr.lproj/Ecosia.strings b/firefox-ios/Ecosia/L10N/fr.lproj/Ecosia.strings index 93e6e85b304d6..fff1b97957e28 100644 Binary files a/firefox-ios/Ecosia/L10N/fr.lproj/Ecosia.strings and b/firefox-ios/Ecosia/L10N/fr.lproj/Ecosia.strings differ diff --git a/firefox-ios/Ecosia/L10N/it.lproj/Ecosia.strings b/firefox-ios/Ecosia/L10N/it.lproj/Ecosia.strings index b00fd72d47e31..b1cac285e2390 100644 Binary files a/firefox-ios/Ecosia/L10N/it.lproj/Ecosia.strings and b/firefox-ios/Ecosia/L10N/it.lproj/Ecosia.strings differ diff --git a/firefox-ios/Ecosia/L10N/nl.lproj/Ecosia.strings b/firefox-ios/Ecosia/L10N/nl.lproj/Ecosia.strings index 6f07cf53100b8..55833cec73ecc 100644 Binary files a/firefox-ios/Ecosia/L10N/nl.lproj/Ecosia.strings and b/firefox-ios/Ecosia/L10N/nl.lproj/Ecosia.strings differ diff --git a/firefox-ios/Ecosia/UI/DesignSystem/UIFont+DesignSystem.swift b/firefox-ios/Ecosia/UI/DesignSystem/UIFont+DesignSystem.swift index 837e27683eb79..016ad25413fd1 100644 --- a/firefox-ios/Ecosia/UI/DesignSystem/UIFont+DesignSystem.swift +++ b/firefox-ios/Ecosia/UI/DesignSystem/UIFont+DesignSystem.swift @@ -3,11 +3,20 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit +import SwiftUI + +fileprivate let familyBrandFontName = "FoundersGroteskCond-SmBd" extension UIFont { - private static let familyBrandFontName = "FoundersGroteskCond-SmBd" public static func ecosiaFamilyBrand(size: CGFloat) -> UIFont { return UIFont(name: familyBrandFontName, size: size) ?? systemFont(ofSize: size, weight: .semibold) } } + +extension Font { + + public static func ecosiaFamilyBrand(size: CGFloat) -> Font { + return .custom(familyBrandFontName, size: size) + } +} diff --git a/firefox-ios/EcosiaTests/Analytics/AnalyticsSpyTests.swift b/firefox-ios/EcosiaTests/Analytics/AnalyticsSpyTests.swift index 9ad9598ca28ca..6020d876c98ed 100644 --- a/firefox-ios/EcosiaTests/Analytics/AnalyticsSpyTests.swift +++ b/firefox-ios/EcosiaTests/Analytics/AnalyticsSpyTests.swift @@ -68,16 +68,9 @@ final class AnalyticsSpy: Analytics { } } - var introDisplayingPageCalled: Property.OnboardingPage? - override func introDisplaying(page: Property.OnboardingPage?) { - introDisplayingPageCalled = page - } - - var introClickLabelCalled: Label.Onboarding? - var introClickPageCalled: Property.OnboardingPage? - override func introClick(_ label: Label.Onboarding, page: Property.OnboardingPage?) { - introClickLabelCalled = label - introClickPageCalled = page + var introWelcomeActionCalled: Action.Welcome? + override func introWelcome(action: Action.Welcome) { + introWelcomeActionCalled = action } var navigationActionCalled: Action? @@ -394,127 +387,6 @@ final class AnalyticsSpyTests: XCTestCase { } } - // MARK: - Onboarding / Welcome Tests - - func testWelcomeViewDidAppearTracksIntroDisplayingAndIntroClickStart() { - // Arrange - let welcome = makeWelcome() - XCTAssertNil(analyticsSpy.introDisplayingPageCalled) - - // Act - welcome.loadViewIfNeeded() - welcome.viewDidAppear(false) - - // Assert - XCTAssertEqual(analyticsSpy.introDisplayingPageCalled, .start, "Analytics should track intro displaying page as .start.") - } - - func testWelcomeGetStartedTracksIntroClickNext() { - // Arrange - let welcome = makeWelcome() - XCTAssertNil(analyticsSpy.introClickLabelCalled) - XCTAssertNil(analyticsSpy.introClickPageCalled) - - // Act - welcome.getStarted() - - // Assert - XCTAssertEqual(analyticsSpy.introClickLabelCalled, .next, "Analytics should track intro click label as .next.") - XCTAssertEqual(analyticsSpy.introClickPageCalled, .start, "Analytics should track intro click page as .start.") - } - - func testWelcomeSkipTracksIntroClickSkip() { - // Arrange - let welcome = makeWelcome() - XCTAssertNil(analyticsSpy.introClickLabelCalled) - XCTAssertNil(analyticsSpy.introClickPageCalled) - - // Act - welcome.skip() - - // Assert - XCTAssertEqual(analyticsSpy.introClickLabelCalled, .skip, "Analytics should track intro click label as .skip.") - XCTAssertEqual(analyticsSpy.introClickPageCalled, .start, "Analytics should track intro click page as .start.") - } - - // MARK: - Onboarding / Welcome Tour Tests - - func testWelcomeTourViewDidAppearTracksIntroDisplaying() { - // Arrange - let welcomeTour = makeWelcomeTour() - XCTAssertNil(analyticsSpy.introDisplayingPageCalled) - - // Act - welcomeTour.loadViewIfNeeded() - welcomeTour.viewDidAppear(false) - - // Assert - XCTAssertEqual(analyticsSpy.introDisplayingPageCalled, .greenSearch, "Analytics should track intro displaying page as .greenSearch.") - } - - func testWelcomeTourNextTracksIntroClickNext() { - // Arrange - let welcomeTour = makeWelcomeTour() - XCTAssertNil(analyticsSpy.introClickLabelCalled) - XCTAssertNil(analyticsSpy.introClickPageCalled) - - // Act - welcomeTour.loadViewIfNeeded() - welcomeTour.viewDidAppear(false) - welcomeTour.forward() - - // Assert - XCTAssertEqual(analyticsSpy.introClickLabelCalled, .next, "Analytics should track intro click label as .next.") - XCTAssertEqual(analyticsSpy.introClickPageCalled, .greenSearch, "Analytics should track intro click page as .greenSearch.") - } - - func testWelcomeTourSkipTracksIntroClickSkip() { - // Arrange - let welcomeTour = makeWelcomeTour() - XCTAssertNil(analyticsSpy.introClickLabelCalled) - XCTAssertNil(analyticsSpy.introClickPageCalled) - - // Act - welcomeTour.loadViewIfNeeded() - welcomeTour.viewDidAppear(false) - welcomeTour.skip() - - // Assert - XCTAssertEqual(analyticsSpy.introClickLabelCalled, .skip, "Analytics should track intro click label as .skip.") - XCTAssertEqual(analyticsSpy.introClickPageCalled, .greenSearch, "Analytics should track intro click page as .greenSearch.") - } - - func testWelcomeTourTracksAnalyticsForAllPages() { - // Arrange - let welcomeTour = makeWelcomeTour() - let pages: [Analytics.Property.OnboardingPage] = [ - .greenSearch, - .profits, - .action, - .transparentFinances - ] - welcomeTour.loadViewIfNeeded() - welcomeTour.viewDidAppear(false) - - for (index, page) in pages.enumerated() { - // Reset analyticsSpy properties - analyticsSpy.introDisplayingPageCalled = nil - - if index < pages.count - 1 { - // Act - welcomeTour.forward() - - // Assert - XCTAssertEqual(analyticsSpy.introClickLabelCalled, .next, "Analytics should track intro click label as .next.") - XCTAssertEqual(analyticsSpy.introClickPageCalled, page, "Analytics should track intro click page as \(page).") - } - - // Reset analyticsSpy properties - analyticsSpy.introClickLabelCalled = nil - analyticsSpy.introClickPageCalled = nil - } - } - // MARK: - News Detail Tests func testNewsControllerViewDidAppearTracksNavigationViewNews() { @@ -962,12 +834,8 @@ extension AnalyticsSpyTests { return analyticsSpy } - func makeWelcomeTour() -> WelcomeTour { - WelcomeTour(delegate: MockWelcomeTourDelegate(), windowUUID: .XCTestDefaultUUID) - } - - func makeWelcome() -> Welcome { - Welcome(delegate: MockWelcomeDelegate(), windowUUID: .XCTestDefaultUUID) + func makeWelcomeView() -> WelcomeView { + WelcomeView(windowUUID: .XCTestDefaultUUID, onFinish: {}) } func makeInstructionsViewSUT(onButtonTap: @escaping () -> Void = {}) -> InstructionStepsView { diff --git a/firefox-ios/EcosiaTests/Mocks/MockWelcomeDelegate.swift b/firefox-ios/EcosiaTests/Mocks/MockWelcomeDelegate.swift index 81743eb076860..4cc7729d5fe7d 100644 --- a/firefox-ios/EcosiaTests/Mocks/MockWelcomeDelegate.swift +++ b/firefox-ios/EcosiaTests/Mocks/MockWelcomeDelegate.swift @@ -5,9 +5,6 @@ @testable import Client final class MockWelcomeDelegate: WelcomeDelegate { - func welcomeDidFinish(_ welcome: Welcome) {} + func welcomeDidFinish(_ welcome: WelcomeViewController) {} } -final class MockWelcomeTourDelegate: WelcomeTourDelegate { - func welcomeTourDidFinish(_ tour: WelcomeTour) {} -} diff --git a/firefox-ios/EcosiaTests/SnapshotTests/Onboarding/OnboardingTests.swift b/firefox-ios/EcosiaTests/SnapshotTests/Onboarding/OnboardingTests.swift index 0b073bca61940..235fdecb0affe 100644 --- a/firefox-ios/EcosiaTests/SnapshotTests/Onboarding/OnboardingTests.swift +++ b/firefox-ios/EcosiaTests/SnapshotTests/Onboarding/OnboardingTests.swift @@ -10,29 +10,7 @@ final class OnboardingTests: SnapshotBaseTests { func testWelcomeScreen() { SnapshotTestHelper.assertSnapshot(initializingWith: { - Welcome(delegate: MockWelcomeDelegate(), windowUUID: .snapshotTestDefaultUUID) + WelcomeViewController(delegate: MockWelcomeDelegate(), windowUUID: .snapshotTestDefaultUUID) }, wait: 1.0) } - - func testWelcomeStepsScreens() { - // Number of steps in the WelcomeTour - let numberOfSteps = 4 - // Iterate through steps and take snapshots, skipping the first one - for step in 1...numberOfSteps { - let startingStep = WelcomeTour.Step.all[step-1] - /* - Precision at .95 to accommodate a snapshot looking slightly different - due to the different data output from the statistics json - as well as the fact that is not possible to update the Locale.current - hence the component depending on it will show the decimal divider - in the current language. - */ - SnapshotTestHelper.assertSnapshot(initializingWith: { - WelcomeTour(delegate: MockWelcomeTourDelegate(), windowUUID: .snapshotTestDefaultUUID, startingStep: startingStep) - }, - wait: 1.0, - precision: 0.95, - testName: "testWelcomeScreen_step_\(step)") - } - } }