Bug 1845747 - Add "Add search widget" card for Juno Onboarding

Added a new card for the Juno Onboarding, "Add search widget
to homescreen". This is an experiment that aims to increase
DAU and searches.

(cherry picked from commit cba23f261cb6b81b60f994022ae41967e7915ade)
fenix/118.0
DreVla 9 months ago committed by Mergify
parent 8801616dc3
commit fb8aabefa1

@ -1318,6 +1318,105 @@ onboarding:
metadata:
tags:
- Onboarding
add_search_widget_card:
type: event
description: |
User viewed juno onboarding add search widget card.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1848960
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/3310
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
add_search_widget:
type: event
description: |
User tapped on Add Firefox Widget in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1848960
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/3310
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
skip_add_search_widget:
type: event
description: |
User tapped on skip add search widget button in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1848960
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/3310
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
privacy_policy:
type: event
description: |

@ -23,6 +23,15 @@ features:
primary-button-label: juno_onboarding_default_browser_positive_button
secondary-button-label: juno_onboarding_default_browser_negative_button
add-search-widget:
card-type: add-search-widget
title: juno_onboarding_add_search_widget_title
body: juno_onboarding_add_search_widget_description
image-res: ic_onboarding_search_widget
ordering: 15
primary-button-label: juno_onboarding_add_search_widget_positive_button
secondary-button-label: juno_onboarding_add_search_widget_negative_button
sync-sign-in:
card-type: sync-sign-in
title: juno_onboarding_sign_in_title
@ -108,3 +117,5 @@ enums:
description: Allows user to sync with a Firefox account.
notification-permission:
description: Allows user to enable notification permission.
add-search-widget:
description: Allows user to add search widget to homescreen.

