[#465] Add onboarding debug menu (#475)

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Carter Jernigan 2022-06-09 13:17:58 -04:00 committed by GitHub
parent 2145698bba
commit 981d70727b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 225 additions and 111 deletions

View File

@ -43,8 +43,10 @@ class TestOnboardingActivity : ComponentActivity() {
if (!onboardingViewModel.isImporting.collectAsState().value) {
Onboarding(
onboardingState = onboardingViewModel.onboardingState,
isDebugMenuEnabled = false,
onImportWallet = { onboardingViewModel.isImporting.value = true },
onCreateWallet = {}
onCreateWallet = {},
onFixtureWallet = {}
)
reportFullyDrawn()

View File

@ -38,8 +38,11 @@ class OnboardingTestSetup(
ZcashTheme {
Onboarding(
onboardingState,
isDebugMenuEnabled = false,
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() },
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() }
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() },
// We aren't testing this because it is for debug builds only.
onFixtureWallet = {}
)
}
}

View File

@ -1,7 +1,6 @@
package co.electriccoin.zcash.ui
import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
@ -23,13 +22,8 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import cash.z.ecc.sdk.model.PersistableWallet
import cash.z.ecc.sdk.model.SeedPhrase
import cash.z.ecc.sdk.model.ZecRequest
import cash.z.ecc.sdk.send
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.design.compat.FontCompat
@ -45,13 +39,9 @@ import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.view.Onboarding
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding
import co.electriccoin.zcash.ui.screen.profile.WrapProfile
import co.electriccoin.zcash.ui.screen.request.view.Request
import co.electriccoin.zcash.ui.screen.restore.view.RestoreWallet
import co.electriccoin.zcash.ui.screen.restore.viewmodel.CompleteWordSetState
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
import co.electriccoin.zcash.ui.screen.scan.WrapScan
import co.electriccoin.zcash.ui.screen.seed.view.Seed
import co.electriccoin.zcash.ui.screen.send.view.Send
@ -161,97 +151,6 @@ class MainActivity : ComponentActivity() {
}
}
@Composable
private fun WrapOnboarding() {
val onboardingViewModel by viewModels<OnboardingViewModel>()
// TODO [#383]: https://github.com/zcash/secant-android-wallet/issues/383
if (!onboardingViewModel.isImporting.collectAsState().value) {
Onboarding(
onboardingState = onboardingViewModel.onboardingState,
onImportWallet = {
// In the case of the app currently being messed with by the robo test runner on
// Firebase Test Lab or Google Play pre-launch report, we want to skip creating
// a new or restoring an existing wallet screens by persisting an existing wallet
// with a mock seed.
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
persistExistingWalletWithSeedPhrase(SeedPhraseFixture.new())
return@Onboarding
}
onboardingViewModel.isImporting.value = true
},
onCreateWallet = {
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
persistExistingWalletWithSeedPhrase(SeedPhraseFixture.new())
return@Onboarding
}
walletViewModel.persistNewWallet()
}
)
reportFullyDrawn()
} else {
WrapRestore()
}
}
@Composable
private fun WrapRestore() {
val onboardingViewModel by viewModels<OnboardingViewModel>()
val restoreViewModel by viewModels<RestoreViewModel>()
when (val completeWordList = restoreViewModel.completeWordList.collectAsState().value) {
CompleteWordSetState.Loading -> {
// Although it might perform IO, it should be relatively fast.
// Consider whether to display indeterminate progress here.
// Another option would be to go straight to the restore screen with autocomplete
// disabled for a few milliseconds. Users would probably never notice due to the
// time it takes to re-orient on the new screen, unless users were doing this
// on a daily basis and become very proficient at our UI. The Therac-25 has
// historical precedent on how that could cause problems.
}
is CompleteWordSetState.Loaded -> {
RestoreWallet(
completeWordList.list,
restoreViewModel.userWordList,
onBack = { onboardingViewModel.isImporting.value = false },
paste = {
val clipboardManager = getSystemService(ClipboardManager::class.java)
return@RestoreWallet clipboardManager?.primaryClip?.toString()
},
onFinished = {
persistExistingWalletWithSeedPhrase(
SeedPhrase(restoreViewModel.userWordList.current.value)
)
}
)
}
}
}
/**
* Persists existing wallet together with the backup complete flag to disk. Be aware of that, it
* triggers navigation changes, as we observe the WalletViewModel.secretState.
*
* Write the backup complete flag first, then the seed phrase. That avoids the UI flickering to
* the backup screen. Assume if a user is restoring from a backup, then the user has a valid backup.
*
* @param seedPhrase to be persisted along with the wallet object
*/
private fun persistExistingWalletWithSeedPhrase(seedPhrase: SeedPhrase) {
walletViewModel.persistBackupComplete()
val network = ZcashNetwork.fromResources(application)
val restoredWallet = PersistableWallet(
network,
null,
seedPhrase
)
walletViewModel.persistExistingWallet(restoredWallet)
}
@Suppress("LongMethod")
@Composable
@SuppressWarnings("LongMethod")

