diff --git a/app/pbm.fml.yaml b/app/pbm.fml.yaml index 46055d61fa..7b22ed8a33 100644 --- a/app/pbm.fml.yaml +++ b/app/pbm.fml.yaml @@ -13,7 +13,7 @@ features: defaults: - channel: developer value: - felt-privacy-enabled: true + felt-privacy-enabled: false - channel: nightly value: felt-privacy-enabled: false diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt index 7d65977be2..5aa0e32df5 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.transformWhile import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab @@ -66,6 +67,7 @@ class BrowserToolbarCFRPresenter( private val browserStore: BrowserStore, private val settings: Settings, private val toolbar: BrowserToolbar, + private val isPrivate: Boolean, private val sessionId: String? = null, private val shoppingExperienceFeature: ShoppingExperienceFeature = DefaultShoppingExperienceFeature( context.settings(), @@ -111,6 +113,25 @@ class BrowserToolbarCFRPresenter( } } + ToolbarCFR.ERASE -> { + scope = browserStore.flowScoped { flow -> + flow + .mapNotNull { it.findCustomTabOrSelectedTab(sessionId) } + .filter { it.content.private } + .map { it.content.progress } + // The "transformWhile" below ensures that the 100% progress is only collected once. + .transformWhile { progress -> + emit(progress) + progress != 100 + } + .filter { it == 100 } + .collect { + scope?.cancel() + showEraseCfr() + } + } + } + ToolbarCFR.NONE -> { // no-op } @@ -118,6 +139,10 @@ class BrowserToolbarCFRPresenter( } private fun getCFRToShow(): ToolbarCFR = when { + settings.shouldShowEraseActionCFR && isPrivate -> { + ToolbarCFR.ERASE + } + settings.shouldShowTotalCookieProtectionCFR && ( !settings.shouldShowCookieBannerReEngagementDialog() || settings.openTabsCount >= CFR_MINIMUM_NUMBER_OPENED_TABS @@ -144,6 +169,48 @@ class BrowserToolbarCFRPresenter( scope?.cancel() } + @VisibleForTesting + internal fun showEraseCfr() { + CFRPopup( + anchor = toolbar.findViewById( + R.id.mozac_browser_toolbar_navigation_actions, + ), + properties = CFRPopupProperties( + popupAlignment = INDICATOR_CENTERED_IN_ANCHOR, + popupBodyColors = listOf( + getColor(context, R.color.fx_mobile_layer_color_gradient_end), + getColor(context, R.color.fx_mobile_layer_color_gradient_start), + ), + popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp, + dismissButtonColor = getColor(context, R.color.fx_mobile_icon_color_oncolor), + indicatorDirection = if (settings.toolbarPosition == ToolbarPosition.TOP) { + CFRPopup.IndicatorDirection.UP + } else { + CFRPopup.IndicatorDirection.DOWN + }, + ), + onDismiss = { + when (it) { + true -> TrackingProtection.tcpCfrExplicitDismissal.record(NoExtras()) + false -> TrackingProtection.tcpCfrImplicitDismissal.record(NoExtras()) + } + }, + text = { + FirefoxTheme { + Text( + text = context.getString(R.string.erase_action_cfr_message), + color = FirefoxTheme.colors.textOnColorPrimary, + style = FirefoxTheme.typography.body2, + ) + } + }, + ).run { + settings.shouldShowEraseActionCFR = false + popup = this + show() + } + } + @VisibleForTesting internal fun showTcpCfr() { CFRPopup( @@ -257,5 +324,5 @@ class BrowserToolbarCFRPresenter( * The CFR to be shown in the toolbar. */ private enum class ToolbarCFR { - TCP, SHOPPING, SHOPPING_OPTED_IN, NONE + TCP, SHOPPING, SHOPPING_OPTED_IN, ERASE, NONE } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt index 5f35eafdfb..d37346e9a4 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt @@ -99,6 +99,7 @@ class DefaultToolbarIntegration( browserStore = context.components.core.store, settings = context.settings(), toolbar = toolbar, + isPrivate = isPrivate, sessionId = sessionId, ) diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt index ce0f732866..6bd6358c34 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt @@ -29,7 +29,7 @@ interface BrowserToolbarInteractor { fun onHomeButtonClicked() /** - * Deletase all tabs and navigates to the Home screen. Called when a user taps on the erase button. + * Deletes all tabs and navigates to the Home screen. Called when a user taps on the erase button. */ fun onEraseButtonClicked() } diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 7a241f5ab9..14e0540269 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -779,6 +779,15 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = { enabledTotalCookieProtectionCFR }, ) + /** + * Indicates if the total cookie protection CRF should be shown. + */ + var shouldShowEraseActionCFR by lazyFeatureFlagPreference( + appContext.getPreferenceKey(R.string.pref_key_should_show_erase_action_popup), + featureFlag = true, + default = { feltPrivateBrowsingEnabled }, + ) + val blockCookiesSelectionInCustomTrackingProtection by stringPreference( key = appContext.getPreferenceKey(R.string.pref_key_tracking_protection_custom_cookies_select), default = if (enabledTotalCookieProtection) { diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 2d6b9c9300..215ca733ea 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -276,6 +276,8 @@ pref_key_should_show_home_onboarding_dialog pref_key_should_show_total_cookie_protection_popup + + pref_key_should_show_erase_action_popup pref_key_debug_settings diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c576c360f8..fcf943fe3b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,6 +87,11 @@ Learn about Total Cookie Protection + + + Tap here to start a fresh private session. Delete your history, cookies — everything. + + Camera access needed. Go to Android settings, tap permissions, and tap allow. diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt index 07c08208c9..4f49da5a67 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt @@ -136,12 +136,41 @@ class BrowserToolbarCFRPresenterTest { } @Test - fun `GIVEN the TCP CFR should not be shown WHEN the feature starts THEN don't observe the store for updates`() { + fun `GIVEN the Erase CFR should be shown WHEN in private mode and the current tab is fully loaded THEN the Erase CFR is only shown once`() { + val tab = createTab(url = "", private = true) + + val browserStore = createBrowserStore( + tab = tab, + selectedTabId = tab.id, + ) + + val presenter = createPresenterThatShowsCFRs( + browserStore = browserStore, + settings = mockk { + every { shouldShowEraseActionCFR } returns true + }, + isPrivate = true, + ) + + presenter.start() + + assertNotNull(presenter.scope) + + browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 99)).joinBlocking() + browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking() + browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking() + browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking() + verify { presenter.showEraseCfr() } + } + + @Test + fun `GIVEN no CFR shown WHEN the feature starts THEN don't observe the store for updates`() { val presenter = createPresenter( settings = mockk { every { shouldShowTotalCookieProtectionCFR } returns false every { shouldShowCookieBannerReEngagementDialog() } returns false every { shouldShowReviewQualityCheckCFR } returns false + every { shouldShowEraseActionCFR } returns false }, ) @@ -152,15 +181,15 @@ class BrowserToolbarCFRPresenterTest { @Test fun `GIVEN the store is observed for updates WHEN the presenter is stopped THEN stop observing the store`() { - val tcpScope: CoroutineScope = mockk { + val scope: CoroutineScope = mockk { every { cancel() } just Runs } val presenter = createPresenter() - presenter.scope = tcpScope + presenter.scope = scope presenter.stop() - verify { tcpScope.cancel() } + verify { scope.cancel() } } @Test @@ -203,6 +232,7 @@ class BrowserToolbarCFRPresenterTest { every { shouldShowTotalCookieProtectionCFR } returns false every { shouldShowReviewQualityCheckCFR } returns true every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis() + every { shouldShowEraseActionCFR } returns false }, ) every { presenter.showShoppingCFR(any()) } just Runs @@ -224,35 +254,6 @@ class BrowserToolbarCFRPresenterTest { verify { presenter.showShoppingCFR(eq(false)) } } - @Test - fun `GIVEN review quality CFR was previously displayed WHEN starting the presenter THEN don't observe the store`() { - val settings = mockk { - every { shouldShowReviewQualityCheckCFR } returns false - every { shouldShowTotalCookieProtectionCFR } returns false - } - val presenter = createPresenter(settings = settings) - - presenter.start() - - assertNull(presenter.scope) - } - - @Test - fun `GIVEN review quality feature is not enabled WHEN starting the presenter THEN don't observe the store`() { - val presenter = createPresenter( - settings = mockk { - every { shouldShowTotalCookieProtectionCFR } returns false - }, - shoppingExperienceFeature = mockk { - every { isEnabled } returns false - }, - ) - - presenter.start() - - assertNull(presenter.scope) - } - @Test fun `GIVEN the user opted in the shopping feature AND the opted in shopping CFR should be shown WHEN the tab is not loading THEN the CFR is shown`() { val tab = createTab(url = "") @@ -265,6 +266,7 @@ class BrowserToolbarCFRPresenterTest { settings = mockk { every { shouldShowTotalCookieProtectionCFR } returns false every { shouldShowReviewQualityCheckCFR } returns true + every { shouldShowEraseActionCFR } returns false every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis() - Settings.TWO_DAYS_MS }, browserStore = browserStore, @@ -299,12 +301,15 @@ class BrowserToolbarCFRPresenterTest { every { shouldShowTotalCookieProtectionCFR } returns true every { shouldShowCookieBannerReEngagementDialog() } returns false every { shouldShowReviewQualityCheckCFR } returns false + every { shouldShowEraseActionCFR } returns false }, toolbar: BrowserToolbar = mockk(), + isPrivate: Boolean = false, sessionId: String? = null, - ) = spyk(createPresenter(context, anchor, browserStore, settings, toolbar, sessionId)) { + ) = spyk(createPresenter(context, anchor, browserStore, settings, toolbar, sessionId, isPrivate)) { every { showTcpCfr() } just Runs every { showShoppingCFR(any()) } just Runs + every { showEraseCfr() } just Runs } /** @@ -322,13 +327,16 @@ class BrowserToolbarCFRPresenterTest { settings: Settings = mockk(relaxed = true) { every { shouldShowTotalCookieProtectionCFR } returns true every { shouldShowCookieBannerReEngagementDialog() } returns false + every { shouldShowEraseActionCFR } returns true every { shouldShowReviewQualityCheckCFR } returns true }, toolbar: BrowserToolbar = mockk { every { findViewById(R.id.mozac_browser_toolbar_security_indicator) } returns anchor every { findViewById(R.id.mozac_browser_toolbar_page_actions) } returns anchor + every { findViewById(R.id.mozac_browser_toolbar_navigation_actions) } returns anchor }, sessionId: String? = null, + isPrivate: Boolean = false, shoppingExperienceFeature: ShoppingExperienceFeature = mockk { every { isEnabled } returns true }, @@ -339,6 +347,7 @@ class BrowserToolbarCFRPresenterTest { settings = settings, toolbar = toolbar, sessionId = sessionId, + isPrivate = isPrivate, shoppingExperienceFeature = shoppingExperienceFeature, ), )