[#70] Generate screenshots automatically

Test Orchestrator must be enabled, so that screenshot generation can walk through the app like a newly registered user.  The screenshots are stored outside of the app's storage directory, so that Test Orchestrator does not delete them.  Because hard-coded paths are used, it is possible this could be brittle with future versions of Android

A future enhancement would be to also copy these screenshots off of tests run on Firebase Test Lab.  This would likely involve configuring Fladle/Flank with additional directories to pull.
This commit is contained in:
Carter Jernigan 2022-01-31 16:42:22 -05:00 committed by GitHub
parent 7cb3021caa
commit 2aba8fe33b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 502 additions and 32 deletions

View File

@ -40,4 +40,6 @@ If you plan to fork the project to create a new app of your own, please make the
1. Android Studio will warn about the Gradle checksum. This is a [known issue](https://github.com/gradle/gradle/issues/9361) and can be safely ignored.
1. [#96](https://github.com/zcash/secant-android-wallet/issues/96) - Release builds print some R8 warnings which can be safely ignored.
1. During app first launch, the following exception starting with `AndroidKeysetManager: keyset not found, will generate a new one` is printed twice. This exception is not an error, and the code is not being invoked twice.
1. The task `ktlintFormat` fails on Java 16 and greater. As a workaround, the task is run under Java 11. This requires that JDK 11 be installed, even if a newer JDK is also installed. While this is configured to use the Java toolchain API, [toolchain support for Java 11 does not work on Apple Silicon](https://github.com/gradle/gradle/issues/19140). [Azul Zulu](https://www.azul.com/downloads/?os=macos&architecture=arm-64-bit&package=jdk) does offer JDK 11, which must be installed manually to run this task from the command line on Apple Silicon.
1. The task `ktlintFormat` fails on Java 16 and greater. As a workaround, the task is run under Java 11. This requires that JDK 11 be installed, even if a newer JDK is also installed. While this is configured to use the Java toolchain API, [toolchain support for Java 11 does not work on Apple Silicon](https://github.com/gradle/gradle/issues/19140). [Azul Zulu](https://www.azul.com/downloads/?os=macos&architecture=arm-64-bit&package=jdk) does offer JDK 11, which must be installed manually to run this task from the command line on Apple Silicon.
1. When running instrumentation tests for the app module, this warning will be printed `WARNING: Failed to retrieve additional test outputs from device.
com.android.ddmlib.SyncException: Remote object doesn't exist!` followed by a stacktrace. This can be safely ignored.

View File

@ -8,6 +8,9 @@ plugins {
val packageName = "cash.z.ecc"
// Force orchestrator to be used for this module, because we need cleared state to generate screenshots
val isOrchestratorEnabled = true
android {
defaultConfig {
applicationId = packageName
@ -16,6 +19,16 @@ android {
// when the deployment runs
versionCode = project.property("ZCASH_VERSION_CODE").toString().toInt()
versionName = project.property("ZCASH_VERSION_NAME").toString()
if (isOrchestratorEnabled) {
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
}
if (isOrchestratorEnabled) {
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
}
compileOptions {
@ -118,9 +131,13 @@ dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(projects.uiLib)
androidTestImplementation(libs.androidx.compose.test.junit)
androidTestImplementation(libs.androidx.navigation.compose)
androidTestImplementation(libs.androidx.uiAutomator)
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(projects.sdkExtLib)
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
if (isOrchestratorEnabled) {
androidTestUtil(libs.androidx.test.orchestrator) {
artifact {
type = "apk"
@ -173,3 +190,25 @@ if (googlePlayServiceKeyFilePath.isNotEmpty()) {
}
}
}
val reportsDirectory = "${buildDir}/reports/androidTests/connected"
// This is coordinated with `EccScreenCaptureProcessor`
val onDeviceScreenshotsDirectory = "/sdcard/Pictures/zcash_screenshots"
val clearScreenshotsTask = tasks.create<Exec>("clearScreenshots") {
executable = project.android.adbExecutable.absolutePath
args = listOf("shell", "rm", "-r", onDeviceScreenshotsDirectory)
}
val fetchScreenshotsTask = tasks.create<Exec>("fetchScreenshots") {
executable = project.android.adbExecutable.absolutePath
args = listOf("pull", onDeviceScreenshotsDirectory, reportsDirectory)
finalizedBy(clearScreenshotsTask)
}
tasks.whenTaskAdded {
if (name == "connectedZcashmainnetDebugAndroidTest") {
finalizedBy(fetchScreenshotsTask)
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.test">
<!-- Required to write screenshots -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest>

View File

@ -0,0 +1,324 @@
package cash.z.ecc.app
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.screenshot.Screenshot
import cash.z.ecc.app.test.EccScreenCaptureProcessor
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import cash.z.ecc.ui.MainActivity
import cash.z.ecc.ui.R
import cash.z.ecc.ui.screen.backup.BackupTag
import cash.z.ecc.ui.screen.home.viewmodel.SecretState
import cash.z.ecc.ui.screen.restore.RestoreTag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
class ScreenshotTest {
companion object {
@BeforeClass
@JvmStatic
fun setupPPlus() {
if (Build.VERSION_CODES.P <= Build.VERSION.SDK_INT) {
val instrumentation = InstrumentationRegistry.getInstrumentation()
if (PackageManager.PERMISSION_DENIED == instrumentation.context.checkCallingOrSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
instrumentation.uiAutomation.grantRuntimePermission(instrumentation.context.packageName, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
}
fun takeScreenshot(screenshotName: String) {
val screenshot = Screenshot.capture().apply {
name = screenshotName
}
screenshot.process(setOf(EccScreenCaptureProcessor.new()))
}
}
private val composeTestRule = createAndroidComposeRule(MainActivity::class.java)
@get:Rule
val ruleChain = if (Build.VERSION_CODES.P <= Build.VERSION.SDK_INT) {
composeTestRule
} else {
val runtimePermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
RuleChain.outerRule(runtimePermissionRule).around(composeTestRule)
}
private fun navigateTo(route: String) = runBlocking {
withContext(Dispatchers.Main) {
composeTestRule.activity.navControllerForTesting.navigate(route)
}
}
@Test
@SmallTest
fun take_screenshots_for_restore_wallet() {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_header)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_header)).also {
it.assertExists()
}
takeScreenshot("Import 1")
val seedPhraseSplitLength = SeedPhraseFixture.new().split.size
SeedPhraseFixture.new().split.forEachIndexed { index, string ->
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.performTextInput(string)
// Take a screenshot half-way through filling in the seed phrase
if (index == seedPhraseSplitLength / 2) {
takeScreenshot("Import 2")
}
}
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_complete_header)).also {
it.assertExists()
}
takeScreenshot("Import 3")
}
@Test
@SmallTest
fun take_screenshots_for_new_wallet_and_rest_of_app() {
onboardingScreenshots(composeTestRule)
backupScreenshots(composeTestRule)
homeScreenshots(composeTestRule)
// Profile screen
// navigateTo(MainActivity.NAV_PROFILE)
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.home_profile_content_description))).also {
it.assertExists()
it.performClick()
}
profileScreenshots(composeTestRule)
// Settings is a subscreen of profile
composeTestRule.onNode(hasText(getStringResource(R.string.profile_settings))).also {
it.assertExists()
it.performClick()
}
settingsScreenshots(composeTestRule)
// Back to profile
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.settings_back_content_description))).also {
it.assertExists()
it.performClick()
}
// Address Details is a subscreen of profile
composeTestRule.onNode(hasText(getStringResource(R.string.profile_see_address_details))).also {
it.assertExists()
it.performClick()
}
addressDetailsScreenshots(composeTestRule)
}
}
private fun onboardingScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Onboarding 1")
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_2_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot("Onboarding 2")
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_3_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot("Onboarding 3")
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot("Onboarding 4")
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also {
it.performClick()
}
}
private fun backupScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.NeedsBackup }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_1_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Backup 1")
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_1_button)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_2_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Backup 2")
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_2_button)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Backup 3")
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_finished)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_4_header_verify)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Backup 4")
// Fail test first
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_4_header_ouch)).also {
it.assertExists()
ScreenshotTest.takeScreenshot("Backup Fail")
}
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_4_button_retry))).performClick()
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_header)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_finished)).also {
it.performClick()
}
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_4_header_verify)).also {
it.assertExists()
}
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it.assertCountEquals(4)
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
}
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_5_body))).also {
it.assertExists()
ScreenshotTest.takeScreenshot("Backup 5")
}
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_5_button_finished))).also {
it.assertExists()
it.performClick()
}
}
private fun homeScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready }
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
composeTestRule.onNode(hasText(getStringResource(R.string.home_button_send))).also {
it.assertExists()
ScreenshotTest.takeScreenshot("Home 1")
}
}
private fun profileScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.addresses.value != null }
composeTestRule.onNode(hasText(getStringResource(R.string.profile_title))).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Profile 1")
}
private fun settingsScreenshots(composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.settings_header))).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Settings 1")
}
private fun addressDetailsScreenshots(composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.wallet_address_title))).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot("Addresses 1")
}

