[#762] Simplified seed backup UX

Because we’re making UI text changes, I’ve forked our views into “long” and “short” versions that preserve the previous experience while allowing the shorter experience to be different.

One limitation of the current approach is that the screenshot tests are limited to the current configuration, so there is some risk of ‘bit rot’ with the screenshot tests for the longer onboarding.  For this PR, I manually switched the feature flags and re-ran the screenshot tests to make sure they still worked.

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Carter Jernigan 2023-03-03 08:06:03 -05:00 committed by GitHub
parent 4acd5d3593
commit 8e17d07ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 942 additions and 360 deletions

View File

@ -0,0 +1,28 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=":ui-benchmark-test:connectedBenchmarkAndroidTest" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="ORG_GRADLE_PROJECT_IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY" value="true" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":ui-benchmark-test:connectedBenchmarkAndroidTest" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,28 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=":ui-screenshot-test:connectedCheck" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="ORG_GRADLE_PROJECT_IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY" value="true" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":ui-screenshot-test:connectedCheck" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@ -1,55 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ui-benchmark-test:connectedBenchmarkAndroidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-app.ui-benchmark-test" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="TEST_NAME_REGEX" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="RETENTION_ENABLED" value="No" />
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@ -1,20 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ui-design-lib:connectedCheck" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-app.ui-design-lib" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="TEST_NAME_REGEX" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="RETENTION_ENABLED" value="No" />
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />

View File

@ -1,57 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ui-screenshot-test:connectedCheck" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-app.ui-screenshot-test" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="RETENTION_ENABLED" value="No" />
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@ -31,10 +31,10 @@ android {
"src/main/res/ui/common",
"src/main/res/ui/home",
"src/main/res/ui/onboarding",
"src/main/res/ui/scan",
"src/main/res/ui/receive",
"src/main/res/ui/restore",
"src/main/res/ui/request",
"src/main/res/ui/restore",
"src/main/res/ui/scan",
"src/main/res/ui/seed",
"src/main/res/ui/send",
"src/main/res/ui/settings",

View File

@ -9,7 +9,7 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.UiTestingActivity
import co.electriccoin.zcash.ui.fixture.TestChoicesFixture
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
import co.electriccoin.zcash.ui.screen.backup.view.BackupTestSetup
import co.electriccoin.zcash.ui.screen.backup.view.LongBackupTestSetup
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
@ -20,8 +20,8 @@ class BackupActivityTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createAndroidComposeRule<UiTestingActivity>()
private fun newTestSetup(): BackupTestSetup {
return BackupTestSetup(
private fun newTestSetup(): LongBackupTestSetup {
return LongBackupTestSetup(
composeTestRule,
BackupStage.EducationOverview,
TestChoicesFixture.new(TestChoicesFixture.INITIAL_CHOICES)

View File

@ -16,7 +16,7 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.fixture.TestChoicesFixture
import co.electriccoin.zcash.ui.screen.backup.BackupTag
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
import co.electriccoin.zcash.ui.screen.backup.view.BackupTestSetup
import co.electriccoin.zcash.ui.screen.backup.view.LongBackupTestSetup
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule
import kotlin.test.Test
@ -29,8 +29,8 @@ class BackupIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(initialStage: BackupStage): BackupTestSetup {
return BackupTestSetup(
private fun newTestSetup(initialStage: BackupStage): LongBackupTestSetup {
return LongBackupTestSetup(
composeTestRule,
initialStage,
TestChoicesFixture.new(TestChoicesFixture.INITIAL_CHOICES)

View File

@ -9,7 +9,7 @@ import co.electriccoin.zcash.ui.screen.backup.state.BackupState
import co.electriccoin.zcash.ui.screen.backup.state.TestChoices
import java.util.concurrent.atomic.AtomicInteger
class BackupTestSetup(
class LongBackupTestSetup(
private val composeTestRule: ComposeContentTestRule,
initialStage: BackupStage,
private val initialChoices: TestChoices
@ -53,7 +53,7 @@ class BackupTestSetup(
@Suppress("TestFunctionName")
fun DefaultContent() {
ZcashTheme {
BackupWallet(
LongNewWalletBackup(
PersistableWalletFixture.new(),
state,
initialChoices,

View File

@ -21,12 +21,12 @@ import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class BackupViewTest : UiTestPrerequisites() {
class LongBackupViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(initialStage: BackupStage): BackupTestSetup {
return BackupTestSetup(composeTestRule, initialStage, TestChoicesFixture.new(mutableMapOf())).apply {
private fun newTestSetup(initialStage: BackupStage): LongBackupTestSetup {
return LongBackupTestSetup(composeTestRule, initialStage, TestChoicesFixture.new(mutableMapOf())).apply {
setDefaultContent()
}
}
@ -212,17 +212,18 @@ class BackupViewTest : UiTestPrerequisites() {
}
}
fun ComposeContentTestRule.clickCopyToBuffer() {
private fun ComposeContentTestRule.clickCopyToBuffer() {
// open menu
onNodeWithContentDescription(
getStringResource(R.string.new_wallet_toolbar_more_button_content_description)
).also { moreMenu ->
moreMenu.performClick()
// click menu button
onNodeWithText(
getStringResource(R.string.new_wallet_3_button_copy)
).also { menuButton ->
menuButton.performClick()
}
}
// click menu button
onNodeWithText(
getStringResource(R.string.new_wallet_3_button_copy)
).also { menuButton ->
menuButton.performClick()
}
}

View File

@ -19,7 +19,7 @@ import org.junit.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class BackupViewsSecuredScreenTest : UiTestPrerequisites() {
class LongBackupViewsSecuredScreenTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@ -54,7 +54,7 @@ class BackupViewsSecuredScreenTest : UiTestPrerequisites() {
composeTestRule.setContent {
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
ZcashTheme {
BackupWallet(
LongNewWalletBackup(
PersistableWalletFixture.new(),
state,
TestChoicesFixture.new(mutableMapOf()),

View File

@ -0,0 +1,44 @@
package co.electriccoin.zcash.ui.screen.backup.view
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicInteger
class ShortBackupTestSetup(
private val composeTestRule: ComposeContentTestRule,
) {
private val onCopyToClipboardCount = AtomicInteger(0)
private val onCompleteCallbackCount = AtomicInteger(0)
fun getOnCopyToClipboardCount(): Int {
composeTestRule.waitForIdle()
return onCopyToClipboardCount.get()
}
fun getOnCompleteCallbackCount(): Int {
composeTestRule.waitForIdle()
return onCompleteCallbackCount.get()
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
ZcashTheme {
ShortNewWalletBackup(
PersistableWalletFixture.new(),
onCopyToClipboard = { onCopyToClipboardCount.incrementAndGet() },
onComplete = { onCompleteCallbackCount.incrementAndGet() },
)
}
}
fun setDefaultContent() {
composeTestRule.setContent {
DefaultContent()
}
}
}

View File

@ -0,0 +1,64 @@
package co.electriccoin.zcash.ui.screen.backup.view
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class ShortBackupViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(): ShortBackupTestSetup {
return ShortBackupTestSetup(composeTestRule).apply {
setDefaultContent()
}
}
@Test
@MediumTest
fun copy_to_clipboard() {
val testSetup = newTestSetup()
composeTestRule.copyToClipboard()
assertEquals(1, testSetup.getOnCopyToClipboardCount())
}
@Test
@MediumTest
fun click_finish() {
val testSetup = newTestSetup()
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_short_button_finished)).also {
it.performClick()
}
assertEquals(0, testSetup.getOnCopyToClipboardCount())
assertEquals(1, testSetup.getOnCompleteCallbackCount())
}
}
private fun ComposeContentTestRule.copyToClipboard() {
// open menu
onNodeWithContentDescription(
getStringResource(R.string.new_wallet_toolbar_more_button_content_description)
).also { moreMenu ->
moreMenu.performClick()
}
// click menu button
onNodeWithText(
getStringResource(R.string.new_wallet_short_copy)
).also { menuButton ->
menuButton.performClick()
}
}

View File

@ -0,0 +1,55 @@
package co.electriccoin.zcash.ui.screen.backup.view
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.LocalScreenSecurity
import co.electriccoin.zcash.ui.common.ScreenSecurity
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class ShortBackupViewsSecuredScreenTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup() =
TestSetup(composeTestRule).apply {
setContentView()
}
@Test
@MediumTest
fun acquireScreenSecurity() = runTest {
val testSetup = newTestSetup()
assertEquals(1, testSetup.getSecureScreenCount())
}
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
private val screenSecurity = ScreenSecurity()
fun getSecureScreenCount() = screenSecurity.referenceCount.value
fun setContentView() {
composeTestRule.setContent {
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
ZcashTheme {
ShortNewWalletBackup(
PersistableWalletFixture.new(),
onCopyToClipboard = { },
onComplete = { }
)
}
}
}
}
}
}

View File

@ -5,21 +5,13 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
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.view.LongOnboarding
import java.util.concurrent.atomic.AtomicInteger
class OnboardingTestSetup(
class LongOnboardingTestSetup(
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)
@ -45,8 +37,7 @@ class OnboardingTestSetup(
@Suppress("TestFunctionName")
fun DefaultContent() {
ZcashTheme {
Onboarding(
isFullOnboardingEnabled,
LongOnboarding(
onboardingState,
isDebugMenuEnabled = false,
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() },

View File

@ -0,0 +1,44 @@
package co.electriccoin.zcash.ui.screen.onboarding
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.onboarding.view.ShortOnboarding
import java.util.concurrent.atomic.AtomicInteger
class ShortOnboardingTestSetup(
private val composeTestRule: ComposeContentTestRule,
) {
private val onCreateWalletCallbackCount = AtomicInteger(0)
private val onImportWalletCallbackCount = AtomicInteger(0)
fun getOnCreateWalletCallbackCount(): Int {
composeTestRule.waitForIdle()
return onCreateWalletCallbackCount.get()
}
fun getOnImportWalletCallbackCount(): Int {
composeTestRule.waitForIdle()
return onImportWalletCallbackCount.get()
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
ZcashTheme {
ShortOnboarding(
isDebugMenuEnabled = false,
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() },
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() },
// We aren't testing this because it is for debug builds only.
onFixtureWallet = {}
)
}
}
fun setDefaultContent() {
composeTestRule.setContent {
DefaultContent()
}
}
}

View File

@ -7,7 +7,7 @@ import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.UiTestingActivity
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingTestSetup
import co.electriccoin.zcash.ui.screen.onboarding.LongOnboardingTestSetup
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
@ -18,9 +18,8 @@ class OnboardingActivityTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createAndroidComposeRule<UiTestingActivity>()
private fun newTestSetup() = OnboardingTestSetup(
private fun newTestSetup() = LongOnboardingTestSetup(
composeTestRule,
isFullOnboardingEnabled = true,
OnboardingStage.ShieldedByDefault
)

View File

@ -7,7 +7,7 @@ import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingTestSetup
import co.electriccoin.zcash.ui.screen.onboarding.LongOnboardingTestSetup
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
@ -18,9 +18,8 @@ class OnboardingIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(initialStage: OnboardingStage) = OnboardingTestSetup(
private fun newTestSetup(initialStage: OnboardingStage) = LongOnboardingTestSetup(
composeTestRule,
isFullOnboardingEnabled = true,
initialStage
)

View File

@ -9,19 +9,19 @@ import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingTestSetup
import co.electriccoin.zcash.ui.screen.onboarding.LongOnboardingTestSetup
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class OnboardingViewTest : UiTestPrerequisites() {
class LongOnboardingViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(isFullOnboardingEnabled: Boolean = true, initialStage: OnboardingStage): OnboardingTestSetup {
return OnboardingTestSetup(composeTestRule, isFullOnboardingEnabled, initialStage).apply {
private fun newTestSetup(initialStage: OnboardingStage): LongOnboardingTestSetup {
return LongOnboardingTestSetup(composeTestRule, initialStage).apply {
setDefaultContent()
}
}
@ -180,36 +180,6 @@ 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() {

View File

@ -0,0 +1,68 @@
package co.electriccoin.zcash.ui.screen.onboarding.view
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.onboarding.ShortOnboardingTestSetup
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class ShortOnboardingViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(): ShortOnboardingTestSetup {
return ShortOnboardingTestSetup(composeTestRule).apply {
setDefaultContent()
}
}
@Test
@MediumTest
fun layout() {
newTestSetup()
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_short_import_existing_wallet)).also {
it.assertExists()
it.assertIsEnabled()
it.assertHasClickAction()
}
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_short_create_new_wallet)).also {
it.assertExists()
it.assertIsEnabled()
it.assertHasClickAction()
}
}
@Test
@MediumTest
fun click_create_wallet() {
val testSetup = newTestSetup()
val newWalletButton = composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_short_create_new_wallet))
newWalletButton.performClick()
assertEquals(1, testSetup.getOnCreateWalletCallbackCount())
assertEquals(0, testSetup.getOnImportWalletCallbackCount())
}
@Test
@MediumTest
fun click_import_wallet() {
val testSetup = newTestSetup()
val newWalletButton = composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_short_import_existing_wallet))
newWalletButton.performClick()
assertEquals(1, testSetup.getOnImportWalletCallbackCount())
assertEquals(0, testSetup.getOnCreateWalletCallbackCount())
}
}

View File

@ -24,7 +24,7 @@ import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Override
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.backup.WrapBackup
import co.electriccoin.zcash.ui.screen.backup.WrapNewWallet
import co.electriccoin.zcash.ui.screen.home.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -129,7 +129,7 @@ class MainActivity : ComponentActivity() {
WrapOnboarding()
}
is SecretState.NeedsBackup -> {
WrapBackup(
WrapNewWallet(
secretState.persistableWallet,
onBackupComplete = { walletViewModel.persistBackupComplete() }
)

View File

@ -9,7 +9,12 @@ object ConfigurationEntries {
/*
* 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)
val IS_SHORT_ONBOARDING_UX = BooleanConfigurationEntry(ConfigKey("is_short_onboarding_ux"), true)
/*
* The full new wallet flow is functional and tested, but it is disabled by default for an initially minimal feature set.
*/
val IS_SHORT_NEW_WALLET_BACKUP_UX = BooleanConfigurationEntry(ConfigKey("is_short_new_wallet_backup_ux"), true)
/*
* A troubleshooting step. If we fix our bugs, this should be unnecessary.

View File

@ -11,27 +11,48 @@ import androidx.compose.runtime.saveable.rememberSaveable
import cash.z.ecc.android.sdk.model.PersistableWallet
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.backup.ext.Saver
import co.electriccoin.zcash.ui.screen.backup.state.BackupState
import co.electriccoin.zcash.ui.screen.backup.state.TestChoices
import co.electriccoin.zcash.ui.screen.backup.view.BackupWallet
import co.electriccoin.zcash.ui.screen.backup.view.LongNewWalletBackup
import co.electriccoin.zcash.ui.screen.backup.view.ShortNewWalletBackup
@Composable
internal fun MainActivity.WrapBackup(
internal fun MainActivity.WrapNewWallet(
persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit
) {
WrapBackup(this, persistableWallet, onBackupComplete)
if (ConfigurationEntries.IS_SHORT_NEW_WALLET_BACKUP_UX.getValue(RemoteConfig.current)) {
WrapShortNewWallet(
persistableWallet,
onBackupComplete = onBackupComplete
)
} else {
WrapLongNewWallet(
persistableWallet,
onBackupComplete = onBackupComplete
)
}
}
@Composable
internal fun MainActivity.WrapLongNewWallet(
persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit
) {
WrapLongNewWallet(this, persistableWallet, onBackupComplete)
}
// This layer of indirection allows for activity re-creation tests
@Composable
internal fun WrapBackup(
internal fun WrapLongNewWallet(
activity: ComponentActivity,
persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit
) {
WrapBackup(
WrapLongNewWallet(
persistableWallet,
onCopyToClipboard = { copyToClipboard(activity.applicationContext, persistableWallet) },
onBackupComplete = onBackupComplete
@ -40,7 +61,7 @@ internal fun WrapBackup(
// This extra layer of indirection allows unit tests to validate the screen state retention.
@Composable
internal fun WrapBackup(
internal fun WrapLongNewWallet(
persistableWallet: PersistableWallet,
onCopyToClipboard: () -> Unit,
onBackupComplete: () -> Unit
@ -48,7 +69,7 @@ internal fun WrapBackup(
val testChoices by rememberSaveable(stateSaver = TestChoices.Saver) { mutableStateOf(TestChoices()) }
val backupState by rememberSaveable(stateSaver = BackupState.Saver) { mutableStateOf(BackupState()) }
BackupWallet(
LongNewWalletBackup(
persistableWallet,
backupState,
testChoices,
@ -58,7 +79,41 @@ internal fun WrapBackup(
)
}
fun copyToClipboard(context: Context, persistableWallet: PersistableWallet) {
@Composable
private fun MainActivity.WrapShortNewWallet(
persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit
) {
WrapShortNewWallet(this, persistableWallet, onBackupComplete)
}
@Composable
private fun WrapShortNewWallet(
activity: ComponentActivity,
persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit
) {
WrapShortNewWallet(
persistableWallet,
onCopyToClipboard = { copyToClipboard(activity.applicationContext, persistableWallet) },
onNewWalletComplete = onBackupComplete
)
}
@Composable
private fun WrapShortNewWallet(
persistableWallet: PersistableWallet,
onCopyToClipboard: () -> Unit,
onNewWalletComplete: () -> Unit
) {
ShortNewWalletBackup(
persistableWallet,
onCopyToClipboard = onCopyToClipboard,
onComplete = onNewWalletComplete,
)
}
internal fun copyToClipboard(context: Context, persistableWallet: PersistableWallet) {
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText(
context.getString(R.string.new_wallet_clipboard_tag),

View File

@ -3,6 +3,7 @@
package co.electriccoin.zcash.ui.screen.backup.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -22,6 +23,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
@ -38,6 +40,7 @@ import kotlinx.collections.immutable.ImmutableList
* @param onChoiceSelected Callback with the positional index of the item the user selected from [choices].
*/
@Composable
@Suppress("LongMethod")
fun ChipDropDown(
chipIndex: Index,
dropdownText: String,
@ -58,7 +61,11 @@ fun ChipDropDown(
tonalElevation = 8.dp,
shadowElevation = 8.dp
) {
Row(modifier = Modifier.padding(8.dp)) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = (chipIndex.value + 1).toString(),
style = ZcashTheme.typography.chipIndex,

View File

@ -65,7 +65,7 @@ import kotlinx.collections.immutable.toPersistentList
fun ComposablePreview() {
ZcashTheme(darkTheme = false) {
GradientSurface {
BackupWallet(
LongNewWalletBackup(
PersistableWalletFixture.new(),
BackupState(BackupStage.EducationOverview),
TestChoicesFixture.new(mutableMapOf()),
@ -83,7 +83,7 @@ fun ComposablePreview() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Suppress("LongParameterList")
fun BackupWallet(
fun LongNewWalletBackup(
wallet: PersistableWallet,
backupState: BackupState,
choices: TestChoices,

View File

@ -0,0 +1,159 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.backup.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
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.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
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.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.SecureScreen
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.ChipGrid
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.collections.immutable.toPersistentList
@Preview(device = Devices.PIXEL_4)
@Composable
fun ComposablePreviewShort() {
ZcashTheme(darkTheme = false) {
GradientSurface {
ShortNewWalletBackup(
PersistableWalletFixture.new(),
onCopyToClipboard = {},
onComplete = {},
)
}
}
}
/**
* @param onComplete Callback when the user has confirmed viewing the seed phrase.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShortNewWalletBackup(
wallet: PersistableWallet,
onCopyToClipboard: () -> Unit,
onComplete: () -> Unit,
) {
Scaffold(
topBar = {
ShortNewWalletTopAppBar(
onCopyToClipboard = onCopyToClipboard,
)
},
bottomBar = {
ShortNewWalletBottomNav(
onComplete = onComplete,
)
}
) { paddingValues ->
ShortNewWalletMainContent(
paddingValues = paddingValues,
wallet = wallet,
)
}
}
@Composable
private fun ShortNewWalletMainContent(
paddingValues: PaddingValues,
wallet: PersistableWallet,
) {
Column(
Modifier
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding()
)
) {
SeedPhrase(wallet)
}
}
@Composable
private fun SeedPhrase(persistableWallet: PersistableWallet) {
SecureScreen()
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = ZcashTheme.paddings.padding)
) {
Body(stringResource(R.string.new_wallet_short_body))
ChipGrid(persistableWallet.seedPhrase.split.toPersistentList())
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun ShortNewWalletTopAppBar(
onCopyToClipboard: () -> Unit,
) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.new_wallet_short_header)) },
actions = {
CopySeedMenu(onCopyToClipboard)
}
)
}
@Composable
private fun CopySeedMenu(onCopyToClipboard: () -> Unit) {
Column {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
Icons.Default.MoreVert,
contentDescription = stringResource(R.string.new_wallet_toolbar_more_button_content_description)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.new_wallet_short_copy)) },
onClick = {
expanded = false
onCopyToClipboard()
}
)
}
}
}
@Suppress("LongParameterList")
@Composable
private fun ShortNewWalletBottomNav(
onComplete: () -> Unit
) {
Column {
PrimaryButton(onClick = onComplete, text = stringResource(R.string.new_wallet_short_button_finished))
}
}

View File

@ -21,9 +21,8 @@ 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.view.LongOnboarding
import co.electriccoin.zcash.ui.screen.onboarding.view.ShortOnboarding
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
@ -51,55 +50,58 @@ 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(
isFullOnboardingEnabled = isFullOnboardingEnabled,
onboardingState = 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.setIsImporting(true)
},
onCreateWallet = {
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
persistExistingWalletWithSeedPhrase(
applicationContext,
walletViewModel,
SeedPhraseFixture.new()
)
return@Onboarding
}
walletViewModel.persistNewWallet()
},
onFixtureWallet = {
val onCreateWallet = {
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
persistExistingWalletWithSeedPhrase(
applicationContext,
walletViewModel,
SeedPhraseFixture.new()
)
} else {
walletViewModel.persistNewWallet()
}
)
}
val 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()
)
} else {
onboardingViewModel.setIsImporting(true)
}
}
val onFixtureWallet = {
persistExistingWalletWithSeedPhrase(
applicationContext,
walletViewModel,
SeedPhraseFixture.new()
)
}
if (ConfigurationEntries.IS_SHORT_ONBOARDING_UX.getValue(RemoteConfig.current)) {
ShortOnboarding(
isDebugMenuEnabled = isDebugMenuEnabled,
onImportWallet = onImportWallet,
onCreateWallet = onCreateWallet,
onFixtureWallet = onFixtureWallet
)
} else {
LongOnboarding(
onboardingState = onboardingViewModel.onboardingState,
isDebugMenuEnabled = isDebugMenuEnabled,
onImportWallet = onImportWallet,
onCreateWallet = onCreateWallet,
onFixtureWallet = onFixtureWallet
)
}
activity.reportFullyDrawn()
} else {

View File

@ -54,8 +54,7 @@ import co.electriccoin.zcash.ui.screen.onboarding.state.OnboardingState
fun ComposablePreview() {
ZcashTheme(darkTheme = true) {
GradientSurface {
Onboarding(
isFullOnboardingEnabled = true,
LongOnboarding(
OnboardingState(OnboardingStage.Wallet),
isDebugMenuEnabled = false,
onImportWallet = {},
@ -67,16 +66,13 @@ 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.
*/
@Composable
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongParameterList")
fun Onboarding(
isFullOnboardingEnabled: Boolean,
fun LongOnboarding(
onboardingState: OnboardingState,
isDebugMenuEnabled: Boolean,
onImportWallet: () -> Unit,
@ -86,7 +82,7 @@ fun Onboarding(
val currentStage = onboardingState.current.collectAsStateWithLifecycle().value
Scaffold(
topBar = {
OnboardingTopAppBar(isFullOnboardingEnabled, onboardingState, isDebugMenuEnabled, onFixtureWallet)
OnboardingTopAppBar(onboardingState, isDebugMenuEnabled, onFixtureWallet)
},
bottomBar = {
BottomNav(currentStage, onboardingState::goNext, onCreateWallet, onImportWallet)
@ -102,7 +98,6 @@ fun Onboarding(
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun OnboardingTopAppBar(
isFullOnboardingEnabled: Boolean,
onboardingState: OnboardingState,
isDebugMenuEnabled: Boolean,
onFixtureWallet: () -> Unit
@ -112,14 +107,12 @@ private fun OnboardingTopAppBar(
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
navigationIcon = {
if (isFullOnboardingEnabled) {
if (currentStage.hasPrevious()) {
IconButton(onboardingState::goPrevious) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.onboarding_back)
)
}
if (currentStage.hasPrevious()) {
IconButton(onboardingState::goPrevious) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.onboarding_back)
)
}
}
},

View File

@ -0,0 +1,143 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.onboarding.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
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.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview
@Composable
fun ShortOnboardingComposablePreview() {
ZcashTheme(darkTheme = true) {
GradientSurface {
ShortOnboarding(
isDebugMenuEnabled = false,
onImportWallet = {},
onCreateWallet = {},
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
@OptIn(ExperimentalMaterial3Api::class)
fun ShortOnboarding(
isDebugMenuEnabled: Boolean,
onImportWallet: () -> Unit,
onCreateWallet: () -> Unit,
onFixtureWallet: () -> Unit
) {
Scaffold(
topBar = {
OnboardingTopAppBar(isDebugMenuEnabled, onFixtureWallet)
}
) { paddingValues ->
OnboardingMainContent(
paddingValues,
onImportWallet = onImportWallet,
onCreateWallet = onCreateWallet
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun OnboardingTopAppBar(
isDebugMenuEnabled: Boolean,
onFixtureWallet: () -> Unit
) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
actions = {
if (isDebugMenuEnabled) {
DebugMenu(onFixtureWallet)
}
}
)
}
@Composable
private fun DebugMenu(onFixtureWallet: () -> Unit) {
Column {
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
)
}
}
}
@Composable
private fun OnboardingMainContent(
paddingValues: PaddingValues,
onImportWallet: () -> Unit,
onCreateWallet: () -> Unit,
) {
Column(
Modifier.padding(top = paddingValues.calculateTopPadding())
) {
Column(
Modifier
.padding(
start = ZcashTheme.paddings.padding,
end = ZcashTheme.paddings.padding,
bottom = paddingValues.calculateBottomPadding()
)
.fillMaxWidth()
) {
Header(
modifier = Modifier.padding(
top = ZcashTheme.paddings.padding,
bottom = ZcashTheme.paddings.paddingHalf
),
text = stringResource(R.string.onboarding_short_header)
)
PrimaryButton(onCreateWallet, stringResource(R.string.onboarding_short_create_new_wallet), Modifier.fillMaxWidth())
TertiaryButton(
onImportWallet,
stringResource(R.string.onboarding_short_import_existing_wallet),
Modifier.fillMaxWidth()
)
}
}
}

View File

@ -4,6 +4,11 @@
<string name="new_wallet_navigation_back_button_content_description">Back</string>
<string name="new_wallet_toolbar_more_button_content_description">More</string>
<string name="new_wallet_short_header">My secret phrase</string>
<string name="new_wallet_short_body">These words represent your funds and the security used to protect them.\n\nBack them up now!</string>
<string name="new_wallet_short_button_finished">I wrote it down</string>
<string name="new_wallet_short_copy">Copy to buffer</string>
<string name="new_wallet_1_header">First things first</string>
<string name="new_wallet_1_body_1">It is important to understand that you are in charge here. Great, right? YOU get to be the bank!</string>
<string name="new_wallet_1_body_2">But it also means that YOU are the customer, and you need to be self-reliant.\n\nSo how do you recover funds that youve hidden on a complete decentralized and private block-chain?</string>

View File

@ -0,0 +1,6 @@
<resources>
<string name="new_wallet_header">My secret phrase</string>
<string name="new_wallet_body">These words represent your funds and the security used to protect them.\n\nBack them up now!.</string>
<string name="new_wallet_button_finished">I wrote it down</string>
<string name="new_wallet_button_copy">Copy to buffer</string>
</resources>

View File

@ -18,4 +18,8 @@
<string name="onboarding_4_image_content_description"></string>
<string name="onboarding_4_create_new_wallet">Create New Wallet</string>
<string name="onboarding_4_import_existing_wallet">Import an Existing Wallet</string>
<string name="onboarding_short_header">Its time to set up your no-frills wallet, powered by Zcash</string>
<string name="onboarding_short_create_new_wallet">Create New Wallet</string>
<string name="onboarding_short_import_existing_wallet">Import an Existing Wallet</string>
</resources>

View File

@ -2,8 +2,8 @@
<resources>
<string name="receive_title">Receive</string>
<string name="receive_back_content_description">Back</string>
<string name="receive_qr_code_content_description">QR code for unified address</string>
<string name="receive_caption">Your UA Address</string>
<string name="receive_qr_code_content_description">QR code for address</string>
<string name="receive_caption">Your Address</string>
<string name="receive_see_address_details">See Address Details</string>
</resources>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="wallet_address_title">My wallet address</string>
<string name="wallet_address_title">My wallet addresses</string>
<string name="wallet_address_back_content_description">Back</string>
<string name="wallet_address_unified">Your Unified Address</string>
<string name="wallet_address_header_includes">which includes</string>
<string name="wallet_address_sapling">Shielded Sapling (NU1)</string>

View File

@ -146,7 +146,12 @@ class ScreenshotTest : UiTestPrerequisites() {
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None }
if (ConfigurationEntries.IS_FULL_ONBOARDING_ENABLED.getValue(emptyConfiguration)) {
if (ConfigurationEntries.IS_SHORT_ONBOARDING_UX.getValue(emptyConfiguration)) {
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_short_import_existing_wallet)).also {
it.assertExists()
it.performClick()
}
} else {
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists()
}
@ -155,11 +160,11 @@ class ScreenshotTest : UiTestPrerequisites() {
it.assertExists()
it.performClick()
}
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_import_existing_wallet)).also {
it.assertExists()
it.performClick()
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_import_existing_wallet)).also {
it.assertExists()
it.performClick()
}
}
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_title)).also {
@ -262,7 +267,16 @@ 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)) {
if (ConfigurationEntries.IS_SHORT_ONBOARDING_UX.getValue(emptyConfiguration)) {
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_short_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Onboarding 1")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_short_create_new_wallet)).also {
it.performClick()
}
} else {
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists()
}
@ -287,119 +301,131 @@ 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()
ScreenshotTest.takeScreenshot(tag, "Onboarding 4")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_header)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Onboarding 4")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_create_new_wallet)).also {
it.performClick()
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_create_new_wallet)).also {
it.performClick()
}
}
}
private fun backupScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.secretState.value is SecretState.NeedsBackup }
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 1")
if (ConfigurationEntries.IS_SHORT_ONBOARDING_UX.getValue(emptyConfiguration)) {
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_short_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 1")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_button)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_short_button_finished)).also {
it.assertExists()
it.performClick()
}
} else {
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 1")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 2")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_button)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_button)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 2")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 3")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_button)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 3")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 4")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performClick()
}
// Fail test first
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it[0].performScrollTo()
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 4")
it[1].performScrollTo()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
// Fail test first
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it[0].performScrollTo()
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
it[2].performScrollTo()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
it[1].performScrollTo()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
it[3].performScrollTo()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_ouch)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Backup Fail")
}
it[2].performScrollTo()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_4_button_retry))).also {
it.performClick()
}
it[3].performScrollTo()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_ouch)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Backup Fail")
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_header)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performClick()
}
composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_4_button_retry))).also {
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_header)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performClick()
}
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it.assertCountEquals(4)
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header)).also {
it.assertExists()
}
it[0].performScrollTo()
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it.assertCountEquals(4)
it[1].performScrollTo()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
it[0].performScrollTo()
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
it[2].performScrollTo()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
it[1].performScrollTo()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
it[3].performScrollTo()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
}
it[2].performScrollTo()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_5_body))).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Backup 5")
}
it[3].performScrollTo()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
}
composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_5_button_finished))).also {
it.assertExists()
it.performClick()
composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_5_body))).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Backup 5")
}
composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_5_button_finished))).also {
it.assertExists()
it.performClick()
}
}
}