@ -20,15 +20,27 @@ class JunoOnboardingMapperTest {
HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Test
fun showNotificationTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages() {
fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData, notificationPageUiData)
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true))
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, false))
}
@Test
fun showNotificationFalse_pagesToDisplay_returnsSortedListOfConvertedPagesWithoutNotificationPage() {
fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData)
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false))
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, false))
}
@Test
fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() {
val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData)
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, true))
}
@Test
fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() {
val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData, notificationPageUiData)
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, true))
}
}
@ -41,6 +53,15 @@ private val defaultBrowserPageUiData = OnboardingPageUiData(
primaryButtonLabel = "default browser primary button text",
secondaryButtonLabel = "default browser secondary button text",
)
private val addSearchWidgetPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget,
title = "add search widget title",
description = "add search widget body with link text",
linkText = "link text",
primaryButtonLabel = "add search widget primary button text",
secondaryButtonLabel = "add search widget secondary button text",
)
private val syncPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
@ -68,6 +89,16 @@ private val defaultBrowserCardData = OnboardingCardData(
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
ordering = 10,
)
private val addSearchWidgetCardData = OnboardingCardData(
cardType = OnboardingCardType.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget,
title = StringHolder(null, "add search widget title"),
body = StringHolder(null, "add search widget body with link text"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
ordering = 15,
)
private val syncCardData = OnboardingCardData(
cardType = OnboardingCardType.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
@ -91,4 +122,5 @@ private val unsortedAllKnownCardData = listOf(
syncCardData,
notificationCardData,
defaultBrowserCardData,
addSearchWidgetCardData,
)

@ -338,6 +338,13 @@
android:resource="@xml/search_widget_info" />
</receiver>
<receiver android:name=".onboarding.WidgetPinnedReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.mozilla.fenix.onboarding.WidgetPinnedReceiver.widgetPinned"/>
</intent-filter>
</receiver>
<service android:name=".session.PrivateNotificationService"
android:exported="false" />

@ -5,7 +5,10 @@
package org.mozilla.fenix.onboarding
import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.os.Build
import android.os.Bundle
@ -18,6 +21,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.fragment.findNavController
import mozilla.components.support.base.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.R
@ -34,14 +38,21 @@ import org.mozilla.fenix.onboarding.view.telemetrySequenceId
import org.mozilla.fenix.onboarding.view.toPageUiData
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.gecko.search.SearchWidgetProvider
/**
* Fragment displaying the juno onboarding flow.
*/
class JunoOnboardingFragment : Fragment() {
private val pagesToDisplay by lazy { pagesToDisplay(shouldShowNotificationPage(requireContext())) }
private val pagesToDisplay by lazy {
pagesToDisplay(
canShowNotificationPage(requireContext()),
canShowAddWidgetCard(),
)
}
private val telemetryRecorder by lazy { JunoOnboardingTelemetryRecorder() }
private val pinAppWidgetReceiver = WidgetPinnedReceiver()
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
@ -49,6 +60,9 @@ class JunoOnboardingFragment : Fragment() {
if (isNotATablet()) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
val filter = IntentFilter(WidgetPinnedReceiver.ACTION)
LocalBroadcastManager.getInstance(requireContext())
.registerReceiver(pinAppWidgetReceiver, filter)
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@ -74,6 +88,7 @@ class JunoOnboardingFragment : Fragment() {
if (isNotATablet()) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(pinAppWidgetReceiver)
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@ -141,6 +156,15 @@ class JunoOnboardingFragment : Fragment() {
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.NOTIFICATION_PERMISSION),
)
},
onAddFirefoxWidgetClick = {
showAddSearchWidgetDialog()
},
onSkipFirefoxWidgetClick = {
telemetryRecorder.onSkipAddWidgetClick(
pagesToDisplay.telemetrySequenceId(),
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.ADD_SEARCH_WIDGET),
)
},
onFinish = {
onFinish(
sequenceId = pagesToDisplay.telemetrySequenceId(),
@ -157,6 +181,19 @@ class JunoOnboardingFragment : Fragment() {
)
}
private fun showAddSearchWidgetDialog() {
// Requesting to pin app widget is only available for Android 8.0 and above
if (canShowAddWidgetCard()) {
val appWidgetManager = AppWidgetManager.getInstance(activity)
val searchWidgetProvider =
ComponentName(requireActivity(), SearchWidgetProvider::class.java)
if (appWidgetManager.isRequestPinAppWidgetSupported) {
val successCallback = WidgetPinnedReceiver.getPendingIntent(requireContext())
appWidgetManager.requestPinAppWidget(searchWidgetProvider, null, successCallback)
}
}
}
private fun onFinish(sequenceId: String, sequencePosition: String) {
requireComponents.fenixOnboarding.finish()
findNavController().nav(
@ -169,12 +206,20 @@ class JunoOnboardingFragment : Fragment() {
)
}
private fun shouldShowNotificationPage(context: Context) =
private fun canShowNotificationPage(context: Context) =
!NotificationManagerCompat.from(context.applicationContext)
.areNotificationsEnabledSafe() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
private fun canShowAddWidgetCard() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
private fun isNotATablet() = !resources.getBoolean(R.bool.tablet)
private fun pagesToDisplay(showNotificationPage: Boolean): List<OnboardingPageUiData> =
FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData(showNotificationPage)
private fun pagesToDisplay(
showNotificationPage: Boolean,
showAddWidgetPage: Boolean,
): List<OnboardingPageUiData> =
FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData(
showNotificationPage,
showAddWidgetPage,
)
}

@ -49,6 +49,17 @@ class JunoOnboardingTelemetryRecorder {
)
}
OnboardingPageUiData.Type.ADD_SEARCH_WIDGET -> {
Onboarding.addSearchWidgetCard.record(
Onboarding.AddSearchWidgetCardExtra(
action = ACTION_IMPRESSION,
elementType = ET_ONBOARDING_CARD,
sequenceId = sequenceId,
sequencePosition = sequencePosition,
),
)
}
OnboardingPageUiData.Type.SYNC_SIGN_IN -> {
Onboarding.signInCard.record(
Onboarding.SignInCardExtra(
@ -121,6 +132,22 @@ class JunoOnboardingTelemetryRecorder {
)
}
/**
* Records add search widget click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onAddSearchWidgetClick(sequenceId: String, sequencePosition: String) {
Onboarding.addSearchWidget.record(
Onboarding.AddSearchWidgetExtra(
action = ACTION_CLICK,
elementType = ET_PRIMARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = sequencePosition,
),
)
}
/**
* Records skip set to default click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
@ -153,6 +180,22 @@ class JunoOnboardingTelemetryRecorder {
)
}
/**
* Records skip add widget click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onSkipAddWidgetClick(sequenceId: String, sequencePosition: String) {
Onboarding.skipAddSearchWidget.record(
Onboarding.SkipAddSearchWidgetExtra(
action = ACTION_CLICK,
elementType = ET_SECONDARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = sequencePosition,
),
)
}
/**
* Records skip notification permission click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.

@ -0,0 +1,83 @@
/* 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/. */
package org.mozilla.fenix.onboarding
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import mozilla.components.support.utils.PendingIntentUtils
import org.mozilla.fenix.onboarding.view.JunoOnboardingScreen
/**
* Receiver required to catch callback from Launcher when prompted
* to add search widget from the Juno Onboarding.
*/
class WidgetPinnedReceiver : BroadcastReceiver() {
companion object {
const val ACTION = "org.mozilla.fenix.onboarding.WidgetPinnedReceiver.PIN_SEARCH_WIDGET_SUCCESS"
/**
* Prepare success callback for when requesting to pin Search Widget.
*/
fun getPendingIntent(context: Context): PendingIntent {
val callbackIntent = Intent(context, WidgetPinnedReceiver::class.java)
val bundle = Bundle()
bundle.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, 1)
callbackIntent.putExtras(bundle)
return PendingIntent.getBroadcast(
context,
0,
callbackIntent,
PendingIntentUtils.defaultFlags or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
}
/**
* Object containing boolean that updates behavior of Add Search Widget
* card from [JunoOnboardingScreen].
* - True if widget added successfully and app resumed from launcher add widget dialog.
* - False if dialog opened but widget was not added.
*/
object WidgetPinnedState {
private val _isPinned = MutableStateFlow(false)
val isPinned: StateFlow<Boolean> = _isPinned
/**
* Update state when resumed to add search widget card
* and the widget was added successfully.
*/
fun widgetPinned() {
_isPinned.value = true
}
}
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null || intent == null) {
return
} else if (intent.action == ACTION) {
// Returned to fragment, go to next page and update button behavior.
WidgetPinnedState.widgetPinned()
}
val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (widgetId == -1) {
// No widget id received.
return
} else {
// Callback from system, widget pinned successfully, update compose now.
val updateIntent = Intent(ACTION)
LocalBroadcastManager.getInstance(context).sendBroadcast(updateIntent)
}
}
}

@ -12,12 +12,21 @@ import org.mozilla.fenix.settings.SupportUtils
/**
* Returns a list of all the required Nimbus 'cards' that have been converted to [OnboardingPageUiData].
*/
internal fun Collection<OnboardingCardData>.toPageUiData(showNotificationPage: Boolean): List<OnboardingPageUiData> =
internal fun Collection<OnboardingCardData>.toPageUiData(
showNotificationPage: Boolean,
canShowAddWidgetPage: Boolean,
): List<OnboardingPageUiData> =
filter {
if (it.cardType == OnboardingCardType.NOTIFICATION_PERMISSION) {
showNotificationPage
} else {
true
when (it.cardType) {
OnboardingCardType.NOTIFICATION_PERMISSION -> {
showNotificationPage
}
OnboardingCardType.ADD_SEARCH_WIDGET -> {
canShowAddWidgetPage
}
else -> {
true
}
}
}.sortedBy { it.ordering }
.map { it.toPageUiData() }
@ -36,6 +45,7 @@ private fun OnboardingCardType.toPageUiDataType() = when (this) {
OnboardingCardType.DEFAULT_BROWSER -> OnboardingPageUiData.Type.DEFAULT_BROWSER
OnboardingCardType.SYNC_SIGN_IN -> OnboardingPageUiData.Type.SYNC_SIGN_IN
OnboardingCardType.NOTIFICATION_PERMISSION -> OnboardingPageUiData.Type.NOTIFICATION_PERMISSION
OnboardingCardType.ADD_SEARCH_WIDGET -> OnboardingPageUiData.Type.ADD_SEARCH_WIDGET
}
/**
@ -52,6 +62,8 @@ internal fun mapToOnboardingPageState(
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onNotificationPermissionSkipClick: () -> Unit,
onAddFirefoxWidgetClick: () -> Unit,
onAddFirefoxWidgetSkipClick: () -> Unit,
): OnboardingPageState = when (onboardingPageUiData.type) {
OnboardingPageUiData.Type.DEFAULT_BROWSER -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
@ -60,6 +72,13 @@ internal fun mapToOnboardingPageState(
onUrlClick = onPrivacyPolicyClick,
)
OnboardingPageUiData.Type.ADD_SEARCH_WIDGET -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onAddFirefoxWidgetClick,
onNegativeButtonClick = onAddFirefoxWidgetSkipClick,
onUrlClick = onPrivacyPolicyClick,
)
OnboardingPageUiData.Type.SYNC_SIGN_IN -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onSignInButtonClick,

@ -19,6 +19,8 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
@ -30,12 +32,15 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.PagerIndicator
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.onboarding.JunoOnboardingTelemetryRecorder
import org.mozilla.fenix.onboarding.WidgetPinnedReceiver.WidgetPinnedState
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -50,11 +55,13 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onNotificationPermissionButtonClick Invoked when positive button on notification page is
* clicked.
* @param onSkipNotificationClick Invoked when negative button on notification page is clicked.
* @param onAddFirefoxWidgetClick Invoked when positive button on add search widget page is clicked.
* @param onSkipFirefoxWidgetClick Invoked when negative button on add search widget page is clicked.
* @param onFinish Invoked when the onboarding is completed.
* @param onImpression Invoked when a page in the pager is displayed.
*/
@Composable
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
fun JunoOnboardingScreen(
pagesToDisplay: List<OnboardingPageUiData>,
onMakeFirefoxDefaultClick: () -> Unit,
@ -64,6 +71,8 @@ fun JunoOnboardingScreen(
onSkipSignInClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onSkipNotificationClick: () -> Unit,
onAddFirefoxWidgetClick: () -> Unit,
onSkipFirefoxWidgetClick: () -> Unit,
onFinish: (pageType: OnboardingPageUiData) -> Unit,
onImpression: (pageType: OnboardingPageUiData) -> Unit,
) {
@ -71,6 +80,9 @@ fun JunoOnboardingScreen(
val pagerState = rememberPagerState()
val isSignedIn: State<Boolean?> = components.backgroundServices.syncStore
.observeAsComposableState { it.account != null }
val telemetryRecorder by lazy { JunoOnboardingTelemetryRecorder() }
val widgetPinnedFlow: StateFlow<Boolean> = WidgetPinnedState.isPinned
val isWidgetPinnedState by widgetPinnedFlow.collectAsState()
BackHandler(enabled = pagerState.currentPage > 0) {
coroutineScope.launch {
@ -100,6 +112,16 @@ fun JunoOnboardingScreen(
}
}
LaunchedEffect(isWidgetPinnedState) {
if (isWidgetPinnedState) {
scrollToNextPageOrDismiss()
telemetryRecorder.onAddSearchWidgetClick(
pagesToDisplay.telemetrySequenceId(),
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.ADD_SEARCH_WIDGET),
)
}
}
JunoOnboardingContent(
pagesToDisplay = pagesToDisplay,
pagerState = pagerState,
@ -130,6 +152,17 @@ fun JunoOnboardingScreen(
scrollToNextPageOrDismiss()
onSkipNotificationClick()
},
onAddFirefoxWidgetClick = {
if (isWidgetPinnedState) {
scrollToNextPageOrDismiss()
} else {
onAddFirefoxWidgetClick()
}
},
onSkipFirefoxWidgetClick = {
scrollToNextPageOrDismiss()
onSkipFirefoxWidgetClick()
},
)
}
@ -145,6 +178,8 @@ private fun JunoOnboardingContent(
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onNotificationPermissionSkipClick: () -> Unit,
onAddFirefoxWidgetClick: () -> Unit,
onSkipFirefoxWidgetClick: () -> Unit,
) {
val nestedScrollConnection = remember { DisableForwardSwipeNestedScrollConnection(pagerState) }
@ -172,6 +207,8 @@ private fun JunoOnboardingContent(
onSignInSkipClick = onSignInSkipClick,
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
onNotificationPermissionSkipClick = onNotificationPermissionSkipClick,
onAddFirefoxWidgetClick = onAddFirefoxWidgetClick,
onAddFirefoxWidgetSkipClick = onSkipFirefoxWidgetClick,
)
OnboardingPage(pageState = onboardingPageState)
}
@ -224,6 +261,8 @@ private fun JunoOnboardingScreenPreview() {
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
onAddFirefoxWidgetClick = {},
onSkipFirefoxWidgetClick = {},
)
}
}

@ -32,6 +32,9 @@ data class OnboardingPageUiData(
SYNC_SIGN_IN(
telemetryId = "sync",
),
ADD_SEARCH_WIDGET(
telemetryId = "search_widget",
),
NOTIFICATION_PERMISSION(
telemetryId = "notification",
),

File diff suppressed because one or more lines are too long

@ -330,6 +330,17 @@
<string name="juno_onboarding_enable_notifications_positive_button" tools:ignore="UnusedResources">Turn on notifications</string>
<!-- Text for the button dismiss the screen and move on with the flow -->
<string name="juno_onboarding_enable_notifications_negative_button" tools:ignore="UnusedResources">Not now</string>
<!-- Title for add search widget screen.
Note: The word "Firefox" should NOT be translated -->
<string name="juno_onboarding_add_search_widget_title" tools:ignore="UnusedResources">Try the Firefox search widget</string>
<!-- Description for add search widget screen.
Note: The word "Firefox" should NOT be translated -->
<string name="juno_onboarding_add_search_widget_description" tools:ignore="UnusedResources">With Firefox on your home screen, youll have easy access to the privacy-first browser that blocks cross-site trackers.</string>
<!-- Text for the button to add search widget on the device
Note: The word "Firefox" should NOT be translated -->
<string name="juno_onboarding_add_search_widget_positive_button" tools:ignore="UnusedResources">Add Firefox widget</string>
<!-- Text for the button to dismiss the screen and move on with the flow -->
<string name="juno_onboarding_add_search_widget_negative_button" tools:ignore="UnusedResources">Not now</string>
<!-- Search Widget -->
<!-- Content description for searching with a widget. The first parameter is the name of the application.-->

@ -45,6 +45,8 @@ class JunoOnboardingMapperTest {
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
onAddFirefoxWidgetClick = {},
onAddFirefoxWidgetSkipClick = {},
)
assertEquals(expected, actual)
@ -78,6 +80,8 @@ class JunoOnboardingMapperTest {
onSignInSkipClick = unitLambda,
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
onAddFirefoxWidgetClick = {},
onAddFirefoxWidgetSkipClick = {},
)
assertEquals(expected, actual)
@ -111,6 +115,48 @@ class JunoOnboardingMapperTest {
onSignInSkipClick = {},
onNotificationPermissionButtonClick = unitLambda,
onNotificationPermissionSkipClick = unitLambda,
onAddFirefoxWidgetClick = {},
onAddFirefoxWidgetSkipClick = {},
)
assertEquals(expected, actual)
}
@Test
fun `GIVEN an add search widget page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() {
val expected = OnboardingPageState(
imageRes = R.drawable.ic_onboarding_search_widget,
title = "add search widget title",
description = "add search widget body with link text",
linkTextState = LinkTextState(
text = "link text",
url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
onClick = stringLambda,
),
primaryButton = Action("add search widget primary button text", unitLambda),
secondaryButton = Action("add search widget secondary button text", unitLambda),
)
val onboardingPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget,
title = "add search widget title",
description = "add search widget body with link text",
linkText = "link text",
primaryButtonLabel = "add search widget primary button text",
secondaryButtonLabel = "add search widget secondary button text",
)
val actual = mapToOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onMakeFirefoxDefaultClick = {},
onMakeFirefoxDefaultSkipClick = {},
onPrivacyPolicyClick = stringLambda,
onSignInButtonClick = {},
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
onAddFirefoxWidgetClick = unitLambda,
onAddFirefoxWidgetSkipClick = unitLambda,
)
assertEquals(expected, actual)

Loading…
Cancel
Save