View File

@ -0,0 +1,55 @@
package cash.z.ecc.app.test
import android.os.Build
import android.os.Environment
import androidx.test.runner.screenshot.ScreenCapture
import androidx.test.runner.screenshot.ScreenCaptureProcessor
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.UUID
class EccScreenCaptureProcessor private constructor(private val screenshotDir: File) : ScreenCaptureProcessor {
@Throws(IOException::class)
override fun process(capture: ScreenCapture): String {
screenshotDir.checkDirectoryIsWriteable()
val filename = newFilename(
name = capture.name ?: "",
suffix = capture.format.toString().lowercase()
)
BufferedOutputStream(FileOutputStream(File(screenshotDir, filename))).use {
capture.bitmap.compress(capture.format, DEFAULT_QUALITY, it)
it.flush()
}
return filename
}
companion object {
const val DEFAULT_QUALITY = 100
fun new(): EccScreenCaptureProcessor {
// Screenshots need to be stored in a public directory so they won't get cleared by Test Orchestrator
// This path must be coordinated with the build.gradle.kts script which copies these off the device
@Suppress("DEPRECATION")
val screenshotsDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "zcash_screenshots").also {
it.mkdirs()
}
return EccScreenCaptureProcessor(screenshotsDirectory)
}
private fun newFilename(name: String, suffix: String) = "screenshot-$name-${Build.VERSION.SDK_INT}-${Build.DEVICE}-${UUID.randomUUID()}.$suffix"
}
}
private fun File.checkDirectoryIsWriteable() {
if (!isDirectory && !canWrite()) {
throw IOException("The directory $this does not exist or is not writable.")
}
}

View File

@ -0,0 +1,7 @@
package cash.z.ecc.app
import android.content.Context
import androidx.annotation.StringRes
import androidx.test.core.app.ApplicationProvider
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)

View File

@ -54,6 +54,10 @@ A variety of Gradle tasks are set up within the project, and these tasks are als
* `lint` - Performs static analysis with Android lint
* `dependencyUpdates` - Checks for available dependency updates
A few notes on running instrumentation tests on the app module:
- Screenshots are generated automatically and copied to (/app/build/reports/androidTests/connected/zcash_screenshots)[../app/build/reports/androidTests/connected/zcash_screenshots]
- Running the Android tests on the app module will erase the data stored by the app. This is because Test Orchestrator is required to reset app state to successfully perform integration tests.
## Gradle Properties
A variety of Gradle properties can be used to configure the build.

View File

@ -62,6 +62,7 @@ dependencyResolutionManagement {
includeGroup("android.arch.lifecycle")
includeGroup("android.arch.core")
includeGroup("com.google.android.material")
includeGroup("com.google.testing.platform")
includeGroupByRegex("androidx.*")
includeGroupByRegex("com\\.android.*")
}

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier
import androidx.core.content.res.ResourcesCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@ -44,6 +45,7 @@ import cash.z.ecc.ui.screen.wallet_address.view.WalletAddresses
import cash.z.ecc.ui.theme.ZcashTheme
import cash.z.ecc.ui.util.AndroidApiVersion
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration
@ -53,7 +55,11 @@ import kotlin.time.Duration.Companion.seconds
@Suppress("TooManyFunctions")
class MainActivity : ComponentActivity() {
private val walletViewModel by viewModels<WalletViewModel>()
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val walletViewModel by viewModels<WalletViewModel>()
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
lateinit var navControllerForTesting: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -98,9 +104,7 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
.fillMaxHeight()
) {
val secretState = walletViewModel.secretState.collectAsState().value
when (secretState) {
when (val secretState = walletViewModel.secretState.collectAsState().value) {
SecretState.Loading -> {
// For now, keep displaying splash screen using condition above.
// In the future, we might consider displaying something different here.
@ -111,13 +115,16 @@ class MainActivity : ComponentActivity() {
is SecretState.NeedsBackup -> WrapBackup(secretState.persistableWallet)
is SecretState.Ready -> Navigation()
}
if (secretState != SecretState.Loading) {
reportFullyDrawn()
}
}
}
}
// Force collection to improve performance; sync can start happening while
// the user is going through the backup flow. Don't use eager collection in the view model,
// so that the collection is still tied to UI lifecycle.
lifecycleScope.launch {
walletViewModel.synchronizer.collect()
}
}
@Composable
@ -132,6 +139,8 @@ class MainActivity : ComponentActivity() {
walletViewModel.persistNewWallet()
}
)
reportFullyDrawn()
} else {
WrapRestore()
}
@ -196,51 +205,47 @@ class MainActivity : ComponentActivity() {
@Composable
private fun Navigation() {
val navController = rememberNavController()
val navController = rememberNavController().also {
navControllerForTesting = it
}
val home = "home"
val profile = "profile"
val walletAddressDetails = "wallet_address_details"
val settings = "settings"
val seed = "seed"
NavHost(navController = navController, startDestination = home) {
composable(home) {
NavHost(navController = navController, startDestination = NAV_HOME) {
composable(NAV_HOME) {
WrapHome(
goScan = {},
goProfile = { navController.navigate(profile) },
goProfile = { navController.navigate(NAV_PROFILE) },
goSend = {},
goRequest = {}
)
}
composable(profile) {
composable(NAV_PROFILE) {
WrapProfile(
onBack = { navController.popBackStack() },
onAddressDetails = { navController.navigate(walletAddressDetails) },
onAddressDetails = { navController.navigate(NAV_WALLET_ADDRESS_DETAILS) },
onAddressBook = { },
onSettings = { navController.navigate(settings) },
onSettings = { navController.navigate(NAV_SETTINGS) },
onCoinholderVote = { },
onSupport = {}
)
}
composable(walletAddressDetails) {
composable(NAV_WALLET_ADDRESS_DETAILS) {
WrapWalletAddresses(
goBack = {
navController.popBackStack()
}
)
}
composable(settings) {
composable(NAV_SETTINGS) {
WrapSettings(
goBack = {
navController.popBackStack()
},
goWalletBackup = {
navController.navigate(seed)
navController.navigate(NAV_SEED)
}
)
}
composable(seed) {
composable(NAV_SEED) {
WrapSeed(
goBack = {
navController.popBackStack()
@ -269,6 +274,8 @@ class MainActivity : ComponentActivity() {
goSend = goSend,
goProfile = goProfile
)
reportFullyDrawn()
}
}
@ -332,7 +339,7 @@ class MainActivity : ComponentActivity() {
// If wipe ever becomes an operation to also delete the seed, then we'll also need
// to do the following to clear any retained state from onboarding (only happens if
// occuring during same session as onboarding)
// occurring during same session as onboarding)
// onboardingViewModel.onboardingState.goToBeginning()
// onboardingViewModel.isImporting.value = false
}
@ -369,6 +376,21 @@ class MainActivity : ComponentActivity() {
companion object {
@VisibleForTesting
internal val SPLASH_SCREEN_DELAY = 0.seconds
@VisibleForTesting
const val NAV_HOME = "home"
@VisibleForTesting
const val NAV_PROFILE = "profile"
@VisibleForTesting
const val NAV_WALLET_ADDRESS_DETAILS = "wallet_address_details"
@VisibleForTesting
const val NAV_SETTINGS = "settings"
@VisibleForTesting
const val NAV_SEED = "seed"
}
}

View File

@ -124,7 +124,7 @@ private fun HomeMainContent(
}
@Composable
private fun Status(@Suppress("UNUSED_PARAMETER") walletSnapshot: WalletSnapshot) {
private fun Status(walletSnapshot: WalletSnapshot) {
Column(Modifier.fillMaxWidth()) {
Header(text = walletSnapshot.totalBalance().toString())
Body(

View File

@ -87,13 +87,17 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
// This will likely move to an application global, so that it can be referenced by WorkManager
// for background synchronization
/**
* Synchronizer for the Zcash SDK. Note that the synchronizer loads as soon as a secret is stored,
* even if the backup of the secret has not occurred yet.
*/
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
val synchronizer: StateFlow<Synchronizer?> = secretState
.filterIsInstance<SecretState.Ready>()
val synchronizer: StateFlow<Synchronizer?> = persistableWallet
.filterNotNull()
.flatMapConcat {
callbackFlow {
val synchronizer = synchronizerMutex.withLock {
val synchronizer = SynchronizerCompanion.load(application, it.persistableWallet)
val synchronizer = SynchronizerCompanion.load(application, it)
synchronizer.start(viewModelScope)
}

View File

@ -212,6 +212,8 @@ private fun More() {
@Composable
private fun Wallet(onCreateWallet: () -> Unit, onImportWallet: () -> Unit) {
Column {
Header(stringResource(R.string.onboarding_4_header))
Body(stringResource(R.string.onboarding_4_body))
PrimaryButton(onCreateWallet, stringResource(R.string.onboarding_4_create_new_wallet), Modifier.fillMaxWidth())
TertiaryButton(
onImportWallet, stringResource(R.string.onboarding_4_import_existing_wallet),