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.
292 lines
12 KiB
Kotlin
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),
|
|
),
|
|
)
|