[#763] Configure simplified onboarding

This commit is contained in:
Carter Jernigan 2023-02-21 13:28:03 -05:00 committed by Carter Jernigan
parent 9f9ee0fcc1
commit a806d2defa
9 changed files with 138 additions and 71 deletions

View File

@ -11,8 +11,17 @@ import java.util.concurrent.atomic.AtomicInteger
class OnboardingTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val isFullOnboardingEnabled: Boolean,
initialStage: OnboardingStage
) {
init {
if (!isFullOnboardingEnabled) {
require(initialStage == OnboardingStage.Wallet) {
"When full onboarding is disabled, the initial stage must be Wallet"
}
}
}
private val onboardingState = OnboardingState(initialStage)
private val onCreateWalletCallbackCount = AtomicInteger(0)
@ -38,6 +47,7 @@ class OnboardingTestSetup(
fun getDefaultContent() {
ZcashTheme {
Onboarding(
isFullOnboardingEnabled,
onboardingState,
isDebugMenuEnabled = false,
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() },

View File

@ -18,7 +18,11 @@ class OnboardingActivityTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createAndroidComposeRule<UiTestingActivity>()
private fun newTestSetup() = OnboardingTestSetup(composeTestRule, OnboardingStage.ShieldedByDefault)
private fun newTestSetup() = OnboardingTestSetup(
composeTestRule,
isFullOnboardingEnabled = true,
OnboardingStage.ShieldedByDefault
)
@Test
@MediumTest

View File

@ -18,7 +18,11 @@ class OnboardingIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(initialStage: OnboardingStage) = OnboardingTestSetup(composeTestRule, initialStage)
private fun newTestSetup(initialStage: OnboardingStage) = OnboardingTestSetup(
composeTestRule,
isFullOnboardingEnabled = true,
initialStage
)
/**
* The test semantics are built upon StateRestorationTester component. We simulate screen state

View File

@ -20,8 +20,8 @@ class OnboardingViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(initialStage: OnboardingStage): OnboardingTestSetup {
return OnboardingTestSetup(composeTestRule, initialStage).apply {
private fun newTestSetup(isFullOnboardingEnabled: Boolean = true, initialStage: OnboardingStage): OnboardingTestSetup {
return OnboardingTestSetup(composeTestRule, isFullOnboardingEnabled, initialStage).apply {
setDefaultContent()
}
}
@ -30,7 +30,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun verify_test_setup_stage_1() {
val testSetup = newTestSetup(OnboardingStage.ShieldedByDefault)
val testSetup = newTestSetup(initialStage = OnboardingStage.ShieldedByDefault)
assertEquals(OnboardingStage.ShieldedByDefault, testSetup.getOnboardingStage())
assertEquals(0, testSetup.getOnImportWalletCallbackCount())
@ -40,7 +40,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun verify_test_setup_stage_4() {
val testSetup = newTestSetup(OnboardingStage.Wallet)
val testSetup = newTestSetup(initialStage = OnboardingStage.Wallet)
assertEquals(OnboardingStage.Wallet, testSetup.getOnboardingStage())
assertEquals(0, testSetup.getOnImportWalletCallbackCount())
@ -50,7 +50,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun stage_1_layout() {
newTestSetup(OnboardingStage.ShieldedByDefault)
newTestSetup(initialStage = OnboardingStage.ShieldedByDefault)
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
it.assertExists()
@ -77,7 +77,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun stage_2_layout() {
newTestSetup(OnboardingStage.UnifiedAddresses)
newTestSetup(initialStage = OnboardingStage.UnifiedAddresses)
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
it.assertExists()
@ -114,7 +114,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun stage_3_layout() {
newTestSetup(OnboardingStage.More)
newTestSetup(initialStage = OnboardingStage.More)
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
it.assertExists()
@ -151,7 +151,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun stage_4_layout() {
newTestSetup(OnboardingStage.Wallet)
newTestSetup(initialStage = OnboardingStage.Wallet)
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
it.assertDoesNotExist()
@ -180,10 +180,40 @@ class OnboardingViewTest : UiTestPrerequisites() {
}
}
@Test
@MediumTest
fun stage_4_layout_short_onboarding() {
newTestSetup(isFullOnboardingEnabled = false, initialStage = OnboardingStage.Wallet)
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.onboarding_back)).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also {
it.assertExists()
it.assertIsEnabled()
it.assertHasClickAction()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also {
it.assertExists()
it.assertIsEnabled()
it.assertHasClickAction()
}
}
@Test
@MediumTest
fun stage_1_skip() {
val testSetup = newTestSetup(OnboardingStage.ShieldedByDefault)
val testSetup = newTestSetup(initialStage = OnboardingStage.ShieldedByDefault)
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
it.performClick()
@ -195,7 +225,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun last_stage_click_create_wallet() {
val testSetup = newTestSetup(OnboardingStage.Wallet)
val testSetup = newTestSetup(initialStage = OnboardingStage.Wallet)
val newWalletButton = composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet))
newWalletButton.performClick()
@ -207,7 +237,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun last_stage_click_import_wallet() {
val testSetup = newTestSetup(OnboardingStage.Wallet)
val testSetup = newTestSetup(initialStage = OnboardingStage.Wallet)
val newWalletButton = composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet))
newWalletButton.performClick()
@ -219,7 +249,7 @@ class OnboardingViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun multi_stage_progression() {
val testSetup = newTestSetup(OnboardingStage.ShieldedByDefault)
val testSetup = newTestSetup(initialStage = OnboardingStage.ShieldedByDefault)
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
it.performClick()

View File

@ -8,7 +8,11 @@ object ConfigurationEntries {
/*
* Disabled because we don't have the URI parser support in the SDK yet.
*
*/
val IS_REQUEST_ZEC_ENABLED = BooleanConfigurationEntry(ConfigKey("is_request_zec_enabled"), false)
/*
* The full onboarding flow is functional and tested, but it is disabled by default for an initially minimal feature set.
*/
val IS_FULL_ONBOARDING_ENABLED = BooleanConfigurationEntry(ConfigKey("is_full_onboarding_enabled"), false)
}

View File

@ -18,7 +18,11 @@ import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
import co.electriccoin.zcash.ui.screen.onboarding.state.OnboardingState
import co.electriccoin.zcash.ui.screen.onboarding.view.Onboarding
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
import co.electriccoin.zcash.ui.screen.restore.view.RestoreWallet
@ -47,8 +51,18 @@ internal fun WrapOnboarding(
// TODO [#383]: https://github.com/zcash/secant-android-wallet/issues/383
if (!onboardingViewModel.isImporting.collectAsStateWithLifecycle().value) {
val isFullOnboardingEnabled = ConfigurationEntries.IS_FULL_ONBOARDING_ENABLED.getValue(RemoteConfig.current)
val onboardingState = if (isFullOnboardingEnabled) {
onboardingViewModel.onboardingState
} else {
// Force to the last screen, which is the "create wallet" screen.
// This simplifies the implementation inside the Onboarding composable.
OnboardingState(OnboardingStage.values().last())
}
Onboarding(
onboardingState = onboardingViewModel.onboardingState,
isFullOnboardingEnabled = isFullOnboardingEnabled,
onboardingState = onboardingState,
isDebugMenuEnabled = isDebugMenuEnabled,
onImportWallet = {
// In the case of the app currently being messed with by the robo test runner on
@ -64,7 +78,7 @@ internal fun WrapOnboarding(
return@Onboarding
}
onboardingViewModel.isImporting.value = true
onboardingViewModel.setIsImporting(true)
},
onCreateWallet = {
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
@ -115,7 +129,7 @@ private fun WrapRestore(activity: ComponentActivity) {
RestoreWallet(
completeWordList.list,
restoreViewModel.userWordList,
onBack = { onboardingViewModel.isImporting.value = false },
onBack = { onboardingViewModel.setIsImporting(false) },
paste = {
val clipboardManager = applicationContext.getSystemService(ClipboardManager::class.java)
return@RestoreWallet clipboardManager?.primaryClip?.toString()

View File

@ -55,8 +55,9 @@ fun ComposablePreview() {
ZcashTheme(darkTheme = true) {
GradientSurface {
Onboarding(
isFullOnboardingEnabled = true,
OnboardingState(OnboardingStage.Wallet),
false,
isDebugMenuEnabled = false,
onImportWallet = {},
onCreateWallet = {},
onFixtureWallet = {}
@ -66,12 +67,16 @@ fun ComposablePreview() {
}
/**
* @param isFullOnboardingEnabled Feature toggle to control whether the full onboarding flow is enabled. If disabled, then an abbreviated flow is shown
* and the onboarding state is treated effectively as if it is [OnboardingStage.Wallet].
* @param onImportWallet Callback when the user decides to import an existing wallet.
* @param onCreateWallet Callback when the user decides to create a new wallet.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongParameterList")
fun Onboarding(
isFullOnboardingEnabled: Boolean,
onboardingState: OnboardingState,
isDebugMenuEnabled: Boolean,
onImportWallet: () -> Unit,
@ -81,7 +86,7 @@ fun Onboarding(
val currentStage = onboardingState.current.collectAsStateWithLifecycle().value
Scaffold(
topBar = {
OnboardingTopAppBar(onboardingState, isDebugMenuEnabled, onFixtureWallet)
OnboardingTopAppBar(isFullOnboardingEnabled, onboardingState, isDebugMenuEnabled, onFixtureWallet)
},
bottomBar = {
BottomNav(currentStage, onboardingState::goNext, onCreateWallet, onImportWallet)
@ -97,6 +102,7 @@ fun Onboarding(
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun OnboardingTopAppBar(
isFullOnboardingEnabled: Boolean,
onboardingState: OnboardingState,
isDebugMenuEnabled: Boolean,
onFixtureWallet: () -> Unit
@ -106,12 +112,14 @@ private fun OnboardingTopAppBar(
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
navigationIcon = {
if (currentStage.hasPrevious()) {
IconButton(onboardingState::goPrevious) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.onboarding_back)
)
if (isFullOnboardingEnabled) {
if (currentStage.hasPrevious()) {
IconButton(onboardingState::goPrevious) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.onboarding_back)
)
}
}
}
},
@ -145,11 +153,6 @@ private fun DebugMenu(onFixtureWallet: () -> Unit) {
}
}
/**
* @param onImportWallet Callback when the user decides to import an existing wallet.
* @param onCreateWallet Callback when the user decides to create a new wallet.
*/
@Composable
fun OnboardingMainContent(
paddingValues: PaddingValues,

View File

@ -7,7 +7,6 @@ import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.ext.collectWith
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
import co.electriccoin.zcash.ui.screen.onboarding.state.OnboardingState
import kotlinx.coroutines.flow.MutableStateFlow
/*
* Android-specific ViewModel. This is used to save and restore state across Activity recreations
@ -15,7 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
*/
class OnboardingViewModel(
application: Application,
savedStateHandle: SavedStateHandle
private val savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
val onboardingState: OnboardingState = run {
@ -35,21 +34,17 @@ class OnboardingViewModel(
// This is a bit weird being placed here, but onboarding currently is considered complete when
// the user has a persisted wallet. Also import allows the user to go back to onboarding, while
// creating a new wallet does not.
val isImporting = run {
val initialValue = savedStateHandle.get<Boolean?>(KEY_IS_IMPORTING) ?: false
val isImporting = savedStateHandle.getStateFlow(KEY_IS_IMPORTING, false)
MutableStateFlow(initialValue)
fun setIsImporting(isImporting: Boolean) {
savedStateHandle[KEY_IS_IMPORTING] = isImporting
}
init {
// viewModelScope is constructed with Dispatchers.Main.immediate, so this will
// update the save state as soon as a change occurs.
onboardingState.current.collectWith(viewModelScope) {
savedStateHandle.set(KEY_STAGE, it)
}
isImporting.collectWith(viewModelScope) {
savedStateHandle.set(KEY_IS_IMPORTING, it)
savedStateHandle[KEY_STAGE] = it
}
}

View File

@ -79,8 +79,6 @@ class ScreenshotTest : UiTestPrerequisites() {
.captureToBitmap()
.writeToTestStorage("$screenshotName - $tag")
}
private val emptyConfiguration = StringConfiguration(persistentMapOf(), null)
}
@get:Rule
@ -146,13 +144,15 @@ class ScreenshotTest : UiTestPrerequisites() {
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None }
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists()
}
if (ConfigurationEntries.IS_FULL_ONBOARDING_ENABLED.getValue(emptyConfiguration)) {
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_skip)).also {
it.assertExists()
it.performClick()
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_skip)).also {
it.assertExists()
it.performClick()
}
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_import_existing_wallet)).also {
@ -333,32 +333,35 @@ class ScreenshotTest : UiTestPrerequisites() {
}
}
private val emptyConfiguration = StringConfiguration(persistentMapOf(), null)
private fun onboardingScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None }
if (ConfigurationEntries.IS_FULL_ONBOARDING_ENABLED.getValue(emptyConfiguration)) {
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Onboarding 1")
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Onboarding 1")
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_2_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Onboarding 2")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_2_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Onboarding 2")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_3_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Onboarding 3")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick()
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_3_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Onboarding 3")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick()
}
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_header)).also {