From 9a64acd4b6755b9fecd7692bbe95233c6e24fd82 Mon Sep 17 00:00:00 2001 From: Gabriel Luong Date: Mon, 25 Jul 2022 10:58:46 -0400 Subject: [PATCH] For #26169 - MR Home Onboarding Dialog for upgrading users --- app/build.gradle | 1 + .../SessionControlController.kt | 11 +- .../home/sessioncontrol/SessionControlView.kt | 9 +- .../HomeOnboardingDialogFragment.kt | 62 ++-- .../fenix/onboarding/view/Onboarding.kt | 279 ++++++++++++++++++ .../java/org/mozilla/fenix/utils/Settings.kt | 8 - .../fragment_onboarding_home_dialog.xml | 219 -------------- app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values/dimens.xml | 3 - app/src/main/res/values/preference_keys.xml | 1 - app/src/main/res/values/strings.xml | 33 ++- app/src/main/res/values/styles.xml | 2 +- .../sessioncontrol/SessionControlViewTest.kt | 83 ------ buildSrc/src/main/java/Dependencies.kt | 3 + 14 files changed, 356 insertions(+), 361 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/onboarding/view/Onboarding.kt delete mode 100644 app/src/main/res/layout/fragment_onboarding_home_dialog.xml diff --git a/app/build.gradle b/app/build.gradle index f346771cc..6a0644983 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -467,6 +467,7 @@ dependencies { implementation Deps.androidx_constraintlayout implementation Deps.androidx_coordinatorlayout implementation Deps.google_accompanist_drawablepainter + implementation Deps.google_accompanist_insets implementation Deps.sentry diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index ff5536c42..f03b7f90a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -30,7 +30,6 @@ import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.kotlin.isUrl import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.HomeScreen @@ -508,12 +507,10 @@ class DefaultSessionControlController( } override fun handleShowOnboardingDialog() { - if (FeatureFlags.showHomeOnboarding) { - navController.nav( - R.id.homeFragment, - HomeFragmentDirections.actionGlobalHomeOnboardingDialog() - ) - } + navController.nav( + R.id.homeFragment, + HomeFragmentDirections.actionGlobalHomeOnboardingDialog() + ) } override fun handleReadPrivacyNoticeClicked() { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index baac4a035..17d4e172a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -171,13 +171,6 @@ private fun AppState.toAdapterList(settings: Settings): List = when is Mode.Onboarding -> onboardingAdapterItems(mode.state) } -@VisibleForTesting -internal fun AppState.shouldShowHomeOnboardingDialog(settings: Settings): Boolean { - val isAnySectionsVisible = recentTabs.isNotEmpty() || recentBookmarks.isNotEmpty() || - recentHistory.isNotEmpty() || pocketStories.isNotEmpty() - return isAnySectionsVisible && !settings.hasShownHomeOnboardingDialog -} - private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapIndexed { index, tab -> AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex) @@ -211,7 +204,7 @@ class SessionControlView( } fun update(state: AppState, shouldReportMetrics: Boolean = false) { - if (state.shouldShowHomeOnboardingDialog(view.context.settings())) { + if (view.context.settings().showHomeOnboardingDialog) { interactor.showOnboardingDialog() } diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/HomeOnboardingDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/onboarding/HomeOnboardingDialogFragment.kt index 74601c9c9..ae32d2715 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/HomeOnboardingDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/HomeOnboardingDialogFragment.kt @@ -4,48 +4,72 @@ package org.mozilla.fenix.onboarding +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import com.google.accompanist.insets.ProvideWindowInsets +import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.R -import org.mozilla.fenix.databinding.FragmentOnboardingHomeDialogBinding +import org.mozilla.fenix.components.components +import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.onboarding.view.Onboarding +import org.mozilla.fenix.theme.FirefoxTheme /** - * Dialog displayed once when one or multiples of these sections are shown in the home screen - * recentTabs,recentBookmarks,historyMetadata or pocketArticles. + * Dialog displaying a welcome and sync sign in onboarding. */ class HomeOnboardingDialogFragment : DialogFragment() { + @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, R.style.HomeOnboardingDialogStyle) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + override fun onDestroy() { + super.onDestroy() + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? = inflater.inflate(R.layout.fragment_onboarding_home_dialog, container, false) + ): View = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val binding = FragmentOnboardingHomeDialogBinding.bind(view) + setContent { + ProvideWindowInsets { + FirefoxTheme { + val account = + components.backgroundServices.syncStore.observeAsComposableState { state -> state.account } - val appName = requireContext().getString(R.string.app_name) - binding.welcomeTitle.text = - requireContext().getString(R.string.onboarding_home_screen_title_3, appName) - binding.homeTitle.text = requireContext().getString( - R.string.onboarding_home_screen_section_home_title_3, - appName - ) - - binding.finishButton.setOnClickListener { - context?.settings()?.let { settings -> - settings.hasShownHomeOnboardingDialog = true + Onboarding( + isSyncSignIn = account.value != null, + onDismiss = ::onDismiss, + onSignInButtonClick = { + findNavController().nav( + R.id.homeOnboardingDialogFragment, + HomeOnboardingDialogFragmentDirections.actionGlobalTurnOnSync() + ) + onDismiss() + }, + ) + } } - dismiss() } } + + private fun onDismiss() { + context?.settings()?.showHomeOnboardingDialog = false + dismiss() + } } diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/Onboarding.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/Onboarding.kt new file mode 100644 index 000000000..bcbe6df30 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/Onboarding.kt @@ -0,0 +1,279 @@ +/* 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.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.insets.navigationBarsPadding +import com.google.accompanist.insets.statusBarsPadding +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.button.PrimaryButton +import org.mozilla.fenix.compose.button.SecondaryButton +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * Enum that represents the onboarding screen that is displayed. + */ +private enum class OnboardingState { + Welcome, + SyncSignIn +} + +/** + * A screen for displaying a welcome and sync sign in onboarding. + * + * @param isSyncSignIn Whether or not the user is signed into their Firefox Sync account. + * @param onDismiss Invoked when the user clicks on the close or "Skip" button. + * @param onSignInButtonClick Invoked when the user clicks on the "Sign In" button + */ +@Composable +fun Onboarding( + isSyncSignIn: Boolean, + onDismiss: () -> Unit, + onSignInButtonClick: () -> Unit, +) { + var onboardingState by remember { mutableStateOf(OnboardingState.Welcome) } + + Column( + modifier = Modifier + .background(FirefoxTheme.colors.layer1) + .fillMaxSize() + .padding(bottom = 32.dp) + .statusBarsPadding() + .navigationBarsPadding() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + IconButton(onClick = onDismiss) { + Icon( + painter = painterResource(id = R.drawable.mozac_ic_close), + contentDescription = null, + tint = FirefoxTheme.colors.iconPrimary, + ) + } + } + + if (onboardingState == OnboardingState.Welcome) { + OnboardingWelcomeContent() + + OnboardingWelcomeBottomContent( + onboardingState = onboardingState, + isSyncSignIn = isSyncSignIn, + onGetStartedButtonClick = { + if (isSyncSignIn) { + onDismiss() + } else { + onboardingState = OnboardingState.SyncSignIn + } + }, + ) + } else if (onboardingState == OnboardingState.SyncSignIn) { + OnboardingSyncSignInContent() + + OnboardingSyncSignInBottomContent( + onboardingState = onboardingState, + onSignInButtonClick = onSignInButtonClick, + onSkipButtonClick = onDismiss, + ) + } + } +} + +@Composable +private fun OnboardingWelcomeBottomContent( + onboardingState: OnboardingState, + isSyncSignIn: Boolean, + onGetStartedButtonClick: () -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + PrimaryButton( + text = stringResource(id = R.string.onboarding_home_get_started_button), + onClick = onGetStartedButtonClick, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + if (isSyncSignIn) { + Spacer(modifier = Modifier.height(6.dp)) + } else { + Indicators(onboardingState = onboardingState) + } + } +} + +@Composable +private fun OnboardingWelcomeContent() { + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_firefox), + contentDescription = null, + modifier = Modifier.size(109.dp), + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(id = R.string.onboarding_home_welcome_title), + color = FirefoxTheme.colors.textPrimary, + textAlign = TextAlign.Center, + style = FirefoxTheme.typography.headline5, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.onboarding_home_welcome_description), + color = FirefoxTheme.colors.textSecondary, + textAlign = TextAlign.Center, + style = FirefoxTheme.typography.body2, + ) + } +} + +@Composable +private fun OnboardingSyncSignInContent() { + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_scan), + contentDescription = null, + modifier = Modifier.size(320.dp, 166.dp), + contentScale = ContentScale.FillBounds, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(id = R.string.onboarding_home_sync_title), + color = FirefoxTheme.colors.textPrimary, + textAlign = TextAlign.Center, + style = FirefoxTheme.typography.headline5, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.onboarding_home_sync_description), + color = FirefoxTheme.colors.textSecondary, + textAlign = TextAlign.Center, + style = FirefoxTheme.typography.body2, + ) + } +} + +@Composable +private fun OnboardingSyncSignInBottomContent( + onboardingState: OnboardingState, + onSignInButtonClick: () -> Unit, + onSkipButtonClick: () -> Unit, +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + PrimaryButton( + text = stringResource(id = R.string.onboarding_home_sign_in_button), + onClick = onSignInButtonClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SecondaryButton( + text = stringResource(id = R.string.onboarding_home_skip_button), + onClick = onSkipButtonClick, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Indicators(onboardingState = onboardingState) + } +} + +@Composable +private fun Indicators(onboardingState: OnboardingState) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Indicator( + color = if (onboardingState == OnboardingState.Welcome) { + FirefoxTheme.colors.indicatorActive + } else { + FirefoxTheme.colors.indicatorInactive + } + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Indicator( + color = if (onboardingState == OnboardingState.SyncSignIn) { + FirefoxTheme.colors.indicatorActive + } else { + FirefoxTheme.colors.indicatorInactive + } + ) + } +} + +@Composable +private fun Indicator(color: Color) { + Box( + modifier = Modifier + .size(6.dp) + .clip(CircleShape) + .background(color), + ) +} + +@Composable +@Preview +private fun OnboardingPreview() { + FirefoxTheme { + Onboarding( + isSyncSignIn = false, + onDismiss = {}, + onSignInButtonClick = {}, + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 69ab3e3ae..061ed0a5a 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -791,14 +791,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) - /** - * Indicates if the home onboarding dialog has already shown before. - */ - var hasShownHomeOnboardingDialog by booleanPreference( - appContext.getPreferenceKey(R.string.pref_key_has_shown_home_onboarding), - default = false - ) - fun incrementVisitedInstallableCount() = pwaInstallableVisitCount.increment() @VisibleForTesting(otherwise = PRIVATE) diff --git a/app/src/main/res/layout/fragment_onboarding_home_dialog.xml b/app/src/main/res/layout/fragment_onboarding_home_dialog.xml deleted file mode 100644 index b6ec4e2bb..000000000 --- a/app/src/main/res/layout/fragment_onboarding_home_dialog.xml +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -