Bug 1826767 - Update app to use onboarding manifest cards and enum

fenix/114.1.0
t-p-white 1 year ago committed by mergify[bot]
parent 21a16fa586
commit 1c22b3cbee

@ -0,0 +1,87 @@
package org.mozilla.fenix.onboarding.view
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.mozilla.experiments.nimbus.StringHolder
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.nimbus.OnboardingCardData
import org.mozilla.fenix.nimbus.OnboardingCardType
class JunoOnboardingMapperTest {
@get:Rule
val activityTestRule =
HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Test
fun showNotificationTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData, notificationPageUiData)
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true))
}
@Test
fun showNotificationFalse_pagesToDisplay_returnsSortedListOfConvertedPagesWithoutNotificationPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData)
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false))
}
}
private val defaultBrowserPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome,
title = "default browser title",
description = "default browser body with link text",
linkText = "link text",
primaryButtonLabel = "default browser primary button text",
secondaryButtonLabel = "default browser secondary button text",
)
private val syncPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
title = "sync title",
description = "sync body",
primaryButtonLabel = "sync primary button text",
secondaryButtonLabel = "sync secondary button text",
)
private val notificationPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
imageRes = R.drawable.ic_notification_permission,
title = "notification title",
description = "notification body",
primaryButtonLabel = "notification primary button text",
secondaryButtonLabel = "notification secondary button text",
)
private val defaultBrowserCardData = OnboardingCardData(
cardType = OnboardingCardType.DEFAULT_BROWSER,
title = StringHolder(null, "default browser title"),
body = StringHolder(null, "default browser body with link text"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "default browser primary button text"),
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
ordering = 10,
)
private val syncCardData = OnboardingCardData(
cardType = OnboardingCardType.SYNC_SIGN_IN,
title = StringHolder(null, "sync title"),
body = StringHolder(null, "sync body"),
primaryButtonLabel = StringHolder(null, "sync primary button text"),
secondaryButtonLabel = StringHolder(null, "sync secondary button text"),
ordering = 20,
)
private val notificationCardData = OnboardingCardData(
cardType = OnboardingCardType.NOTIFICATION_PERMISSION,
title = StringHolder(null, "notification title"),
body = StringHolder(null, "notification body"),
primaryButtonLabel = StringHolder(null, "notification primary button text"),
secondaryButtonLabel = StringHolder(null, "notification secondary button text"),
ordering = 30,
)
private val unsortedAllKnownCardData = listOf(
syncCardData,
notificationCardData,
defaultBrowserCardData,
)

