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,
),
)