Bug 1845232 - Adds CFR for clear private browsing aciton

fenix/119.0
Jeff Boek 9 months ago committed by mergify[bot]
parent 5d75244515
commit d916e92967

@ -13,7 +13,7 @@ features:
defaults:
- channel: developer
value:
felt-privacy-enabled: true
felt-privacy-enabled: false
- channel: nightly
value:
felt-privacy-enabled: false

@ -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
}

@ -99,6 +99,7 @@ class DefaultToolbarIntegration(
browserStore = context.components.core.store,
settings = context.settings(),
toolbar = toolbar,
isPrivate = isPrivate,
sessionId = sessionId,
)

@ -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()
}

@ -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) {

@ -276,6 +276,8 @@
<string name="pref_key_should_show_home_onboarding_dialog" translatable="false">pref_key_should_show_home_onboarding_dialog</string>
<!-- A value of `true` means the total cookie protection popup has not been shown yet -->
<string name="pref_key_should_show_total_cookie_protection_popup" translatable="false">pref_key_should_show_total_cookie_protection_popup</string>
<!-- A value of `true` means the erase action popup has not been shown yet -->
<string name="pref_key_should_show_erase_action_popup" translatable="false">pref_key_should_show_erase_action_popup</string>
<string name="pref_key_debug_settings" translatable="false">pref_key_debug_settings</string>

@ -87,6 +87,11 @@
<!-- Text displayed that links to website containing documentation about the "Total cookie protection" feature. -->
<string name="tcp_cfr_learn_more">Learn about Total Cookie Protection</string>
<!-- Private browsing erase action "contextual feature recommendation" (CFR) -->
<!-- Text for the message displayed in the contextual feature recommendation popup promoting the erase private browsing feature. -->
<string name="erase_action_cfr_message">Tap here to start a fresh private session. Delete your history, cookies — everything.</string>
<!-- Text for the info dialog when camera permissions have been denied but user tries to access a camera feature. -->
<string name="camera_permissions_needed_message">Camera access needed. Go to Android settings, tap permissions, and tap allow.</string>
<!-- Text for the positive action button to go to Android Settings to grant permissions. -->

@ -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<Settings> {
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<View>(R.id.mozac_browser_toolbar_security_indicator) } returns anchor
every { findViewById<View>(R.id.mozac_browser_toolbar_page_actions) } returns anchor
every { findViewById<View>(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,
),
)

Loading…
Cancel
Save