@ -26,9 +26,12 @@ import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.onboarding.view.JunoOnboardingPageType
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.view.JunoOnboardingScreen
import org.mozilla.fenix.onboarding.view.OnboardingPageUiData
import org.mozilla.fenix.onboarding.view.sequencePosition
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
@ -37,7 +40,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
*/
class JunoOnboardingFragment : Fragment() {
private val onboardingPageTypeList by lazy { onboardingPageTypeList(requireContext()) }
private val pagesToDisplay by lazy { pagesToDisplay(shouldShowNotificationPage(requireContext())) }
private val telemetryRecorder by lazy { JunoOnboardingTelemetryRecorder() }
@SuppressLint("SourceLockedOrientationActivity")
@ -76,21 +79,22 @@ class JunoOnboardingFragment : Fragment() {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
@Suppress("LongMethod")
private fun ScreenContent() {
val context = LocalContext.current
JunoOnboardingScreen(
onboardingPageTypeList = onboardingPageTypeList,
pagesToDisplay = pagesToDisplay,
onMakeFirefoxDefaultClick = {
activity?.openSetDefaultBrowserOption(useCustomTab = true)
telemetryRecorder.onSetToDefaultClick(
sequenceId = onboardingPageTypeList.telemetrySequenceId(),
pageType = JunoOnboardingPageType.DEFAULT_BROWSER,
sequenceId = pagesToDisplay.telemetrySequenceId(),
sequencePosition = pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.DEFAULT_BROWSER),
)
},
onSkipDefaultClick = {
telemetryRecorder.onSkipSetToDefaultClick(
onboardingPageTypeList.telemetrySequenceId(),
JunoOnboardingPageType.DEFAULT_BROWSER,
pagesToDisplay.telemetrySequenceId(),
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.DEFAULT_BROWSER),
)
},
onPrivacyPolicyClick = { url ->
@ -101,8 +105,8 @@ class JunoOnboardingFragment : Fragment() {
),
)
telemetryRecorder.onPrivacyPolicyClick(
onboardingPageTypeList.telemetrySequenceId(),
JunoOnboardingPageType.DEFAULT_BROWSER,
pagesToDisplay.telemetrySequenceId(),
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.DEFAULT_BROWSER),
)
},
onSignInButtonClick = {
@ -111,42 +115,48 @@ class JunoOnboardingFragment : Fragment() {
directions = JunoOnboardingFragmentDirections.actionGlobalTurnOnSync(),
)
telemetryRecorder.onSyncSignInClick(
sequenceId = onboardingPageTypeList.telemetrySequenceId(),
pageType = JunoOnboardingPageType.SYNC_SIGN_IN,
sequenceId = pagesToDisplay.telemetrySequenceId(),
sequencePosition = pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.SYNC_SIGN_IN),
)
},
onSkipSignInClick = {
telemetryRecorder.onSkipSignInClick(
onboardingPageTypeList.telemetrySequenceId(),
JunoOnboardingPageType.SYNC_SIGN_IN,
pagesToDisplay.telemetrySequenceId(),
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.SYNC_SIGN_IN),
)
},
onNotificationPermissionButtonClick = {
requireComponents.notificationsDelegate.requestNotificationPermission()
telemetryRecorder.onNotificationPermissionClick(
sequenceId = onboardingPageTypeList.telemetrySequenceId(),
pageType = JunoOnboardingPageType.NOTIFICATION_PERMISSION,
sequenceId = pagesToDisplay.telemetrySequenceId(),
sequencePosition =
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.NOTIFICATION_PERMISSION),
)
},
onSkipNotificationClick = {
telemetryRecorder.onSkipTurnOnNotificationsClick(
onboardingPageTypeList.telemetrySequenceId(),
JunoOnboardingPageType.NOTIFICATION_PERMISSION,
sequenceId = pagesToDisplay.telemetrySequenceId(),
sequencePosition =
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.NOTIFICATION_PERMISSION),
)
},
onFinish = { pageType ->
onFinish(onboardingPageTypeList.telemetrySequenceId(), pageType)
onFinish = {
onFinish(
sequenceId = pagesToDisplay.telemetrySequenceId(),
sequencePosition = pagesToDisplay.sequencePosition(it.type),
)
},
onImpression = { pageType ->
onImpression = {
telemetryRecorder.onImpression(
onboardingPageTypeList.telemetrySequenceId(),
pageType,
sequenceId = pagesToDisplay.telemetrySequenceId(),
pageType = it.type,
sequencePosition = pagesToDisplay.sequencePosition(it.type),
)
},
)
}
private fun onFinish(sequenceId: String, pageType: JunoOnboardingPageType) {
private fun onFinish(sequenceId: String, sequencePosition: String) {
requireComponents.fenixOnboarding.finish()
findNavController().nav(
id = R.id.junoOnboardingFragment,
@ -154,23 +164,16 @@ class JunoOnboardingFragment : Fragment() {
)
telemetryRecorder.onOnboardingComplete(
sequenceId = sequenceId,
pageType = pageType,
sequencePosition = sequencePosition,
)
}
private fun onboardingPageTypeList(context: Context): List<JunoOnboardingPageType> =
buildList {
add(JunoOnboardingPageType.DEFAULT_BROWSER)
add(JunoOnboardingPageType.SYNC_SIGN_IN)
if (shouldShowNotificationPage(context)) {
add(JunoOnboardingPageType.NOTIFICATION_PERMISSION)
}
}
private fun shouldShowNotificationPage(context: Context) =
!NotificationManagerCompat.from(context.applicationContext).areNotificationsEnabledSafe() &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
!NotificationManagerCompat.from(context.applicationContext)
.areNotificationsEnabledSafe() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
private fun isNotATablet() = !resources.getBoolean(R.bool.tablet)
private fun isNotATablet() =
!resources.getBoolean(R.bool.tablet)
private fun pagesToDisplay(showNotificationPage: Boolean): List<OnboardingPageUiData> =
FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData(showNotificationPage)
}

@ -5,7 +5,7 @@
package org.mozilla.fenix.onboarding
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.onboarding.view.JunoOnboardingPageType
import org.mozilla.fenix.onboarding.view.OnboardingPageUiData
/**
* Abstraction responsible for recording telemetry events for JunoOnboarding.
@ -15,51 +15,58 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records "onboarding_completed" telemetry event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page on which the completed event occurred.
* @param sequencePosition The sequence position of the page on which the completed event occurred.
*/
fun onOnboardingComplete(sequenceId: String, pageType: JunoOnboardingPageType) {
fun onOnboardingComplete(sequenceId: String, sequencePosition: String) {
Onboarding.completed.record(
Onboarding.CompletedExtra(
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
/**
* Records impression events for a given [JunoOnboardingPageType].
* Records impression events for a given [OnboardingPageUiData.Type].
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page for which the impression occurred.
* @param pageType The page type for which the impression occurred.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onImpression(sequenceId: String, pageType: JunoOnboardingPageType) {
fun onImpression(
sequenceId: String,
pageType: OnboardingPageUiData.Type,
sequencePosition: String,
) {
when (pageType) {
JunoOnboardingPageType.DEFAULT_BROWSER -> {
OnboardingPageUiData.Type.DEFAULT_BROWSER -> {
Onboarding.setToDefaultCard.record(
Onboarding.SetToDefaultCardExtra(
action = ACTION_IMPRESSION,
elementType = ET_ONBOARDING_CARD,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
JunoOnboardingPageType.SYNC_SIGN_IN -> {
OnboardingPageUiData.Type.SYNC_SIGN_IN -> {
Onboarding.signInCard.record(
Onboarding.SignInCardExtra(
action = ACTION_IMPRESSION,
elementType = ET_ONBOARDING_CARD,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
JunoOnboardingPageType.NOTIFICATION_PERMISSION -> {
OnboardingPageUiData.Type.NOTIFICATION_PERMISSION -> {
Onboarding.turnOnNotificationsCard.record(
Onboarding.TurnOnNotificationsCardExtra(
action = ACTION_IMPRESSION,
elementType = ET_ONBOARDING_CARD,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
@ -69,15 +76,15 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records set to default click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page for which the impression occurred.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onSetToDefaultClick(sequenceId: String, pageType: JunoOnboardingPageType) {
fun onSetToDefaultClick(sequenceId: String, sequencePosition: String) {
Onboarding.setToDefault.record(
Onboarding.SetToDefaultExtra(
action = ACTION_CLICK,
elementType = ET_PRIMARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
@ -85,15 +92,15 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records sync sign in click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page for which the impression occurred.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onSyncSignInClick(sequenceId: String, pageType: JunoOnboardingPageType) {
fun onSyncSignInClick(sequenceId: String, sequencePosition: String) {
Onboarding.signIn.record(
Onboarding.SignInExtra(
action = ACTION_CLICK,
elementType = ET_PRIMARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
@ -101,15 +108,15 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records notification permission click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page for which the impression occurred.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onNotificationPermissionClick(sequenceId: String, pageType: JunoOnboardingPageType) {
fun onNotificationPermissionClick(sequenceId: String, sequencePosition: String) {
Onboarding.turnOnNotifications.record(
Onboarding.TurnOnNotificationsExtra(
action = ACTION_CLICK,
elementType = ET_PRIMARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
@ -117,18 +124,15 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records skip set to default click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page for which the impression occurred.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onSkipSetToDefaultClick(
sequenceId: String,
pageType: JunoOnboardingPageType,
) {
fun onSkipSetToDefaultClick(sequenceId: String, sequencePosition: String) {
Onboarding.skipDefault.record(
Onboarding.SkipDefaultExtra(
action = ACTION_CLICK,
elementType = ET_SECONDARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
@ -136,18 +140,15 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records skip sign in click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page for which the impression occurred.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onSkipSignInClick(
sequenceId: String,
pageType: JunoOnboardingPageType,
) {
fun onSkipSignInClick(sequenceId: String, sequencePosition: String) {
Onboarding.skipSignIn.record(
Onboarding.SkipSignInExtra(
action = ACTION_CLICK,
elementType = ET_SECONDARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
@ -155,18 +156,15 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records skip notification permission click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page for which the impression occurred.
* @param sequencePosition The sequence position of the page for which the impression occurred.
*/
fun onSkipTurnOnNotificationsClick(
sequenceId: String,
pageType: JunoOnboardingPageType,
) {
fun onSkipTurnOnNotificationsClick(sequenceId: String, sequencePosition: String) {
Onboarding.skipTurnOnNotifications.record(
Onboarding.SkipTurnOnNotificationsExtra(
action = ACTION_CLICK,
elementType = ET_SECONDARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}
@ -174,15 +172,15 @@ class JunoOnboardingTelemetryRecorder {
/**
* Records privacy policy link text click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
* @param pageType The page on which the link click event occurred.
* @param sequencePosition The sequence position of the page on which the link click event occurred.
*/
fun onPrivacyPolicyClick(sequenceId: String, pageType: JunoOnboardingPageType) {
fun onPrivacyPolicyClick(sequenceId: String, sequencePosition: String) {
Onboarding.privacyPolicy.record(
Onboarding.PrivacyPolicyExtra(
action = ACTION_CLICK,
elementType = ET_SECONDARY_BUTTON,
sequenceId = sequenceId,
sequencePosition = pageType.sequencePosition,
sequencePosition = sequencePosition,
),
)
}

@ -4,21 +4,63 @@
package org.mozilla.fenix.onboarding.view
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import org.mozilla.fenix.R
import org.mozilla.fenix.nimbus.OnboardingCardData
import org.mozilla.fenix.nimbus.OnboardingCardType
import org.mozilla.fenix.settings.SupportUtils
/**
* Mapper to convert [JunoOnboardingPageType] to [OnboardingPageState] that is a param for
* Returns a list of all the required Nimbus 'cards' that have been converted to [OnboardingPageUiData].
*/
internal fun Collection<OnboardingCardData>.toPageUiData(showNotificationPage: Boolean): List<OnboardingPageUiData> =
filter {
if (it.cardType == OnboardingCardType.NOTIFICATION_PERMISSION) {
showNotificationPage
} else {
true
}
}.sortedBy { it.ordering }
.map { it.toPageUiData() }
private fun OnboardingCardData.toPageUiData(): OnboardingPageUiData {
return when (cardType) {
OnboardingCardType.DEFAULT_BROWSER -> OnboardingPageUiData(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome,
title = title,
description = body,
linkText = linkText,
primaryButtonLabel = primaryButtonLabel,
secondaryButtonLabel = secondaryButtonLabel,
)
OnboardingCardType.SYNC_SIGN_IN -> OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
title = title,
description = body,
primaryButtonLabel = primaryButtonLabel,
secondaryButtonLabel = secondaryButtonLabel,
)
OnboardingCardType.NOTIFICATION_PERMISSION -> OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
imageRes = R.drawable.ic_notification_permission,
title = title,
description = body,
primaryButtonLabel = primaryButtonLabel,
secondaryButtonLabel = secondaryButtonLabel,
)
}
}
/**
* Mapper to convert [OnboardingPageUiData] to [OnboardingPageState] that is a param for
* [OnboardingPage] composable.
*/
@ReadOnlyComposable
@Composable
@Suppress("LongParameterList")
internal fun mapToOnboardingPageState(
onboardingPageType: JunoOnboardingPageType,
onboardingPageUiData: OnboardingPageUiData,
onMakeFirefoxDefaultClick: () -> Unit,
onMakeFirefoxDefaultSkipClick: () -> Unit,
onPrivacyPolicyClick: (String) -> Unit,
@ -26,99 +68,43 @@ internal fun mapToOnboardingPageState(
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onNotificationPermissionSkipClick: () -> Unit,
): OnboardingPageState = when (onboardingPageType) {
JunoOnboardingPageType.DEFAULT_BROWSER -> defaultBrowserPageState(
): OnboardingPageState = when (onboardingPageUiData.type) {
OnboardingPageUiData.Type.DEFAULT_BROWSER -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onMakeFirefoxDefaultClick,
onNegativeButtonClick = onMakeFirefoxDefaultSkipClick,
onUrlClick = onPrivacyPolicyClick,
)
JunoOnboardingPageType.SYNC_SIGN_IN -> syncSignInPageState(
OnboardingPageUiData.Type.SYNC_SIGN_IN -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onSignInButtonClick,
onNegativeButtonClick = onSignInSkipClick,
)
JunoOnboardingPageType.NOTIFICATION_PERMISSION -> notificationPermissionPageState(
OnboardingPageUiData.Type.NOTIFICATION_PERMISSION -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onNotificationPermissionButtonClick,
onNegativeButtonClick = onNotificationPermissionSkipClick,
)
}
@Composable
@ReadOnlyComposable
private fun notificationPermissionPageState(
onPositiveButtonClick: () -> Unit,
onNegativeButtonClick: () -> Unit,
) = OnboardingPageState(
image = R.drawable.ic_notification_permission,
title = stringResource(
id = R.string.juno_onboarding_enable_notifications_title,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
description = stringResource(
id = R.string.juno_onboarding_enable_notifications_description,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
primaryButton = Action(
text = stringResource(id = R.string.juno_onboarding_enable_notifications_positive_button),
onClick = onPositiveButtonClick,
),
secondaryButton = Action(
text = stringResource(id = R.string.juno_onboarding_enable_notifications_negative_button),
onClick = onNegativeButtonClick,
),
onRecordImpressionEvent = {},
)
@Composable
@ReadOnlyComposable
private fun syncSignInPageState(
onPositiveButtonClick: () -> Unit,
onNegativeButtonClick: () -> Unit,
) = OnboardingPageState(
image = R.drawable.ic_onboarding_sync,
title = stringResource(id = R.string.juno_onboarding_sign_in_title),
description = stringResource(id = R.string.juno_onboarding_sign_in_description),
primaryButton = Action(
text = stringResource(id = R.string.juno_onboarding_sign_in_positive_button),
onClick = onPositiveButtonClick,
),
secondaryButton = Action(
text = stringResource(id = R.string.juno_onboarding_sign_in_negative_button),
onClick = onNegativeButtonClick,
),
onRecordImpressionEvent = {},
)
@Composable
@ReadOnlyComposable
private fun defaultBrowserPageState(
private fun createOnboardingPageState(
onboardingPageUiData: OnboardingPageUiData,
onPositiveButtonClick: () -> Unit,
onNegativeButtonClick: () -> Unit,
onUrlClick: (String) -> Unit,
) = OnboardingPageState(
image = R.drawable.ic_onboarding_welcome,
title = stringResource(
id = R.string.juno_onboarding_default_browser_title,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
description = stringResource(
id = R.string.juno_onboarding_default_browser_description,
formatArgs = arrayOf(
stringResource(R.string.firefox),
stringResource(R.string.juno_onboarding_default_browser_description_link_text),
),
),
linkTextState = LinkTextState(
text = stringResource(id = R.string.juno_onboarding_default_browser_description_link_text),
url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
onClick = onUrlClick,
),
primaryButton = Action(
text = stringResource(id = R.string.juno_onboarding_default_browser_positive_button),
onClick = onPositiveButtonClick,
),
secondaryButton = Action(
text = stringResource(id = R.string.juno_onboarding_default_browser_negative_button),
onClick = onNegativeButtonClick,
),
onRecordImpressionEvent = {},
onUrlClick: (String) -> Unit = {},
): OnboardingPageState = OnboardingPageState(
image = onboardingPageUiData.imageRes,
title = onboardingPageUiData.title,
description = onboardingPageUiData.description,
linkTextState = onboardingPageUiData.linkText?.let {
LinkTextState(
text = it,
url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
onClick = onUrlClick,
)
},
primaryButton = Action(onboardingPageUiData.primaryButtonLabel, onPositiveButtonClick),
secondaryButton = Action(onboardingPageUiData.secondaryButtonLabel, onNegativeButtonClick),
)

@ -1,36 +0,0 @@
/* 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.view
/**
* Model for different types of Onboarding Pages.
* @param sequencePosition Position of the page in the onboarding flow, used in telemetry.
* @param id Identifier for the page, used in telemetry.
*/
enum class JunoOnboardingPageType(
val sequencePosition: String,
val id: String,
) {
DEFAULT_BROWSER(
sequencePosition = "1",
id = "default",
),
SYNC_SIGN_IN(
sequencePosition = "2",
id = "sync",
),
NOTIFICATION_PERMISSION(
sequencePosition = "3",
id = "notification",
),
}
/**
* Helper function for telemetry that maps List<JunoOnboardingPageType> to a string of page names
* separated by an underscore.
* e.g. [DEFAULT_BROWSER, SYNC_SIGN_IN] will be mapped to "default_sync".
*/
fun List<JunoOnboardingPageType>.telemetrySequenceId(): String =
joinToString(separator = "_") { it.id }

@ -22,12 +22,14 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
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 com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState
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
@ -36,7 +38,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
/**
* A screen for displaying juno onboarding.
*
* @param onboardingPageTypeList List of pages to be displayed in onboarding pager ui.
* @param pagesToDisplay List of pages to be displayed in onboarding pager ui.
* @param onMakeFirefoxDefaultClick Invoked when positive button on default browser page is clicked.
* @param onSkipDefaultClick Invoked when negative button on default browser page is clicked.
* @param onPrivacyPolicyClick Invoked when the privacy policy link text is clicked.
@ -51,7 +53,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
@Composable
@Suppress("LongParameterList")
fun JunoOnboardingScreen(
onboardingPageTypeList: List<JunoOnboardingPageType>,
pagesToDisplay: List<OnboardingPageUiData>,
onMakeFirefoxDefaultClick: () -> Unit,
onSkipDefaultClick: () -> Unit,
onPrivacyPolicyClick: (url: String) -> Unit,
@ -59,8 +61,8 @@ fun JunoOnboardingScreen(
onSkipSignInClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onSkipNotificationClick: () -> Unit,
onFinish: (pageType: JunoOnboardingPageType) -> Unit,
onImpression: (pageType: JunoOnboardingPageType) -> Unit,
onFinish: (pageType: OnboardingPageUiData) -> Unit,
onImpression: (pageType: OnboardingPageUiData) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState()
@ -75,7 +77,7 @@ fun JunoOnboardingScreen(
val scrollToNextPageOrDismiss: () -> Unit = {
if (pagerState.currentPage == pagerState.pageCount - 1) {
onFinish(onboardingPageTypeList[pagerState.currentPage])
onFinish(pagesToDisplay[pagerState.currentPage])
} else {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
@ -91,12 +93,12 @@ fun JunoOnboardingScreen(
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
onImpression(onboardingPageTypeList[page])
onImpression(pagesToDisplay[page])
}
}
JunoOnboardingContent(
onboardingPageTypeList = onboardingPageTypeList,
pagesToDisplay = pagesToDisplay,
pagerState = pagerState,
onMakeFirefoxDefaultClick = {
scrollToNextPageOrDismiss()
@ -131,7 +133,7 @@ fun JunoOnboardingScreen(
@Composable
@Suppress("LongParameterList")
private fun JunoOnboardingContent(
onboardingPageTypeList: List<JunoOnboardingPageType>,
pagesToDisplay: List<OnboardingPageUiData>,
pagerState: PagerState,
onMakeFirefoxDefaultClick: () -> Unit,
onMakeFirefoxDefaultSkipClick: () -> Unit,
@ -150,16 +152,16 @@ private fun JunoOnboardingContent(
.navigationBarsPadding(),
) {
HorizontalPager(
count = onboardingPageTypeList.size,
count = pagesToDisplay.size,
state = pagerState,
key = { onboardingPageTypeList[it] },
key = { pagesToDisplay[it].type },
modifier = Modifier
.weight(1f)
.nestedScroll(nestedScrollConnection),
) { pageIndex ->
val onboardingPageType = onboardingPageTypeList[pageIndex]
val pageState = mapToOnboardingPageState(
onboardingPageType = onboardingPageType,
val pageUiState = pagesToDisplay[pageIndex]
val onboardingPageState = mapToOnboardingPageState(
onboardingPageUiData = pageUiState,
onMakeFirefoxDefaultClick = onMakeFirefoxDefaultClick,
onMakeFirefoxDefaultSkipClick = onMakeFirefoxDefaultSkipClick,
onPrivacyPolicyClick = onPrivacyPolicyClick,
@ -168,7 +170,7 @@ private fun JunoOnboardingContent(
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
onNotificationPermissionSkipClick = onNotificationPermissionSkipClick,
)
OnboardingPage(pageState = pageState)
OnboardingPage(pageState = onboardingPageState)
}
PagerIndicator(
@ -187,8 +189,8 @@ private class DisableForwardSwipeNestedScrollConnection(
private val pagerState: PagerState,
) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
return if (available.x > 0) {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
if (available.x > 0) {
// Allow going back on swipe
Offset.Zero
} else {
@ -197,12 +199,11 @@ private class DisableForwardSwipeNestedScrollConnection(
// snap to the appropriate item.
// Else consume the whole offset and disable going forward.
if (pagerState.currentPageOffset < 0) {
return Offset.Zero
Offset.Zero
} else {
Offset(available.x, 0f)
}
}
}
}
@LightDarkPreview
@ -210,11 +211,7 @@ private class DisableForwardSwipeNestedScrollConnection(
private fun JunoOnboardingScreenPreview() {
FirefoxTheme {
JunoOnboardingContent(
onboardingPageTypeList = listOf(
JunoOnboardingPageType.DEFAULT_BROWSER,
JunoOnboardingPageType.SYNC_SIGN_IN,
JunoOnboardingPageType.NOTIFICATION_PERMISSION,
),
pagesToDisplay = defaultPreviewPages(),
pagerState = PagerState(0),
onMakeFirefoxDefaultClick = {},
onMakeFirefoxDefaultSkipClick = {},
@ -226,3 +223,32 @@ private fun JunoOnboardingScreenPreview() {
)
}
}
@Composable
private fun defaultPreviewPages() = listOf(
OnboardingPageUiData(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome,
title = stringResource(R.string.juno_onboarding_default_browser_title_nimbus),
description = stringResource(R.string.juno_onboarding_default_browser_description_nimbus),
linkText = stringResource(R.string.juno_onboarding_default_browser_description_link_text),
primaryButtonLabel = stringResource(R.string.juno_onboarding_default_browser_positive_button),
secondaryButtonLabel = stringResource(R.string.juno_onboarding_default_browser_negative_button),
),
OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
title = stringResource(R.string.juno_onboarding_sign_in_title),
description = stringResource(R.string.juno_onboarding_sign_in_description),
primaryButtonLabel = stringResource(R.string.juno_onboarding_sign_in_positive_button),
secondaryButtonLabel = stringResource(R.string.juno_onboarding_sign_in_negative_button),
),
OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
imageRes = R.drawable.ic_notification_permission,
title = stringResource(R.string.juno_onboarding_enable_notifications_title_nimbus),
description = stringResource(R.string.juno_onboarding_enable_notifications_description_nimbus),
primaryButtonLabel = stringResource(R.string.juno_onboarding_enable_notifications_positive_button),
secondaryButtonLabel = stringResource(R.string.juno_onboarding_enable_notifications_negative_button),
),
)

@ -24,7 +24,7 @@ data class OnboardingPageState(
val linkTextState: LinkTextState? = null,
val primaryButton: Action,
val secondaryButton: Action? = null,
val onRecordImpressionEvent: () -> Unit,
val onRecordImpressionEvent: () -> Unit = {},
)
/**

@ -0,0 +1,53 @@
/* 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.view
import androidx.annotation.DrawableRes
import org.mozilla.fenix.nimbus.OnboardingCardData
/**
* Model containing the required data from a raw [OnboardingCardData] object in a UI state.
*/
data class OnboardingPageUiData(
val type: Type,
@DrawableRes val imageRes: Int,
val title: String,
val description: String,
val linkText: String? = null,
val primaryButtonLabel: String,
val secondaryButtonLabel: String,
) {
/**
* Model for different types of Onboarding Pages.
* @param telemetryId Identifier for the page, used in telemetry.
*/
enum class Type(
val telemetryId: String,
) {
DEFAULT_BROWSER(
telemetryId = "default",
),
SYNC_SIGN_IN(
telemetryId = "sync",
),
NOTIFICATION_PERMISSION(
telemetryId = "notification",
),
}
}
/**
* Returns the sequence position for the given [OnboardingPageUiData.Type].
*/
fun List<OnboardingPageUiData>.sequencePosition(type: OnboardingPageUiData.Type): String =
indexOfFirst { it.type == type }.inc().toString()
/**
* Helper function for telemetry that maps List<OnboardingPageUiData> to a string of page names
* separated by an underscore.
* e.g. [DEFAULT_BROWSER, SYNC_SIGN_IN] will be mapped to "default_sync".
*/
fun List<OnboardingPageUiData>.telemetrySequenceId(): String =
joinToString(separator = "_") { it.type.telemetryId }

@ -0,0 +1,126 @@
/* 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.view
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.SupportUtils
class JunoOnboardingMapperTest {
@Test
fun `GIVEN a default browser page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() {
val expected = OnboardingPageState(
image = R.drawable.ic_onboarding_welcome,
title = "default browser title",
description = "default browser body with link text",
linkTextState = LinkTextState(
text = "link text",
url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
onClick = stringLambda,
),
primaryButton = Action("default browser primary button text", unitLambda),
secondaryButton = Action("default browser secondary button text", unitLambda),
)
val onboardingPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome,
title = "default browser title",
description = "default browser body with link text",
linkText = "link text",
primaryButtonLabel = "default browser primary button text",
secondaryButtonLabel = "default browser secondary button text",
)
val actual = mapToOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onMakeFirefoxDefaultClick = unitLambda,
onMakeFirefoxDefaultSkipClick = unitLambda,
onPrivacyPolicyClick = stringLambda,
onSignInButtonClick = {},
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
)
assertEquals(expected, actual)
}
@Test
fun `GIVEN a sync page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() {
val expected = OnboardingPageState(
image = R.drawable.ic_onboarding_sync,
title = "sync title",
description = "sync body",
primaryButton = Action("sync primary button text", unitLambda),
secondaryButton = Action("sync secondary button text", unitLambda),
)
val onboardingPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
title = "sync title",
description = "sync body",
linkText = null,
primaryButtonLabel = "sync primary button text",
secondaryButtonLabel = "sync secondary button text",
)
val actual = mapToOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onMakeFirefoxDefaultClick = {},
onMakeFirefoxDefaultSkipClick = {},
onPrivacyPolicyClick = {},
onSignInButtonClick = unitLambda,
onSignInSkipClick = unitLambda,
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
)
assertEquals(expected, actual)
}
@Test
fun `GIVEN a notification page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() {
val expected = OnboardingPageState(
image = R.drawable.ic_notification_permission,
title = "notification title",
description = "notification body",
primaryButton = Action("notification primary button text", unitLambda),
secondaryButton = Action("notification secondary button text", unitLambda),
)
val onboardingPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
imageRes = R.drawable.ic_notification_permission,
title = "notification title",
description = "notification body",
linkText = null,
primaryButtonLabel = "notification primary button text",
secondaryButtonLabel = "notification secondary button text",
)
val actual = mapToOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onMakeFirefoxDefaultClick = {},
onMakeFirefoxDefaultSkipClick = {},
onPrivacyPolicyClick = {},
onSignInButtonClick = {},
onSignInSkipClick = {},
onNotificationPermissionButtonClick = unitLambda,
onNotificationPermissionSkipClick = unitLambda,
)
assertEquals(expected, actual)
}
}
private val unitLambda = { dummyUnitFunc() }
private val stringLambda = { s: String -> dummyStringArgFunc(s) }
private fun dummyUnitFunc() {}
private fun dummyStringArgFunc(string: String) {
print(string)
}

@ -1,44 +0,0 @@
package org.mozilla.fenix.onboarding.view
import org.junit.Assert.assertEquals
import org.junit.Test
class JunoOnboardingPageTypeTest {
@Test
fun `GIVEN a list of JunoOnboardingPageType sequenceId() should map to the correct sequence id - 1`() {
val list = listOf(
JunoOnboardingPageType.DEFAULT_BROWSER,
JunoOnboardingPageType.SYNC_SIGN_IN,
JunoOnboardingPageType.NOTIFICATION_PERMISSION,
)
val expected = "default_sync_notification"
val actual = list.telemetrySequenceId()
assertEquals(expected, actual)
}
@Test
fun `GIVEN a list of JunoOnboardingPageType sequenceId() should map to the correct sequence id - 2`() {
val list = listOf(
JunoOnboardingPageType.DEFAULT_BROWSER,
JunoOnboardingPageType.SYNC_SIGN_IN,
)
val expected = "default_sync"
val actual = list.telemetrySequenceId()
assertEquals(expected, actual)
}
@Test
fun `GIVEN a list of JunoOnboardingPageType sequenceId() should map to the correct sequence id - 3`() {
val list = listOf(JunoOnboardingPageType.DEFAULT_BROWSER)
val expected = "default"
val actual = list.telemetrySequenceId()
assertEquals(expected, actual)
}
}

@ -0,0 +1,86 @@
/* 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.view
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.R
class OnboardingPageUiDataTest {
@Test
fun `GIVEN first page in the list WHEN sequencePosition called THEN returns the index plus 1`() {
val expected = "1"
val actual = allKnownPages.sequencePosition(defaultBrowserPageUiData.type)
assertEquals(expected, actual)
}
@Test
fun `GIVEN last page in the list WHEN sequencePosition called THEN returns the index plus 1`() {
val expected = "3"
val actual = allKnownPages.sequencePosition(notificationPageUiData.type)
assertEquals(expected, actual)
}
@Test
fun `GIVEN all known pages of WHEN sequenceId() called THEN should map to the correct sequence id`() {
val expected = "default_sync_notification"
val actual = allKnownPages.telemetrySequenceId()
assertEquals(expected, actual)
}
@Test
fun `GIVEN some of the known pages WHEN sequenceId() called THEN should map to the correct sequence id`() {
val expected = "default_sync"
val actual = listOf(defaultBrowserPageUiData, syncPageUiData).telemetrySequenceId()
assertEquals(expected, actual)
}
@Test
fun `GIVEN a single known page WHEN sequenceId() called THEN should map to the correct sequence id`() {
val expected = "default"
val actual = listOf(defaultBrowserPageUiData).telemetrySequenceId()
assertEquals(expected, actual)
}
}
private val defaultBrowserPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome,
title = "default browser title",
description = "default browser body with link text",
linkText = "link text",
primaryButtonLabel = "default browser primary button text",
secondaryButtonLabel = "default browser secondary button text",
)
private val syncPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
title = "sync title",
description = "sync body",
primaryButtonLabel = "sync primary button text",
secondaryButtonLabel = "sync secondary button text",
)
private val notificationPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
imageRes = R.drawable.ic_notification_permission,
title = "notification title",
description = "notification body",
primaryButtonLabel = "notification primary button text",
secondaryButtonLabel = "notification secondary button text",
)
private val allKnownPages = listOf(
defaultBrowserPageUiData,
syncPageUiData,
notificationPageUiData,
)
Loading…
Cancel
Save