You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingScreen.kt

292 lines
12 KiB
Kotlin

/* 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/. */
@file:OptIn(ExperimentalFoundationApi::class)
package org.mozilla.fenix.onboarding.view
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
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
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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 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.WidgetPinnedReceiver.WidgetPinnedState
import org.mozilla.fenix.theme.FirefoxTheme
/**
* A screen for displaying juno onboarding.
*
* @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.
* @param onSignInButtonClick Invoked when the positive button on the sign in page is clicked.
* @param onSkipSignInClick Invoked when the negative button on the sign in page is clicked.
* @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", "LongMethod")
fun JunoOnboardingScreen(
pagesToDisplay: List<OnboardingPageUiData>,
onMakeFirefoxDefaultClick: () -> Unit,
onSkipDefaultClick: () -> Unit,
onPrivacyPolicyClick: (url: String) -> Unit,
onSignInButtonClick: () -> Unit,
onSkipSignInClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onSkipNotificationClick: () -> Unit,
onAddFirefoxWidgetClick: () -> Unit,
onSkipFirefoxWidgetClick: () -> Unit,
onFinish: (pageType: OnboardingPageUiData) -> Unit,
onImpression: (pageType: OnboardingPageUiData) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState()
val isSignedIn: State<Boolean?> = components.backgroundServices.syncStore
.observeAsComposableState { it.account != null }
val widgetPinnedFlow: StateFlow<Boolean> = WidgetPinnedState.isPinned
val isWidgetPinnedState by widgetPinnedFlow.collectAsState()
BackHandler(enabled = pagerState.currentPage > 0) {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
}
val scrollToNextPageOrDismiss: () -> Unit = {
if (pagerState.currentPage == pagesToDisplay.lastIndex) {
onFinish(pagesToDisplay[pagerState.currentPage])
} else {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}
}
LaunchedEffect(isSignedIn.value) {
if (isSignedIn.value == true) {
scrollToNextPageOrDismiss()
}
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
onImpression(pagesToDisplay[page])
}
}
LaunchedEffect(isWidgetPinnedState) {
if (isWidgetPinnedState) {
scrollToNextPageOrDismiss()
}
}
JunoOnboardingContent(
pagesToDisplay = pagesToDisplay,
pagerState = pagerState,
onMakeFirefoxDefaultClick = {
scrollToNextPageOrDismiss()
onMakeFirefoxDefaultClick()
},
onMakeFirefoxDefaultSkipClick = {
scrollToNextPageOrDismiss()
onSkipDefaultClick()
},
onPrivacyPolicyClick = {
onPrivacyPolicyClick(it)
},
onSignInButtonClick = {
onSignInButtonClick()
scrollToNextPageOrDismiss()
},
onSignInSkipClick = {
scrollToNextPageOrDismiss()
onSkipSignInClick()
},
onNotificationPermissionButtonClick = {
scrollToNextPageOrDismiss()
onNotificationPermissionButtonClick()
},
onNotificationPermissionSkipClick = {
scrollToNextPageOrDismiss()
onSkipNotificationClick()
},
onAddFirefoxWidgetClick = {
if (isWidgetPinnedState) {
scrollToNextPageOrDismiss()
} else {
onAddFirefoxWidgetClick()
}
},
onSkipFirefoxWidgetClick = {
scrollToNextPageOrDismiss()
onSkipFirefoxWidgetClick()
},
)
}
@Composable
@Suppress("LongParameterList")
private fun JunoOnboardingContent(
pagesToDisplay: List<OnboardingPageUiData>,
pagerState: PagerState,
onMakeFirefoxDefaultClick: () -> Unit,
onMakeFirefoxDefaultSkipClick: () -> Unit,
onPrivacyPolicyClick: (String) -> Unit,
onSignInButtonClick: () -> Unit,
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onNotificationPermissionSkipClick: () -> Unit,
onAddFirefoxWidgetClick: () -> Unit,
onSkipFirefoxWidgetClick: () -> Unit,
) {
val nestedScrollConnection = remember { DisableForwardSwipeNestedScrollConnection(pagerState) }
Column(
modifier = Modifier
.background(FirefoxTheme.colors.layer1)
.statusBarsPadding()
.navigationBarsPadding(),
) {
HorizontalPager(
pageCount = pagesToDisplay.size,
state = pagerState,
key = { pagesToDisplay[it].type },
modifier = Modifier
.weight(1f)
.nestedScroll(nestedScrollConnection),
) { pageIndex ->
val pageUiState = pagesToDisplay[pageIndex]
val onboardingPageState = mapToOnboardingPageState(
onboardingPageUiData = pageUiState,
onMakeFirefoxDefaultClick = onMakeFirefoxDefaultClick,
onMakeFirefoxDefaultSkipClick = onMakeFirefoxDefaultSkipClick,
onPrivacyPolicyClick = onPrivacyPolicyClick,
onSignInButtonClick = onSignInButtonClick,
onSignInSkipClick = onSignInSkipClick,
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
onNotificationPermissionSkipClick = onNotificationPermissionSkipClick,
onAddFirefoxWidgetClick = onAddFirefoxWidgetClick,
onAddFirefoxWidgetSkipClick = onSkipFirefoxWidgetClick,
)
OnboardingPage(pageState = onboardingPageState)
}
PagerIndicator(
pagerState = pagerState,
pageCount = pagesToDisplay.size,
activeColor = FirefoxTheme.colors.actionPrimary,
inactiveColor = FirefoxTheme.colors.actionSecondary,
leaveTrail = true,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
)
}
}
private class DisableForwardSwipeNestedScrollConnection(
private val pagerState: PagerState,
) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
if (available.x > 0) {
// Allow going back on swipe
Offset.Zero
} else {
// For forward swipe, only allow if the visible item offset is less than 0,
// this would be a result of a slow back fling, and we should allow snapper to
// snap to the appropriate item.
// Else consume the whole offset and disable going forward.
if (pagerState.currentPageOffsetFraction < 0) {
Offset.Zero
} else {
Offset(available.x, 0f)
}
}
}
@LightDarkPreview
@Composable
private fun JunoOnboardingScreenPreview() {
FirefoxTheme {
JunoOnboardingContent(
pagesToDisplay = defaultPreviewPages(),
pagerState = PagerState(0),
onMakeFirefoxDefaultClick = {},
onMakeFirefoxDefaultSkipClick = {},
onPrivacyPolicyClick = {},
onSignInButtonClick = {},
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
onAddFirefoxWidgetClick = {},
onSkipFirefoxWidgetClick = {},
)
}
}
@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),
),
)