diff --git a/BrowserKit/Sources/Shared/Prefs.swift b/BrowserKit/Sources/Shared/Prefs.swift index 78b002957ea29..409c4608101dd 100644 --- a/BrowserKit/Sources/Shared/Prefs.swift +++ b/BrowserKit/Sources/Shared/Prefs.swift @@ -169,6 +169,7 @@ public struct PrefsKeys { public static let translationsFeature = "settings.translationFeature" public static let translationPreferredLanguages = "settings.translationPreferredLanguages" public static let translationAutoTranslate = "settings.translationAutoTranslate" + public static let translationAutoTranslatePromptShown = "settings.translationAutoTranslatePromptShown" public static let aiKillSwitchFeature = "settings.aiKillSwitchFeature" } diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 3595b4e95653b..917c7ef793fb7 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -459,10 +459,12 @@ 2FCAE2781ABB531100877008 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE25C1ABB531100877008 /* Visit.swift */; }; 2FCAE2841ABB533A00877008 /* MockFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2791ABB533A00877008 /* MockFiles.swift */; }; 2FDB10931A9FBEC5006CF312 /* PrefsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDB10921A9FBEC5006CF312 /* PrefsTests.swift */; }; + 31286F788EBE4BDE9BB9FF21 /* AutoTranslatePromptState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47E805FFBBD74D9BA77BD057 /* AutoTranslatePromptState.swift */; }; 318FB6EB1DB5600D0004E40F /* SQLiteHistoryFactories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318FB6EA1DB5600D0004E40F /* SQLiteHistoryFactories.swift */; }; 31ADB5DA1E58CEC300E87909 /* ClipboardBarDisplayHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ADB5D91E58CEC300E87909 /* ClipboardBarDisplayHandler.swift */; }; 354F8F612E098214007DFFEB /* SearchBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354F8F602E09820B007DFFEB /* SearchBarState.swift */; }; 354F8FAB2E0985B9007DFFEB /* SearchBarStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354F8FAA2E0985A0007DFFEB /* SearchBarStateTests.swift */; }; + 367C100A7ABD4B4585D029D5 /* AutoTranslatePromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137DDFADC6034016BE36E02B /* AutoTranslatePromptView.swift */; }; 39012F281F8ED262002E3D31 /* ScreenGraphTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39012F271F8ED262002E3D31 /* ScreenGraphTest.swift */; }; 391B4FFF1F9767F50094F841 /* FxScreenGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EB46981E26DDB4006346E8 /* FxScreenGraph.swift */; }; 39236E721FCC600200A38F1B /* TabEventHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39236E711FCC600200A38F1B /* TabEventHandlerTests.swift */; }; @@ -1301,6 +1303,7 @@ 8C19532E2B85E7AE00761B20 /* SelfSizingHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C19532D2B85E7AE00761B20 /* SelfSizingHostingController.swift */; }; 8C1953302B85E7EC00761B20 /* AutofillFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C19532F2B85E7EC00761B20 /* AutofillFooterView.swift */; }; 8C1953322B85EAB500761B20 /* AutofillHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C1953312B85EAB500761B20 /* AutofillHeaderView.swift */; }; + 8C196D512F7D2B74006EB316 /* AutoTranslatePromptStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C196D502F7D2B74006EB316 /* AutoTranslatePromptStateTests.swift */; }; 8C1C2E452C32D1470087846C /* RemoveAddressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C1C2E442C32D1470087846C /* RemoveAddressButton.swift */; }; 8C2447E82DFADF3D00BD2C91 /* ActivityEventHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2447E72DFADF3D00BD2C91 /* ActivityEventHelper.swift */; }; 8C2447EA2DFADF6300BD2C91 /* OnboardingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2447E92DFADF6300BD2C91 /* OnboardingServiceDelegate.swift */; }; @@ -2859,6 +2862,7 @@ 1323403C8071FC19BB79C191 /* su */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = su; path = su.lproj/InfoPlist.strings; sourceTree = ""; }; 13254478B55183D37507CAD4 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/AuthenticationManager.strings; sourceTree = ""; }; 13494C4690BBB3D709203A9F /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/Shared.strings; sourceTree = ""; }; + 137DDFADC6034016BE36E02B /* AutoTranslatePromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoTranslatePromptView.swift; sourceTree = ""; }; 138B419D8FCE3231B9DAF990 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/ClearPrivateDataConfirm.strings; sourceTree = ""; }; 13AF4D85B2FF8756DF86D969 /* ne-NP */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ne-NP"; path = "ne-NP.lproj/PrivateBrowsing.strings"; sourceTree = ""; }; 13D04C9EBFDCF1754B722152 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/ClearPrivateData.strings; sourceTree = ""; }; @@ -8429,6 +8433,7 @@ 479E4C288C50C0EB08B1B256 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Search.strings; sourceTree = ""; }; 47AB43C3BA340E57628A1A7F /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/ClearPrivateData.strings; sourceTree = ""; }; 47C747D6A0ECEFD4E5137C4C /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/ClearHistoryConfirm.strings"; sourceTree = ""; }; + 47E805FFBBD74D9BA77BD057 /* AutoTranslatePromptState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoTranslatePromptState.swift; sourceTree = ""; }; 47F340559D8B5DEB4306AE43 /* br */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = br; path = br.lproj/Search.strings; sourceTree = ""; }; 483744B5A1EBF3F05FC1AC1F /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Menu.strings; sourceTree = ""; }; 484E44CEA10A06DC7753D146 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/HistoryPanel.strings; sourceTree = ""; }; @@ -9484,6 +9489,7 @@ 8C19532D2B85E7AE00761B20 /* SelfSizingHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingHostingController.swift; sourceTree = ""; }; 8C19532F2B85E7EC00761B20 /* AutofillFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillFooterView.swift; sourceTree = ""; }; 8C1953312B85EAB500761B20 /* AutofillHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillHeaderView.swift; sourceTree = ""; }; + 8C196D502F7D2B74006EB316 /* AutoTranslatePromptStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoTranslatePromptStateTests.swift; sourceTree = ""; }; 8C1C2E442C32D1470087846C /* RemoveAddressButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveAddressButton.swift; sourceTree = ""; }; 8C2447E72DFADF3D00BD2C91 /* ActivityEventHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityEventHelper.swift; sourceTree = ""; }; 8C2447E92DFADF6300BD2C91 /* OnboardingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingServiceDelegate.swift; sourceTree = ""; }; @@ -14494,6 +14500,8 @@ C2886E022EC5DFDD007AE6C3 /* SchemeHandler */, C2886BA82EC4A41E007AE6C3 /* TinyRouter */, C27EF6182EBA363800BED719 /* LanguageDetector */, + 137DDFADC6034016BE36E02B /* AutoTranslatePromptView.swift */, + 47E805FFBBD74D9BA77BD057 /* AutoTranslatePromptState.swift */, AA02D5EA2EB3D82F00814F4C /* TranslationsAction.swift */, AA5E81462EB12BD800919E60 /* TranslationsMiddleware.swift */, AA5E81492EF5000000000001 /* PreferredTranslationLanguagesManager.swift */, @@ -14542,6 +14550,7 @@ C27EF6142EBA2EDD00BED719 /* LanguageDetectorTests.swift */, AAD9D64A2EB25B3200BFECAF /* TranslationsMiddlewareTests.swift */, 8C50DD742F56E1D00020E5C4 /* PreferredTranslationLanguagesManagerTests.swift */, + 8C196D502F7D2B74006EB316 /* AutoTranslatePromptStateTests.swift */, ); path = TranslationsTests; sourceTree = ""; @@ -19311,6 +19320,8 @@ D8AA923421A602DC002605C0 /* HomePageSettingViewController.swift in Sources */, AB9CBC042C53B64C00102610 /* TrackingProtectionModel.swift in Sources */, C8DC90C52A066B6A0008832B /* MarkupTokenizingUtility.swift in Sources */, + 367C100A7ABD4B4585D029D5 /* AutoTranslatePromptView.swift in Sources */, + 31286F788EBE4BDE9BB9FF21 /* AutoTranslatePromptState.swift in Sources */, AA02D5EB2EB3D83200814F4C /* TranslationsAction.swift in Sources */, C79CEE5E2EDF3C4B004C4EB8 /* JumpBackInCell.swift in Sources */, 0EBD66922DBFE41000A78532 /* ConcurrencyThrottler.swift in Sources */, @@ -19848,6 +19859,7 @@ C27484E42F55C4B600920313 /* MockSummarizerConfigFactory.swift in Sources */, E1EAA5F32D4A705500D3039C /* ToolbarMiddlewareTests.swift in Sources */, 8AA8389D2CA2FEFC003FA256 /* StoreTestUtility.swift in Sources */, + 8C196D512F7D2B74006EB316 /* AutoTranslatePromptStateTests.swift in Sources */, E1390FB628B040E900C9EF3E /* WallpaperSelectorViewModelTests.swift in Sources */, C27484E02F558C2800920313 /* MockSummarizerLanguageProvider.swift in Sources */, C2B31ECD2EDCEB1B00D53FF5 /* TranslationsVersionHelperTests.swift in Sources */, diff --git a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift index f31d8681bdb2b..628a74d536e15 100644 --- a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift +++ b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift @@ -316,6 +316,14 @@ struct AccessibilityIdentifiers { } } + struct Translations { + struct AutoTranslatePrompt { + static let messageLabel = "Translations.AutoTranslatePrompt.MessageLabel" + static let enableButton = "Translations.AutoTranslatePrompt.EnableButton" + static let closeButton = "Translations.AutoTranslatePrompt.CloseButton" + } + } + struct TermsOfUse { static let logo = "TermsOfUse.Logo" static let title = "TermsOfUse.Title" diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 62724bb900f87..9c09336758b9a 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -57,6 +57,7 @@ struct BrowserViewControllerState: ScreenState { var buttonTapped: UIButton? var frameContext: PasswordGeneratorFrameContext? var microsurveyState: MicrosurveyPromptState + var autoTranslatePromptState: AutoTranslatePromptState var navigationDestination: NavigationDestination? init(appState: AppState, uuid: WindowUUID) { @@ -81,6 +82,7 @@ struct BrowserViewControllerState: ScreenState { buttonTapped: bvcState.buttonTapped, frameContext: bvcState.frameContext, microsurveyState: bvcState.microsurveyState, + autoTranslatePromptState: bvcState.autoTranslatePromptState, navigationDestination: bvcState.navigationDestination) } @@ -95,6 +97,7 @@ struct BrowserViewControllerState: ScreenState { displayView: nil, buttonTapped: nil, microsurveyState: MicrosurveyPromptState(windowUUID: windowUUID), + autoTranslatePromptState: AutoTranslatePromptState(windowUUID: windowUUID), navigationDestination: nil) } @@ -111,6 +114,7 @@ struct BrowserViewControllerState: ScreenState { buttonTapped: UIButton? = nil, frameContext: PasswordGeneratorFrameContext? = nil, microsurveyState: MicrosurveyPromptState, + autoTranslatePromptState: AutoTranslatePromptState, navigationDestination: NavigationDestination? = nil ) { self.searchScreenState = searchScreenState @@ -125,6 +129,7 @@ struct BrowserViewControllerState: ScreenState { self.buttonTapped = buttonTapped self.frameContext = frameContext self.microsurveyState = microsurveyState + self.autoTranslatePromptState = autoTranslatePromptState self.navigationDestination = navigationDestination } @@ -156,6 +161,7 @@ struct BrowserViewControllerState: ScreenState { shouldStartAtHome: false, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action), navigationDestination: nil) } } @@ -186,6 +192,7 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action), navigationDestination: action.navigationDestination ) default: @@ -220,6 +227,7 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action), navigationDestination: NavigationDestination(.summarizer(config: action.summarizerConfig)) ) default: @@ -253,6 +261,7 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action), navigationDestination: NavigationDestination(.homepageZeroSearch) ) default: @@ -288,6 +297,7 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action), navigationDestination: NavigationDestination(.zeroSearch) ) } @@ -299,7 +309,8 @@ struct BrowserViewControllerState: ScreenState { searchScreenState: state.searchScreenState, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -373,7 +384,8 @@ struct BrowserViewControllerState: ScreenState { toast: toastType, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -385,7 +397,8 @@ struct BrowserViewControllerState: ScreenState { showOverlay: showOverlay, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -397,7 +410,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .home, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -409,7 +423,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .newTab, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -421,7 +436,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .backForwardList, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -435,7 +451,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .trackingProtectionDetails, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -448,7 +465,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .menu, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -460,7 +478,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .tabsLongPressActions, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -473,7 +492,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .reloadLongPressAction, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -486,7 +506,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .locationViewLongPressAction, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -498,7 +519,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .back, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -510,7 +532,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .forward, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -522,7 +545,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .tabTray, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -534,7 +558,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .reload, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -546,7 +571,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .reloadNoCache, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -558,7 +584,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .stopLoading, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -571,7 +598,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .share, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -583,7 +611,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .readerMode, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -595,7 +624,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .newTabLongPressActions, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -607,7 +637,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .readerModeLongPressAction, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -618,7 +649,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .dataClearance, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -630,7 +662,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .passwordGenerator, frameContext: action.frameContext, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -641,7 +674,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .summarizer(config: action.summarizerConfig), - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -660,7 +694,8 @@ struct BrowserViewControllerState: ScreenState { translatedToLanguage: action.translatedToLanguage )), buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -674,7 +709,8 @@ struct BrowserViewControllerState: ScreenState { searchScreenState: state.searchScreenState, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: microsurveyState + microsurveyState: microsurveyState, + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action) ) } @@ -684,7 +720,8 @@ struct BrowserViewControllerState: ScreenState { searchScreenState: state.searchScreenState, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: microsurveyState + microsurveyState: microsurveyState, + autoTranslatePromptState: AutoTranslatePromptState.defaultState(from: state.autoTranslatePromptState) ) } @@ -706,7 +743,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, reloadWebView: true, browserViewType: browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } @MainActor @@ -719,6 +757,7 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, shouldStartAtHome: action.shouldStartAtHome ?? false, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + autoTranslatePromptState: AutoTranslatePromptState.reducer(state.autoTranslatePromptState, action)) } } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 134a182c59215..d2b64a9450e1d 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -105,6 +105,7 @@ class BrowserViewController: UIViewController, var zoomPageBar: ZoomPageBar? var addressBarPanGestureHandler: AddressBarPanGestureHandler? var microsurvey: MicrosurveyPromptView? + var autoTranslatePrompt: AutoTranslatePromptView? // TODO: FXIOS-14347 Remove this property as part of cleaning up the toolbar performance var keyboardBackdrop: UIView? var pendingToast: Toast? // A toast that might be waiting for BVC to appear before displaying @@ -1005,6 +1006,9 @@ class BrowserViewController: UIViewController, executeNavigationAndDisplayActions() handleMicrosurvey(state: state) + if featureFlags.isFeatureEnabled(.translationLanguagePicker, checking: .buildOnly) { + handleAutoTranslatePrompt(state: state) + } if let readerMode = tabManager.selectedTab?.getContentScript(name: ReaderMode.name()) as? ReaderMode { if readerMode.state == .active && !contentContainer.hasHomepage { @@ -2141,6 +2145,42 @@ class BrowserViewController: UIViewController, } } + // MARK: - Auto-Translate Prompt + + private func handleAutoTranslatePrompt(state: BrowserViewControllerState) { + if state.autoTranslatePromptState.showPrompt { + showAutoTranslatePrompt() + } else { + removeAutoTranslatePrompt() + } + } + + private func showAutoTranslatePrompt() { + guard autoTranslatePrompt == nil else { return } + let prompt = AutoTranslatePromptView(windowUUID: windowUUID) + autoTranslatePrompt = prompt + + if isBottomSearchBar { + overKeyboardContainer.addArrangedViewToTop(prompt, animated: false, completion: nil) + } else { + bottomContainer.addArrangedViewToTop(prompt, animated: false, completion: nil) + } + + prompt.applyTheme(theme: themeManager.getCurrentTheme(for: windowUUID)) + } + + private func removeAutoTranslatePrompt() { + guard let prompt = autoTranslatePrompt else { return } + + if isBottomSearchBar { + overKeyboardContainer.removeArrangedView(prompt) + } else { + bottomContainer.removeArrangedView(prompt) + } + + autoTranslatePrompt = nil + } + // MARK: - Native Error Page func showEmbeddedNativeErrorPage() { diff --git a/firefox-ios/Client/Frontend/Settings/Translation/TranslationSettingsMiddleware.swift b/firefox-ios/Client/Frontend/Settings/Translation/TranslationSettingsMiddleware.swift index 4fd8ab6a3e590..06eda6c002b0f 100644 --- a/firefox-ios/Client/Frontend/Settings/Translation/TranslationSettingsMiddleware.swift +++ b/firefox-ios/Client/Frontend/Settings/Translation/TranslationSettingsMiddleware.swift @@ -118,6 +118,7 @@ final class TranslationSettingsMiddleware { let supported = await modelsFetcher.fetchSupportedTargetLanguages() let codes = manager.preferredLanguages(supportedTargetLanguages: supported) let isEnabled = prefs.boolForKey(PrefsKeys.Settings.translationsFeature) ?? true + let isAutoTranslateEnabled = prefs.boolForKey(PrefsKeys.Settings.translationAutoTranslate) ?? false let preferred = buildLanguageDetails(from: codes) let available = buildAvailableLanguages(preferred: codes, supported: supported) store.dispatch(TranslationSettingsMiddlewareAction( diff --git a/firefox-ios/Client/Frontend/Translations/AutoTranslatePromptState.swift b/firefox-ios/Client/Frontend/Translations/AutoTranslatePromptState.swift new file mode 100644 index 0000000000000..708c6d659257f --- /dev/null +++ b/firefox-ios/Client/Frontend/Translations/AutoTranslatePromptState.swift @@ -0,0 +1,41 @@ +// 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 Redux +import Common + +struct AutoTranslatePromptState: StateType, Equatable { + var windowUUID: WindowUUID + var showPrompt: Bool + + init(windowUUID: WindowUUID) { + self.init(windowUUID: windowUUID, showPrompt: false) + } + + init(windowUUID: WindowUUID, showPrompt: Bool) { + self.windowUUID = windowUUID + self.showPrompt = showPrompt + } + + static let reducer: Reducer = { state, action in + guard action.windowUUID == .unavailable || action.windowUUID == state.windowUUID else { + return defaultState(from: state) + } + + switch action.actionType { + case TranslationsActionType.showAutoTranslatePrompt: + return AutoTranslatePromptState(windowUUID: state.windowUUID, showPrompt: true) + case TranslationsActionType.didTapEnableAutoTranslate, + TranslationsActionType.didDismissAutoTranslatePrompt: + return AutoTranslatePromptState(windowUUID: state.windowUUID, showPrompt: false) + default: + return defaultState(from: state) + } + } + + static func defaultState(from state: AutoTranslatePromptState) -> AutoTranslatePromptState { + return AutoTranslatePromptState(windowUUID: state.windowUUID, showPrompt: state.showPrompt) + } +} diff --git a/firefox-ios/Client/Frontend/Translations/AutoTranslatePromptView.swift b/firefox-ios/Client/Frontend/Translations/AutoTranslatePromptView.swift new file mode 100644 index 0000000000000..6a5423d54afb3 --- /dev/null +++ b/firefox-ios/Client/Frontend/Translations/AutoTranslatePromptView.swift @@ -0,0 +1,115 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import Common +import ComponentLibrary +import Redux +import Shared +import UIKit + +final class AutoTranslatePromptView: UIView, ThemeApplicable { + private struct UX { + static let borderThickness: CGFloat = 1.0 + static let contentPadding = NSDirectionalEdgeInsets( + top: 14, + leading: 16, + bottom: -14, + trailing: -16 + ) + static let contentSpacing: CGFloat = 8 + } + + private let windowUUID: WindowUUID + + private var topBorderView: UIView = .build() + + private var messageLabel: UILabel = .build { label in + label.adjustsFontForContentSizeCategory = true + label.font = FXFontStyles.Regular.body.scaledFont() + label.numberOfLines = 0 + label.text = .Translations.AutoTranslatePrompt.Message + label.accessibilityIdentifier = AccessibilityIdentifiers.Translations.AutoTranslatePrompt.messageLabel + } + + private lazy var enableButton: UIButton = .build { button in + button.titleLabel?.adjustsFontForContentSizeCategory = true + button.titleLabel?.font = FXFontStyles.Regular.body.scaledFont() + button.setTitle(.Translations.AutoTranslatePrompt.EnableButton, for: .normal) + button.accessibilityLabel = .Translations.AutoTranslatePrompt.EnableButton + button.accessibilityIdentifier = AccessibilityIdentifiers.Translations.AutoTranslatePrompt.enableButton + button.addTarget(self, action: #selector(self.didTapEnable), for: .touchUpInside) + } + + private lazy var closeButton: CloseButton = .build { button in + let viewModel = CloseButtonViewModel( + a11yLabel: .Microsurvey.Prompt.CloseButtonAccessibilityLabel, + a11yIdentifier: AccessibilityIdentifiers.Translations.AutoTranslatePrompt.closeButton + ) + button.configure(viewModel: viewModel) + button.addTarget(self, action: #selector(self.didTapDismiss), for: .touchUpInside) + } + + private lazy var contentRow: UIStackView = .build { stack in + stack.axis = .horizontal + stack.alignment = .center + stack.spacing = UX.contentSpacing + } + + init(windowUUID: WindowUUID) { + self.windowUUID = windowUUID + super.init(frame: .zero) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + contentRow.addArrangedSubview(messageLabel) + contentRow.addArrangedSubview(enableButton) + contentRow.addArrangedSubview(closeButton) + + addSubview(topBorderView) + addSubview(contentRow) + + NSLayoutConstraint.activate([ + topBorderView.leadingAnchor.constraint(equalTo: leadingAnchor), + topBorderView.trailingAnchor.constraint(equalTo: trailingAnchor), + topBorderView.topAnchor.constraint(equalTo: topAnchor), + topBorderView.heightAnchor.constraint(equalToConstant: UX.borderThickness), + + contentRow.topAnchor.constraint(equalTo: topBorderView.bottomAnchor, constant: UX.contentPadding.top), + contentRow.bottomAnchor.constraint(equalTo: bottomAnchor, constant: UX.contentPadding.bottom), + contentRow.leadingAnchor.constraint(equalTo: leadingAnchor, constant: UX.contentPadding.leading), + contentRow.trailingAnchor.constraint(equalTo: trailingAnchor, constant: UX.contentPadding.trailing), + ]) + } + + @objc + private func didTapEnable() { + store.dispatch(TranslationsAction( + windowUUID: windowUUID, + actionType: TranslationsActionType.didTapEnableAutoTranslate + )) + } + + @objc + private func didTapDismiss() { + store.dispatch(TranslationsAction( + windowUUID: windowUUID, + actionType: TranslationsActionType.didDismissAutoTranslatePrompt + )) + } + + // MARK: - ThemeApplicable + + func applyTheme(theme: Theme) { + backgroundColor = theme.colors.layer1 + topBorderView.backgroundColor = theme.colors.borderPrimary + messageLabel.textColor = theme.colors.textPrimary + enableButton.setTitleColor(theme.colors.textAccent, for: .normal) + closeButton.tintColor = theme.colors.textSecondary + } +} diff --git a/firefox-ios/Client/Frontend/Translations/TranslationsAction.swift b/firefox-ios/Client/Frontend/Translations/TranslationsAction.swift index 580b8ef4f8f82..411b5d04415b7 100644 --- a/firefox-ios/Client/Frontend/Translations/TranslationsAction.swift +++ b/firefox-ios/Client/Frontend/Translations/TranslationsAction.swift @@ -39,4 +39,7 @@ struct TranslationLanguageSelectedAction: Action { enum TranslationsActionType: ActionType { case didTapRetryFailedTranslation case didSelectTargetLanguage + case showAutoTranslatePrompt + case didTapEnableAutoTranslate + case didDismissAutoTranslatePrompt } diff --git a/firefox-ios/Client/Frontend/Translations/TranslationsMiddleware.swift b/firefox-ios/Client/Frontend/Translations/TranslationsMiddleware.swift index ecc5c9089cb8f..7caf8a90f1dfd 100644 --- a/firefox-ios/Client/Frontend/Translations/TranslationsMiddleware.swift +++ b/firefox-ios/Client/Frontend/Translations/TranslationsMiddleware.swift @@ -64,6 +64,9 @@ final class TranslationsMiddleware: FeatureFlaggable { guard let action = (action as? TranslationLanguageSelectedAction) else { return } self.handleLanguageSelected(for: action, and: state) + case TranslationsActionType.didTapEnableAutoTranslate: + self.profile.prefs.setBool(true, forKey: PrefsKeys.Settings.translationAutoTranslate) + default: break } @@ -284,6 +287,7 @@ final class TranslationsMiddleware: FeatureFlaggable { translatedToLanguage: targetLanguage, and: action.windowUUID ) + maybeShowAutoTranslatePrompt(windowUUID: action.windowUUID) } catch { let serviceError = TranslationsServiceError.fromUnknown(error) translationsTelemetry.translationFailed( @@ -352,6 +356,17 @@ final class TranslationsMiddleware: FeatureFlaggable { store.dispatch(toastAction) } + private func maybeShowAutoTranslatePrompt(windowUUID: WindowUUID) { + let promptShown = profile.prefs.boolForKey(PrefsKeys.Settings.translationAutoTranslatePromptShown) ?? false + let autoTranslateEnabled = profile.prefs.boolForKey(PrefsKeys.Settings.translationAutoTranslate) ?? false + guard !promptShown && !autoTranslateEnabled else { return } + profile.prefs.setBool(true, forKey: PrefsKeys.Settings.translationAutoTranslatePromptShown) + store.dispatch(TranslationsAction( + windowUUID: windowUUID, + actionType: TranslationsActionType.showAutoTranslatePrompt + )) + } + /// Clears the flow ID for the given action's window. private func clearFlowId(for action: Action) { translationFlowIds[action.windowUUID] = nil diff --git a/firefox-ios/Shared/Strings.swift b/firefox-ios/Shared/Strings.swift index 64ce43a81b064..a6a13c887b12c 100644 --- a/firefox-ios/Shared/Strings.swift +++ b/firefox-ios/Shared/Strings.swift @@ -4265,6 +4265,19 @@ extension String { comment: "On the translation feature bottom sheet, this is the text that describes that an error has occurred.") } } + + public struct AutoTranslatePrompt { + public static let Message = MZLocalizedString( + key: "Translations.AutoTranslatePrompt.Message.v151", + tableName: "Translations", + value: "Automatically translate pages when available?", + comment: "Persistent prompt shown above the address bar after the user's first manual translation, asking if they want to enable auto-translate.") + public static let EnableButton = MZLocalizedString( + key: "Translations.AutoTranslatePrompt.EnableButton.v151", + tableName: "Translations", + value: "Enable", + comment: "Button label on the auto-translate prompt that enables the auto-translate feature when tapped.") + } } } diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TranslationsTests/AutoTranslatePromptStateTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TranslationsTests/AutoTranslatePromptStateTests.swift new file mode 100644 index 0000000000000..e34e919d8237c --- /dev/null +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TranslationsTests/AutoTranslatePromptStateTests.swift @@ -0,0 +1,95 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import Common +import Redux +import XCTest + +@testable import Client + +@MainActor +final class AutoTranslatePromptStateTests: XCTestCase { + override func setUp() async throws { + try await super.setUp() + DependencyHelperMock().bootstrapDependencies() + } + + override func tearDown() async throws { + DependencyHelperMock().reset() + try await super.tearDown() + } + + // MARK: - Reducer Tests + + func test_showAutoTranslatePrompt_setsShowPromptTrue() { + let initialState = createSubject() + let reducer = autoTranslatePromptReducer() + + XCTAssertFalse(initialState.showPrompt) + + let newState = reducer(initialState, getAction(for: .showAutoTranslatePrompt)) + + XCTAssertTrue(newState.showPrompt) + } + + func test_didTapEnableAutoTranslate_setsShowPromptFalse() { + let initialState = AutoTranslatePromptState(windowUUID: .XCTestDefaultUUID, showPrompt: true) + let reducer = autoTranslatePromptReducer() + + XCTAssertTrue(initialState.showPrompt) + + let newState = reducer(initialState, getAction(for: .didTapEnableAutoTranslate)) + + XCTAssertFalse(newState.showPrompt) + } + + func test_didDismissAutoTranslatePrompt_setsShowPromptFalse() { + let initialState = AutoTranslatePromptState(windowUUID: .XCTestDefaultUUID, showPrompt: true) + let reducer = autoTranslatePromptReducer() + + XCTAssertTrue(initialState.showPrompt) + + let newState = reducer(initialState, getAction(for: .didDismissAutoTranslatePrompt)) + + XCTAssertFalse(newState.showPrompt) + } + + func test_unknownAction_preservesState() { + let initialState = AutoTranslatePromptState(windowUUID: .XCTestDefaultUUID, showPrompt: true) + let reducer = autoTranslatePromptReducer() + + let action = TranslationsAction(windowUUID: .XCTestDefaultUUID, actionType: FakeActionType.testAction) + let newState = reducer(initialState, action) + + XCTAssertEqual(newState.showPrompt, true) + } + + func test_actionWithDifferentWindowUUID_preservesState() { + let initialState = AutoTranslatePromptState(windowUUID: .XCTestDefaultUUID, showPrompt: false) + let reducer = autoTranslatePromptReducer() + + let action = TranslationsAction(windowUUID: WindowUUID(), actionType: TranslationsActionType.showAutoTranslatePrompt) + let newState = reducer(initialState, action) + + XCTAssertFalse(newState.showPrompt) + } + + // MARK: - Private + + private func createSubject() -> AutoTranslatePromptState { + return AutoTranslatePromptState(windowUUID: .XCTestDefaultUUID) + } + + private func autoTranslatePromptReducer() -> Reducer { + return AutoTranslatePromptState.reducer + } + + private func getAction(for actionType: TranslationsActionType) -> TranslationsAction { + return TranslationsAction(windowUUID: .XCTestDefaultUUID, actionType: actionType) + } + + enum FakeActionType: ActionType { + case testAction + } +} diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TranslationsTests/TranslationsMiddlewareTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TranslationsTests/TranslationsMiddlewareTests.swift index ab241eafcd823..dda8f2bbb9b23 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TranslationsTests/TranslationsMiddlewareTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TranslationsTests/TranslationsMiddlewareTests.swift @@ -230,6 +230,7 @@ final class TranslationsMiddlewareIntegrationTests: XCTestCase, StoreTestUtility func test_didSelectTargetLanguage_dispatchAction() throws { setTranslationsFeatureEnabled(enabled: true) + mockProfile.prefs.setBool(true, forKey: PrefsKeys.Settings.translationAutoTranslatePromptShown) let subject = createSubject() let action = TranslationLanguageSelectedAction( @@ -543,6 +544,88 @@ final class TranslationsMiddlewareIntegrationTests: XCTestCase, StoreTestUtility XCTAssertEqual(dispatchedActionType, ToolbarActionType.receivedTranslationLanguage) } + // MARK: - maybeShowAutoTranslatePrompt tests + + func test_translationCompleted_whenPromptNotShownAndAutoTranslateOff_dispatchesShowAutoTranslatePrompt() throws { + setTranslationsFeatureEnabled(enabled: true) + let subject = createSubject() + + let action = TranslationLanguageSelectedAction( + windowUUID: .XCTestDefaultUUID, + targetLanguage: "de", + actionType: TranslationsActionType.didSelectTargetLanguage + ) + + let expectation = XCTestExpectation( + description: "expect didStartTranslatingPage, translationCompleted, showAutoTranslatePrompt to be fired" + ) + expectation.expectedFulfillmentCount = 3 + mockStore.dispatchCalled = { expectation.fulfill() } + subject.translationsProvider(mockStore.state, action) + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(mockStore.dispatchedActions.count, 3) + + let thirdAction = try XCTUnwrap(mockStore.dispatchedActions[2] as? TranslationsAction) + let thirdActionType = try XCTUnwrap(thirdAction.actionType as? TranslationsActionType) + + XCTAssertEqual(thirdActionType, TranslationsActionType.showAutoTranslatePrompt) + XCTAssertTrue(mockProfile.prefs.boolForKey(PrefsKeys.Settings.translationAutoTranslatePromptShown) ?? false) + } + + func test_translationCompleted_whenPromptAlreadyShown_doesNotDispatchShowPrompt() throws { + setTranslationsFeatureEnabled(enabled: true) + mockProfile.prefs.setBool(true, forKey: PrefsKeys.Settings.translationAutoTranslatePromptShown) + let subject = createSubject() + + let action = TranslationLanguageSelectedAction( + windowUUID: .XCTestDefaultUUID, + targetLanguage: "de", + actionType: TranslationsActionType.didSelectTargetLanguage + ) + + let expectation = XCTestExpectation( + description: "expect didStartTranslatingPage and translationCompleted to be fired" + ) + expectation.expectedFulfillmentCount = 2 + mockStore.dispatchCalled = { expectation.fulfill() } + subject.translationsProvider(mockStore.state, action) + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(mockStore.dispatchedActions.count, 2) + + let lastActionType = try XCTUnwrap(mockStore.dispatchedActions.last?.actionType as? ToolbarActionType) + XCTAssertEqual(lastActionType, ToolbarActionType.translationCompleted) + } + + func test_translationCompleted_whenAutoTranslateAlreadyEnabled_doesNotDispatchShowPrompt() throws { + setTranslationsFeatureEnabled(enabled: true) + mockProfile.prefs.setBool(true, forKey: PrefsKeys.Settings.translationAutoTranslate) + let subject = createSubject() + + let action = TranslationLanguageSelectedAction( + windowUUID: .XCTestDefaultUUID, + targetLanguage: "de", + actionType: TranslationsActionType.didSelectTargetLanguage + ) + + let expectation = XCTestExpectation( + description: "expect didStartTranslatingPage and translationCompleted to be fired" + ) + expectation.expectedFulfillmentCount = 2 + mockStore.dispatchCalled = { expectation.fulfill() } + subject.translationsProvider(mockStore.state, action) + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(mockStore.dispatchedActions.count, 2) + + let lastActionType = try XCTUnwrap(mockStore.dispatchedActions.last?.actionType as? ToolbarActionType) + XCTAssertEqual(lastActionType, ToolbarActionType.translationCompleted) + } + // MARK: - didTapRetryFailedTranslation tests func test_didTapRetryFailedTranslationAction_withoutFF_doesNotDispatchAction() throws { setTranslationsFeatureEnabled(enabled: false)