View File

@ -0,0 +1,156 @@
package co.electriccoin.zcash.ui.screen.onboarding
import android.content.ClipboardManager
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalContext
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import cash.z.ecc.sdk.model.PersistableWallet
import cash.z.ecc.sdk.model.SeedPhrase
import cash.z.ecc.sdk.type.fromResources
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.screen.home.viewmodel.WalletViewModel
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
import co.electriccoin.zcash.ui.screen.restore.viewmodel.CompleteWordSetState
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
@Composable
internal fun MainActivity.WrapOnboarding() {
WrapOnboarding(this)
}
@Composable
internal fun WrapOnboarding(
activity: ComponentActivity
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
val applicationContext = LocalContext.current.applicationContext
// We might eventually want to check the debuggable property of the manifest instead
// of relying on BuildConfig.
val isDebugMenuEnabled = BuildConfig.DEBUG &&
!FirebaseTestLabUtil.isFirebaseTestLab(applicationContext) &&
!EmulatorWtfUtil.isEmulatorWtf(applicationContext)
// TODO [#383]: https://github.com/zcash/secant-android-wallet/issues/383
if (!onboardingViewModel.isImporting.collectAsState().value) {
Onboarding(
onboardingState = onboardingViewModel.onboardingState,
isDebugMenuEnabled = isDebugMenuEnabled,
onImportWallet = {
// In the case of the app currently being messed with by the robo test runner on
// Firebase Test Lab or Google Play pre-launch report, we want to skip creating
// a new or restoring an existing wallet screens by persisting an existing wallet
// with a mock seed.
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
persistExistingWalletWithSeedPhrase(
applicationContext,
walletViewModel,
SeedPhraseFixture.new()
)
return@Onboarding
}
onboardingViewModel.isImporting.value = true
},
onCreateWallet = {
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
persistExistingWalletWithSeedPhrase(
applicationContext,
walletViewModel,
SeedPhraseFixture.new()
)
return@Onboarding
}
walletViewModel.persistNewWallet()
},
onFixtureWallet = {
persistExistingWalletWithSeedPhrase(
applicationContext,
walletViewModel,
SeedPhraseFixture.new()
)
}
)
activity.reportFullyDrawn()
} else {
WrapRestore(activity)
}
}
@Composable
private fun WrapRestore(activity: ComponentActivity) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
val restoreViewModel by activity.viewModels<RestoreViewModel>()
val applicationContext = LocalContext.current.applicationContext
when (val completeWordList = restoreViewModel.completeWordList.collectAsState().value) {
CompleteWordSetState.Loading -> {
// Although it might perform IO, it should be relatively fast.
// Consider whether to display indeterminate progress here.
// Another option would be to go straight to the restore screen with autocomplete
// disabled for a few milliseconds. Users would probably never notice due to the
// time it takes to re-orient on the new screen, unless users were doing this
// on a daily basis and become very proficient at our UI. The Therac-25 has
// historical precedent on how that could cause problems.
}
is CompleteWordSetState.Loaded -> {
RestoreWallet(
completeWordList.list,
restoreViewModel.userWordList,
onBack = { onboardingViewModel.isImporting.value = false },
paste = {
val clipboardManager = applicationContext.getSystemService(ClipboardManager::class.java)
return@RestoreWallet clipboardManager?.primaryClip?.toString()
},
onFinished = {
persistExistingWalletWithSeedPhrase(
applicationContext,
walletViewModel,
SeedPhrase(restoreViewModel.userWordList.current.value)
)
}
)
}
}
}
/**
* Persists existing wallet together with the backup complete flag to disk. Be aware of that, it
* triggers navigation changes, as we observe the WalletViewModel.secretState.
*
* Write the backup complete flag first, then the seed phrase. That avoids the UI flickering to
* the backup screen. Assume if a user is restoring from a backup, then the user has a valid backup.
*
* @param seedPhrase to be persisted along with the wallet object
*/
private fun persistExistingWalletWithSeedPhrase(
context: Context,
walletViewModel: WalletViewModel,
seedPhrase: SeedPhrase
) {
walletViewModel.persistBackupComplete()
val network = ZcashNetwork.fromResources(context)
val restoredWallet = PersistableWallet(
network,
null,
seedPhrase
)
walletViewModel.persistExistingWallet(restoredWallet)
}

