diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/TestOnboardingActivity.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/TestOnboardingActivity.kt index d8780cf8..a91a30e6 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/TestOnboardingActivity.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/TestOnboardingActivity.kt @@ -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() diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingTestSetup.kt index ba301fe7..30a34c7c 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingTestSetup.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingTestSetup.kt @@ -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 = {} ) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt index 564139bd..dc9f318b 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt @@ -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() - - // 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() - val restoreViewModel by viewModels() - - 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") diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt new file mode 100644 index 00000000..d06c53a7 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt @@ -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() + val onboardingViewModel by activity.viewModels() + + 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() + val onboardingViewModel by activity.viewModels() + val restoreViewModel by activity.viewModels() + + 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) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingView.kt index b495b5dc..4429edee 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/view/OnboardingView.kt @@ -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) }