[#763] Configure simplified onboarding
This commit is contained in:
parent
9f9ee0fcc1
commit
a806d2defa
|
@ -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() },
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,6 +112,7 @@ private fun OnboardingTopAppBar(
|
|||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.app_name)) },
|
||||
navigationIcon = {
|
||||
if (isFullOnboardingEnabled) {
|
||||
if (currentStage.hasPrevious()) {
|
||||
IconButton(onboardingState::goPrevious) {
|
||||
Icon(
|
||||
|
@ -114,6 +121,7 @@ private fun OnboardingTopAppBar(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (currentStage.hasNext()) {
|
||||
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,8 +79,6 @@ class ScreenshotTest : UiTestPrerequisites() {
|
|||
.captureToBitmap()
|
||||
.writeToTestStorage("$screenshotName - $tag")
|
||||
}
|
||||
|
||||
private val emptyConfiguration = StringConfiguration(persistentMapOf(), null)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
|
@ -146,6 +144,7 @@ class ScreenshotTest : UiTestPrerequisites() {
|
|||
|
||||
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()
|
||||
}
|
||||
|
@ -154,6 +153,7 @@ class ScreenshotTest : UiTestPrerequisites() {
|
|||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_import_existing_wallet)).also {
|
||||
it.assertExists()
|
||||
|
@ -333,9 +333,11 @@ 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()
|
||||
}
|
||||
|
@ -360,6 +362,7 @@ private fun onboardingScreenshots(resContext: Context, tag: String, composeTestR
|
|||
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_header)).also {
|
||||
it.assertExists()
|
||||
|
|
Loading…
Reference in New Issue