View File

@ -7,19 +7,30 @@ 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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
@ -48,8 +59,10 @@ fun ComposablePreview() {
GradientSurface {
Onboarding(
OnboardingState(OnboardingStage.UnifiedAddresses),
false,
onImportWallet = {},
onCreateWallet = {}
onCreateWallet = {},
onFixtureWallet = {}
)
}
}
@ -63,20 +76,35 @@ private const val IS_NAVIGATION_IN_APP_BAR = false
* @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
fun Onboarding(
onboardingState: OnboardingState,
isDebugMenuEnabled: Boolean,
onImportWallet: () -> Unit,
onCreateWallet: () -> Unit
onCreateWallet: () -> Unit,
onFixtureWallet: () -> Unit,
) {
Column {
OnboardingTopAppBar(onboardingState)
OnboardingMainContent(onboardingState, onImportWallet = onImportWallet, onCreateWallet = onCreateWallet)
Scaffold(
topBar = {
OnboardingTopAppBar(onboardingState, isDebugMenuEnabled, onFixtureWallet)
}
) { paddingValues ->
OnboardingMainContent(
paddingValues,
onboardingState,
onImportWallet = onImportWallet,
onCreateWallet = onCreateWallet
)
}
}
@Composable
private fun OnboardingTopAppBar(onboardingState: OnboardingState) {
private fun OnboardingTopAppBar(
onboardingState: OnboardingState,
isDebugMenuEnabled: Boolean,
onFixtureWallet: () -> Unit
) {
val currentStage = onboardingState.current.collectAsState().value
SmallTopAppBar(
@ -98,21 +126,47 @@ private fun OnboardingTopAppBar(onboardingState: OnboardingState) {
if (IS_NAVIGATION_IN_APP_BAR && currentStage.hasNext()) {
NavigationButton(onboardingState::goToEnd, stringResource(R.string.onboarding_skip))
}
if (isDebugMenuEnabled) {
DebugMenu(onFixtureWallet)
}
}
)
}
@Composable
private fun DebugMenu(onFixtureWallet: () -> Unit) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Import wallet with fixture seed phrase") },
onClick = onFixtureWallet
)
}
}
/**
* @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,
onboardingState: OnboardingState,
onImportWallet: () -> Unit,
onCreateWallet: () -> Unit
) {
Column {
Column(
Modifier
.padding(top = paddingValues.calculateTopPadding())
) {
if (!IS_NAVIGATION_IN_APP_BAR) {
TopNavButtons(onboardingState)
}