Bug 1821720 - Add juno onboarding pager ui
parent
d9ca5a8e0c
commit
128a59755a
@ -0,0 +1,131 @@
|
||||
/* 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.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
|
||||
/**
|
||||
* Mapper to convert [JunoOnboardingPageType] to [OnboardingPageState] that is a param for
|
||||
* [OnboardingPage] composable.
|
||||
*/
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun mapToOnboardingPageState(
|
||||
onboardingPageType: JunoOnboardingPageType,
|
||||
scrollToNextPageOrDismiss: () -> Unit,
|
||||
onMakeFirefoxDefaultClick: () -> Unit,
|
||||
onPrivacyPolicyClick: (String) -> Unit,
|
||||
onSignInButtonClick: () -> Unit,
|
||||
onNotificationPermissionButtonClick: () -> Unit,
|
||||
): OnboardingPageState = when (onboardingPageType) {
|
||||
JunoOnboardingPageType.DEFAULT_BROWSER -> defaultBrowserPageState(
|
||||
onPositiveButtonClick = {
|
||||
onMakeFirefoxDefaultClick()
|
||||
scrollToNextPageOrDismiss()
|
||||
},
|
||||
onNegativeButtonClick = scrollToNextPageOrDismiss,
|
||||
onUrlClick = onPrivacyPolicyClick,
|
||||
)
|
||||
JunoOnboardingPageType.SYNC_SIGN_IN -> syncSignInPageState(
|
||||
onPositiveButtonClick = {
|
||||
onSignInButtonClick()
|
||||
scrollToNextPageOrDismiss()
|
||||
},
|
||||
onNegativeButtonClick = scrollToNextPageOrDismiss,
|
||||
)
|
||||
JunoOnboardingPageType.NOTIFICATION_PERMISSION -> notificationPermissionPageState(
|
||||
onPositiveButtonClick = {
|
||||
onNotificationPermissionButtonClick()
|
||||
scrollToNextPageOrDismiss()
|
||||
},
|
||||
onNegativeButtonClick = scrollToNextPageOrDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@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(
|
||||
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 = {},
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
/* 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.
|
||||
*/
|
||||
enum class JunoOnboardingPageType {
|
||||
DEFAULT_BROWSER,
|
||||
SYNC_SIGN_IN,
|
||||
NOTIFICATION_PERMISSION,
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
/* 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.activity.compose.BackHandler
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.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.components.components
|
||||
import org.mozilla.fenix.compose.PagerIndicator
|
||||
import org.mozilla.fenix.compose.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private val OnboardingPageTypeList = listOf(
|
||||
JunoOnboardingPageType.DEFAULT_BROWSER,
|
||||
JunoOnboardingPageType.NOTIFICATION_PERMISSION,
|
||||
JunoOnboardingPageType.SYNC_SIGN_IN,
|
||||
)
|
||||
|
||||
/**
|
||||
* A screen for displaying juno onboarding.
|
||||
*
|
||||
* @param onMakeFirefoxDefaultClick Invoked when the positive 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 onNotificationPermissionButtonClick Invoked when the positive button on notification
|
||||
* page is clicked.
|
||||
* @param onFinish Invoked when the onboarding is completed.
|
||||
*/
|
||||
@Composable
|
||||
fun JunoOnboardingScreen(
|
||||
onMakeFirefoxDefaultClick: () -> Unit,
|
||||
onPrivacyPolicyClick: (String) -> Unit,
|
||||
onSignInButtonClick: () -> Unit,
|
||||
onNotificationPermissionButtonClick: () -> Unit,
|
||||
onFinish: () -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState()
|
||||
val isSignedIn: State<Boolean?> = components.backgroundServices.syncStore
|
||||
.observeAsComposableState { it.account != null }
|
||||
|
||||
BackHandler(enabled = pagerState.currentPage > 0) {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage - 1)
|
||||
}
|
||||
}
|
||||
|
||||
val scrollToNextPageOrDismiss: () -> Unit = {
|
||||
if (pagerState.currentPage == pagerState.pageCount - 1) {
|
||||
onFinish()
|
||||
} else {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isSignedIn.value) {
|
||||
if (isSignedIn.value == true) {
|
||||
scrollToNextPageOrDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
JunoOnboardingContent(
|
||||
pagerState = pagerState,
|
||||
scrollToNextPageOrDismiss = scrollToNextPageOrDismiss,
|
||||
onMakeFirefoxDefaultClick = onMakeFirefoxDefaultClick,
|
||||
onPrivacyPolicyClick = onPrivacyPolicyClick,
|
||||
onSignInButtonClick = onSignInButtonClick,
|
||||
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
private fun JunoOnboardingContent(
|
||||
pagerState: PagerState,
|
||||
scrollToNextPageOrDismiss: () -> Unit,
|
||||
onMakeFirefoxDefaultClick: () -> Unit,
|
||||
onPrivacyPolicyClick: (String) -> Unit,
|
||||
onSignInButtonClick: () -> Unit,
|
||||
onNotificationPermissionButtonClick: () -> Unit,
|
||||
) {
|
||||
val nestedScrollConnection = remember { DisableForwardSwipeNestedScrollConnection(pagerState) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(FirefoxTheme.colors.layer1)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = OnboardingPageTypeList.size,
|
||||
state = pagerState,
|
||||
key = { OnboardingPageTypeList[it] },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
) { pageIndex ->
|
||||
val onboardingPageType = OnboardingPageTypeList[pageIndex]
|
||||
val pageState = mapToOnboardingPageState(
|
||||
onboardingPageType = onboardingPageType,
|
||||
scrollToNextPageOrDismiss = scrollToNextPageOrDismiss,
|
||||
onMakeFirefoxDefaultClick = onMakeFirefoxDefaultClick,
|
||||
onPrivacyPolicyClick = onPrivacyPolicyClick,
|
||||
onSignInButtonClick = onSignInButtonClick,
|
||||
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
|
||||
)
|
||||
OnboardingPage(pageState = pageState)
|
||||
}
|
||||
|
||||
PagerIndicator(
|
||||
pagerState = pagerState,
|
||||
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 {
|
||||
return 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.currentPageOffset < 0) {
|
||||
return Offset.Zero
|
||||
} else {
|
||||
Offset(available.x, available.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
private fun JunoOnboardingScreenPreview() {
|
||||
FirefoxTheme {
|
||||
JunoOnboardingContent(
|
||||
pagerState = PagerState(0),
|
||||
onMakeFirefoxDefaultClick = {},
|
||||
onPrivacyPolicyClick = {},
|
||||
onSignInButtonClick = {},
|
||||
onNotificationPermissionButtonClick = {},
|
||||
scrollToNextPageOrDismiss = {},
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue