[#1048] Seed Recovery Screen

[#1048] Seed Recovery Screen

Open Seed Recovery from the Settings screen

[#1014] Unify screen name with Figma

[#1048] UI + logic + tests

Improve click action effect on TopAppBar

[#1049] Add Changelog

Closes #1049

Update PR template
This commit is contained in:
Honza Rychnovský 2023-11-23 10:41:59 +01:00 committed by GitHub
parent cf45a0ef34
commit 771dc114da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 631 additions and 331 deletions

View File

@ -10,7 +10,7 @@
- [ ] Add **automated tests** as appropriate - [ ] Add **automated tests** as appropriate
- [ ] Update the [**manual tests**](../blob/main/docs/testing/manual_testing)[^2] as appropriate - [ ] Update the [**manual tests**](../blob/main/docs/testing/manual_testing)[^2] as appropriate
- [ ] Check the **code coverage**[^3] report for the automated tests - [ ] Check the **code coverage**[^3] report for the automated tests
- [ ] Update **documentation** as appropriate (e.g [README.md](../blob/main/README.md), and [**Architecture.md**](../blob/main/docs/Architecture.md), etc.) - [ ] Update **documentation** as appropriate (e.g [**README.md**](../blob/main/README.md), [**Architecture.md**](../blob/main/docs/Architecture.md), [**CHANGELOG.md**](../blob/main/CHANGELOG.md), etc.)
- [ ] **Run the app** and try the changes - [ ] **Run the app** and try the changes
- [ ] Pull in the latest changes from the **main** branch and **squash** your commits before assigning a reviewer[^4] - [ ] Pull in the latest changes from the **main** branch and **squash** your commits before assigning a reviewer[^4]
@ -23,7 +23,7 @@
- [ ] Perform an **ad hoc review**[^5] - [ ] Perform an **ad hoc review**[^5]
- [ ] Review the **automated tests** - [ ] Review the **automated tests**
- [ ] Review the **manual tests** - [ ] Review the **manual tests**
- [ ] Review the **documentation**, [**README.md**](../blob/main/README.md), and [**Architecture.md**](../blob/main/docs/Architecture.md) as appropriate - [ ] Review the **documentation**, [**README.md**](../blob/main/README.md), [**Architecture.md**](../blob/main/docs/Architecture.md), etc. as appropriate
- [ ] **Run the app** and try the changes[^6] - [ ] **Run the app** and try the changes[^6]
[^1]: _Code often looks different when reviewing the diff in a browser, making it easier to spot potential bugs._ [^1]: _Code often looks different when reviewing the diff in a browser, making it easier to spot potential bugs._

14
CHANGELOG.md Normal file
View File

@ -0,0 +1,14 @@
# Changelog
All notable changes to this application will be documented in this file.
Please be aware that this changelog primarily focuses on user-related modifications, emphasizing changes that can
directly impact users rather than highlighting other crucial architectural updates.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this application adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- The user interface for both the Recovery Seed screen within the New Wallet Screens flow and the one accessible from
Settings has been updated.

View File

@ -1,13 +1,16 @@
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
@ -143,8 +146,14 @@ fun Reference(
textAlign: TextAlign = TextAlign.Start, textAlign: TextAlign = TextAlign.Start,
onClick: () -> Unit onClick: () -> Unit
) { ) {
ClickableText( Box(
text = AnnotatedString(text), modifier = Modifier
.wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clickable { onClick() }
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
.merge( .merge(
TextStyle( TextStyle(
@ -153,12 +162,10 @@ fun Reference(
textDecoration = TextDecoration.Underline textDecoration = TextDecoration.Underline
) )
), ),
modifier = modifier, modifier = modifier
onClick = {
onClick()
}
) )
} }
}
/** /**
* Pass amount of ZECs you want to display and the component appends ZEC symbol to it. We're using * Pass amount of ZECs you want to display and the component appends ZEC symbol to it. We're using

View File

@ -1,10 +1,17 @@
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
@ -22,6 +29,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -209,24 +217,25 @@ fun SmallTopAppBar(
}, },
navigationIcon = { navigationIcon = {
backText?.let { backText?.let {
Box(
modifier = Modifier
.wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clickable { onBack?.run { onBack() } }
) {
Row( Row(
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingDefault),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton( Image(
onClick = if (onBack != null) {
onBack
} else {
{}
}
) {
Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.Filled.ArrowBack,
contentDescription = backContentDescriptionText contentDescription = backContentDescriptionText
) )
} Spacer(modifier = Modifier.size(size = ZcashTheme.dimens.spacingSmall))
Text(text = backText.uppercase()) Text(text = backText.uppercase())
} }
} }
}
}, },
actions = hamburgerMenuActions ?: regularActions ?: {}, actions = hamburgerMenuActions ?: regularActions ?: {},
modifier = modifier modifier = modifier

View File

@ -30,6 +30,7 @@ data class Dimens(
// TopAppBar: // TopAppBar:
val topAppBarZcashLogoHeight: Dp, val topAppBarZcashLogoHeight: Dp,
val topAppBarActionRippleCorner: Dp,
// In screen custom spacings: // In screen custom spacings:
val inScreenZcashLogoHeight: Dp, val inScreenZcashLogoHeight: Dp,
@ -52,6 +53,7 @@ private val defaultDimens = Dimens(
defaultButtonWidth = 230.dp, defaultButtonWidth = 230.dp,
defaultButtonHeight = 50.dp, defaultButtonHeight = 50.dp,
topAppBarZcashLogoHeight = 24.dp, topAppBarZcashLogoHeight = 24.dp,
topAppBarActionRippleCorner = 28.dp,
inScreenZcashLogoHeight = 100.dp, inScreenZcashLogoHeight = 100.dp,
inScreenZcashLogoWidth = 60.dp, inScreenZcashLogoWidth = 60.dp,
inScreenZcashTextLogoHeight = 30.dp, inScreenZcashTextLogoHeight = 30.dp,

View File

@ -40,7 +40,7 @@ android {
"src/main/res/ui/request", "src/main/res/ui/request",
"src/main/res/ui/restore", "src/main/res/ui/restore",
"src/main/res/ui/scan", "src/main/res/ui/scan",
"src/main/res/ui/seed", "src/main/res/ui/seed_recovery",
"src/main/res/ui/send", "src/main/res/ui/send",
"src/main/res/ui/settings", "src/main/res/ui/settings",
"src/main/res/ui/support", "src/main/res/ui/support",

View File

@ -10,7 +10,6 @@ import androidx.test.filters.SmallTest
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.AboutTag
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule import org.junit.Rule
import kotlin.test.Test import kotlin.test.Test

View File

@ -9,6 +9,7 @@ import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY
import co.electriccoin.zcash.ui.design.component.CommonTag import co.electriccoin.zcash.ui.design.component.CommonTag
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule import org.junit.Rule
@ -34,7 +35,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBirthdayCopyCount()) assertEquals(0, testSetup.getOnBirthdayCopyCount())
assertEquals(0, testSetup.getOnCompleteCount()) assertEquals(0, testSetup.getOnCompleteCount())
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_copy)).also { composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_recovery_copy)).also {
it.assertExists() it.assertExists()
} }
@ -57,7 +58,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
it.assertExists() it.assertExists()
} }
composeTestRule.onNodeWithTag(NewWalletRecoveryTag.WALLET_BIRTHDAY).also { composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also {
it.performScrollTo() it.performScrollTo()
it.assertExists() it.assertExists()
} }
@ -80,7 +81,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnSeedCopyCount()) assertEquals(0, testSetup.getOnSeedCopyCount())
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_copy)).also { menuButton -> composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_recovery_copy)).also { menuButton ->
menuButton.performClick() menuButton.performClick()
} }
@ -109,7 +110,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBirthdayCopyCount()) assertEquals(0, testSetup.getOnBirthdayCopyCount())
composeTestRule.onNodeWithTag(NewWalletRecoveryTag.WALLET_BIRTHDAY).also { composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also {
it.performScrollTo() it.performScrollTo()
it.performClick() it.performClick()
} }

View File

@ -158,7 +158,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
private fun copyToClipboard(context: Context, text: String) { private fun copyToClipboard(context: Context, text: String) {
val clipboardManager = context.getSystemService(ClipboardManager::class.java) val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText( val data = ClipData.newPlainText(
context.getString(R.string.new_wallet_seed_clipboard_tag), context.getString(R.string.new_wallet_recovery_seed_clipboard_tag),
text text
) )
clipboardManager.setPrimaryClip(data) clipboardManager.setPrimaryClip(data)

View File

@ -1,87 +0,0 @@
package co.electriccoin.zcash.ui.screen.seed.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.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
@OptIn(ExperimentalCoroutinesApi::class)
class SeedViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun back() = runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.seed_back_content_description)).also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun copyToClipboard() = runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getCopyToClipboardCount())
composeTestRule.onNodeWithText(getStringResource(R.string.seed_copy)).also {
it.performScrollTo()
it.performClick()
}
assertEquals(1, testSetup.getCopyToClipboardCount())
}
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
private val onBackCount = AtomicInteger(0)
private val onCopyToClipboardCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getCopyToClipboardCount(): Int {
composeTestRule.waitForIdle()
return onCopyToClipboardCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
Seed(
PersistableWalletFixture.new(),
onBack = {
onBackCount.incrementAndGet()
},
onCopyToClipboard = {
onCopyToClipboardCount.incrementAndGet()
}
)
}
}
}
}
}

View File

@ -0,0 +1,59 @@
package co.electriccoin.zcash.ui.screen.seedrecovery.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 SeedRecoveryBackupTestSetup(
private val composeTestRule: ComposeContentTestRule,
) {
private val onSeedCopyCount = AtomicInteger(0)
private val onBirthdayCopyCount = AtomicInteger(0)
private val onCompleteCallbackCount = AtomicInteger(0)
private val onBackCount = AtomicInteger(0)
fun getOnSeedCopyCount(): Int {
composeTestRule.waitForIdle()
return onSeedCopyCount.get()
}
fun getOnBirthdayCopyCount(): Int {
composeTestRule.waitForIdle()
return onBirthdayCopyCount.get()
}
fun getOnCompleteCount(): Int {
composeTestRule.waitForIdle()
return onCompleteCallbackCount.get()
}
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
ZcashTheme {
SeedRecovery(
PersistableWalletFixture.new(),
onBack = { onBackCount.incrementAndGet() },
onSeedCopy = { onSeedCopyCount.incrementAndGet() },
onBirthdayCopy = { onBirthdayCopyCount.incrementAndGet() },
onDone = { onCompleteCallbackCount.incrementAndGet() },
)
}
}
fun setDefaultContent() {
composeTestRule.setContent {
DefaultContent()
}
}
}

View File

@ -0,0 +1,164 @@
package co.electriccoin.zcash.ui.screen.seedrecovery.view
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY
import co.electriccoin.zcash.ui.design.component.CommonTag
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule
import kotlin.test.Test
import kotlin.test.assertEquals
class SeedRecoveryViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(): SeedRecoveryBackupTestSetup {
return SeedRecoveryBackupTestSetup(composeTestRule).apply {
setDefaultContent()
}
}
@Test
@MediumTest
fun default_ui_state_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnSeedCopyCount())
assertEquals(0, testSetup.getOnBirthdayCopyCount())
assertEquals(0, testSetup.getOnCompleteCount())
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.seed_recovery_back_content_description))
.also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.seed_recovery_copy)).also {
it.assertExists()
}
composeTestRule.onNodeWithContentDescription(
label = getStringResource(R.string.zcash_logo_content_description)
).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.seed_recovery_header)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.seed_recovery_description)).also {
it.assertExists()
}
composeTestRule.onNodeWithTag(CommonTag.CHIP_LAYOUT).also {
it.performScrollTo()
it.assertExists()
}
composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also {
it.performScrollTo()
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.seed_recovery_button_finished).uppercase())
.also {
it.performScrollTo()
it.assertExists()
}
assertEquals(0, testSetup.getOnSeedCopyCount())
assertEquals(0, testSetup.getOnBirthdayCopyCount())
assertEquals(0, testSetup.getOnCompleteCount())
assertEquals(0, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun back_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.seed_recovery_back_content_description))
.also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun copy_seed_to_clipboard_from_app_bar_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnSeedCopyCount())
composeTestRule.onNodeWithText(getStringResource(R.string.seed_recovery_copy)).also { menuButton ->
menuButton.performClick()
}
assertEquals(1, testSetup.getOnSeedCopyCount())
}
@Test
@MediumTest
fun copy_seed_to_clipboard_content_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnSeedCopyCount())
composeTestRule.onNodeWithTag(CommonTag.CHIP_LAYOUT).also {
it.performScrollTo()
it.performClick()
}
assertEquals(1, testSetup.getOnSeedCopyCount())
}
@Test
@MediumTest
fun copy_birthday_to_clipboard_content_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBirthdayCopyCount())
composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also {
it.performScrollTo()
it.performClick()
}
assertEquals(1, testSetup.getOnBirthdayCopyCount())
}
@Test
@MediumTest
fun click_finish_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnSeedCopyCount())
assertEquals(0, testSetup.getOnBirthdayCopyCount())
assertEquals(0, testSetup.getOnCompleteCount())
composeTestRule.onNodeWithText(
text = getStringResource(R.string.seed_recovery_button_finished),
ignoreCase = true
).also {
it.performScrollTo()
it.performClick()
}
assertEquals(0, testSetup.getOnSeedCopyCount())
assertEquals(0, testSetup.getOnBirthdayCopyCount())
assertEquals(1, testSetup.getOnCompleteCount())
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.seed.view package co.electriccoin.zcash.ui.screen.seedrecovery.view
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
@ -14,31 +14,38 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class SeedViewSecuredScreenTest : UiTestPrerequisites() { class SeedRecoveryViewsSecuredScreenTest : UiTestPrerequisites() {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
private fun newTestSetup() =
TestSetup(composeTestRule).apply {
setContentView()
}
@Test @Test
@MediumTest @MediumTest
fun acquireScreenSecurity() = runTest { fun acquireScreenSecurity() = runTest {
val testSetup = TestSetup(composeTestRule) val testSetup = newTestSetup()
assertEquals(1, testSetup.getSecureScreenCount()) assertEquals(1, testSetup.getSecureScreenCount())
} }
private class TestSetup(composeTestRule: ComposeContentTestRule) { private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
private val screenSecurity = ScreenSecurity() private val screenSecurity = ScreenSecurity()
fun getSecureScreenCount() = screenSecurity.referenceCount.value fun getSecureScreenCount() = screenSecurity.referenceCount.value
init { fun setContentView() {
composeTestRule.setContent { composeTestRule.setContent {
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) { CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
ZcashTheme { ZcashTheme {
Seed( SeedRecovery(
persistableWallet = PersistableWalletFixture.new(), PersistableWalletFixture.new(),
onBack = {}, onBack = {},
onCopyToClipboard = {} onSeedCopy = {},
onBirthdayCopy = {},
onDone = {}
) )
} }
} }

View File

@ -85,7 +85,7 @@ class SettingsViewTestSetup(
onBack = { onBack = {
onBackCount.incrementAndGet() onBackCount.incrementAndGet()
}, },
onBackup = { onSeedRecovery = {
onBackupCount.incrementAndGet() onBackupCount.incrementAndGet()
}, },
onDocumentation = { onDocumentation = {

View File

@ -18,7 +18,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.HOME
import co.electriccoin.zcash.ui.NavigationTargets.RECEIVE import co.electriccoin.zcash.ui.NavigationTargets.RECEIVE
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
import co.electriccoin.zcash.ui.NavigationTargets.SCAN import co.electriccoin.zcash.ui.NavigationTargets.SCAN
import co.electriccoin.zcash.ui.NavigationTargets.SEED import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
import co.electriccoin.zcash.ui.NavigationTargets.SEND import co.electriccoin.zcash.ui.NavigationTargets.SEND
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
@ -33,7 +33,7 @@ import co.electriccoin.zcash.ui.screen.home.WrapHome
import co.electriccoin.zcash.ui.screen.receive.WrapReceive import co.electriccoin.zcash.ui.screen.receive.WrapReceive
import co.electriccoin.zcash.ui.screen.request.WrapRequest import co.electriccoin.zcash.ui.screen.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.seed.WrapSeed import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery
import co.electriccoin.zcash.ui.screen.send.WrapSend import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.settings.WrapSettings import co.electriccoin.zcash.ui.screen.settings.WrapSettings
@ -55,7 +55,7 @@ internal fun MainActivity.Navigation() {
goAbout = { navController.navigateJustOnce(ABOUT) }, goAbout = { navController.navigateJustOnce(ABOUT) },
goHistory = { navController.navigateJustOnce(HISTORY) }, goHistory = { navController.navigateJustOnce(HISTORY) },
goReceive = { navController.navigateJustOnce(RECEIVE) }, goReceive = { navController.navigateJustOnce(RECEIVE) },
goSeedPhrase = { navController.navigateJustOnce(SEED) }, goSeedPhrase = { navController.navigateJustOnce(SEED_RECOVERY) },
goSend = { navController.navigateJustOnce(SEND) }, goSend = { navController.navigateJustOnce(SEND) },
goSettings = { navController.navigateJustOnce(SETTINGS) }, goSettings = { navController.navigateJustOnce(SETTINGS) },
goSupport = { navController.navigateJustOnce(SUPPORT) }, goSupport = { navController.navigateJustOnce(SUPPORT) },
@ -82,13 +82,19 @@ internal fun MainActivity.Navigation() {
}, },
goExportPrivateData = { goExportPrivateData = {
navController.navigateJustOnce(EXPORT_PRIVATE_DATA) navController.navigateJustOnce(EXPORT_PRIVATE_DATA)
},
goSeedRecovery = {
navController.navigateJustOnce(SEED_RECOVERY)
} }
) )
} }
composable(SEED) { composable(SEED_RECOVERY) {
WrapSeed( WrapSeedRecovery(
goBack = { goBack = {
navController.popBackStackJustOnce(SEED) navController.popBackStackJustOnce(SEED_RECOVERY)
},
onDone = {
navController.popBackStackJustOnce(SEED_RECOVERY)
} }
) )
} }
@ -193,7 +199,7 @@ object NavigationTargets {
const val RECEIVE = "receive" const val RECEIVE = "receive"
const val REQUEST = "request" const val REQUEST = "request"
const val SCAN = "scan" const val SCAN = "scan"
const val SEED = "seed" const val SEED_RECOVERY = "seed_recovery"
const val SEND = "send" const val SEND = "send"
const val SETTINGS = "settings" const val SETTINGS = "settings"
const val SUPPORT = "support" const val SUPPORT = "support"

View File

@ -0,0 +1,8 @@
package co.electriccoin.zcash.ui.common.test
/**
* These are only used for automated testing.
*/
object CommonTag {
const val WALLET_BIRTHDAY = "wallet_birthday"
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.about package co.electriccoin.zcash.ui.screen.about.view
/** /**
* These are only used for automated testing. * These are only used for automated testing.

View File

@ -38,7 +38,6 @@ import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.AboutTag
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@Preview("About") @Preview("About")

View File

@ -16,45 +16,28 @@ fun MainActivity.WrapNewWalletRecovery(
WrapNewWalletRecovery(this, persistableWallet, onBackupComplete) WrapNewWalletRecovery(this, persistableWallet, onBackupComplete)
} }
// This layer of indirection allows for activity re-creation tests
@Composable @Composable
private fun WrapNewWalletRecovery( private fun WrapNewWalletRecovery(
activity: ComponentActivity, activity: ComponentActivity,
persistableWallet: PersistableWallet, persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit onBackupComplete: () -> Unit
) { ) {
WrapNewWalletRecovery( NewWalletRecovery(
persistableWallet, persistableWallet,
onSeedCopyToClipboard = { onSeedCopy = {
ClipboardManagerUtil.copyToClipboard( ClipboardManagerUtil.copyToClipboard(
activity.applicationContext, activity.applicationContext,
activity.getString(R.string.new_wallet_seed_clipboard_tag), activity.getString(R.string.new_wallet_recovery_seed_clipboard_tag),
persistableWallet.seedPhrase.joinToString() persistableWallet.seedPhrase.joinToString()
) )
}, },
onBirthdayCopyToClipboard = { onBirthdayCopy = {
ClipboardManagerUtil.copyToClipboard( ClipboardManagerUtil.copyToClipboard(
activity.applicationContext, activity.applicationContext,
activity.getString(R.string.new_wallet_birthday_clipboard_tag), activity.getString(R.string.new_wallet_recovery_birthday_clipboard_tag),
persistableWallet.birthday?.value.toString() persistableWallet.birthday?.value.toString()
) )
}, },
onNewWalletComplete = onBackupComplete onComplete = onBackupComplete
)
}
// This extra layer of indirection allows unit tests to validate the screen state retention
@Composable
private fun WrapNewWalletRecovery(
persistableWallet: PersistableWallet,
onSeedCopyToClipboard: () -> Unit,
onBirthdayCopyToClipboard: () -> Unit,
onNewWalletComplete: () -> Unit
) {
NewWalletRecovery(
persistableWallet,
onSeedCopy = onSeedCopyToClipboard,
onBirthdayCopy = onBirthdayCopyToClipboard,
onComplete = onNewWalletComplete,
) )
} }

View File

@ -1,5 +1,3 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.newwalletrecovery.view package co.electriccoin.zcash.ui.screen.newwalletrecovery.view
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@ -32,6 +30,7 @@ import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.SecureScreen import co.electriccoin.zcash.ui.common.SecureScreen
import co.electriccoin.zcash.ui.common.shouldSecureScreen import co.electriccoin.zcash.ui.common.shouldSecureScreen
import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.BodySmall import co.electriccoin.zcash.ui.design.component.BodySmall
import co.electriccoin.zcash.ui.design.component.ChipGrid import co.electriccoin.zcash.ui.design.component.ChipGrid
@ -41,7 +40,6 @@ import co.electriccoin.zcash.ui.design.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.newwalletrecovery.view.NewWalletRecoveryTag.WALLET_BIRTHDAY
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@Preview(name = "NewWalletRecovery", device = Devices.PIXEL_4) @Preview(name = "NewWalletRecovery", device = Devices.PIXEL_4)
@ -115,7 +113,7 @@ private fun NewWalletRecoveryCopyToBufferMenuItem(
onCopyToClipboard: () -> Unit, onCopyToClipboard: () -> Unit,
) { ) {
Reference( Reference(
text = stringResource(id = R.string.new_wallet_copy), text = stringResource(id = R.string.new_wallet_recovery_copy),
onClick = onCopyToClipboard, onClick = onCopyToClipboard,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = modifier.then( modifier = modifier.then(
@ -224,7 +222,6 @@ private fun NewWalletRecoverySeedPhrase(
} }
} }
@Suppress("LongParameterList")
@Composable @Composable
private fun NewWalletRecoveryBottomNav( private fun NewWalletRecoveryBottomNav(
onComplete: () -> Unit, onComplete: () -> Unit,

View File

@ -1,128 +0,0 @@
package co.electriccoin.zcash.ui.screen.seed.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
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.ArrowBack
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.ui.Modifier
import androidx.compose.ui.res.stringResource
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.common.shouldSecureScreen
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.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.collections.immutable.toPersistentList
@Preview("Seed")
@Composable
private fun PreviewSeed() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Seed(
persistableWallet = PersistableWalletFixture.new(),
onBack = {},
onCopyToClipboard = {}
)
}
}
}
/*
* Note we have some things to determine regarding locking of the secrets for persistableWallet
* (e.g. seed phrase and spending keys) which should require additional authorization to view.
*/
@Composable
fun Seed(
persistableWallet: PersistableWallet,
onBack: () -> Unit,
onCopyToClipboard: () -> Unit
) {
if (shouldSecureScreen) {
SecureScreen()
}
Scaffold(topBar = {
SeedTopAppBar(onBack = onBack)
}) { paddingValues ->
SeedMainContent(
persistableWallet = persistableWallet,
onCopyToClipboard = onCopyToClipboard,
modifier = Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.spacingDefault,
end = ZcashTheme.dimens.spacingDefault
)
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun SeedTopAppBar(onBack: () -> Unit) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.seed_title)) },
navigationIcon = {
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.seed_back_content_description)
)
}
}
)
}
@Composable
private fun SeedMainContent(
persistableWallet: PersistableWallet,
onCopyToClipboard: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
Modifier
.fillMaxHeight()
.verticalScroll(
rememberScrollState()
)
.then(modifier)
) {
Body(stringResource(R.string.seed_body))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
ChipGrid(
wordList = persistableWallet.seedPhrase.split.toPersistentList(),
onGridClick = {}
)
TertiaryButton(
onClick = onCopyToClipboard,
text = stringResource(R.string.seed_copy),
outerPaddingValues = PaddingValues(
horizontal = ZcashTheme.dimens.spacingNone,
vertical = ZcashTheme.dimens.spacingSmall
)
)
}
}

View File

@ -1,6 +1,4 @@
@file:Suppress("ktlint:filename") package co.electriccoin.zcash.ui.screen.seedrecovery
package co.electriccoin.zcash.ui.screen.seed
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
@ -11,19 +9,21 @@ import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.seed.view.Seed import co.electriccoin.zcash.ui.screen.seedrecovery.view.SeedRecovery
@Composable @Composable
internal fun MainActivity.WrapSeed( internal fun MainActivity.WrapSeedRecovery(
goBack: () -> Unit goBack: () -> Unit,
onDone: () -> Unit
) { ) {
WrapSeed(this, goBack) WrapSeedRecovery(this, goBack, onDone)
} }
@Composable @Composable
private fun WrapSeed( private fun WrapSeedRecovery(
activity: ComponentActivity, activity: ComponentActivity,
goBack: () -> Unit goBack: () -> Unit,
onDone: () -> Unit
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
@ -40,16 +40,24 @@ private fun WrapSeed(
if (null == synchronizer || null == persistableWallet) { if (null == synchronizer || null == persistableWallet) {
// Display loading indicator // Display loading indicator
} else { } else {
Seed( SeedRecovery(
persistableWallet = persistableWallet, persistableWallet,
onBack = goBack, onBack = goBack,
onCopyToClipboard = { onSeedCopy = {
ClipboardManagerUtil.copyToClipboard( ClipboardManagerUtil.copyToClipboard(
activity.applicationContext, activity.applicationContext,
activity.getString(R.string.new_wallet_seed_clipboard_tag), activity.getString(R.string.seed_recovery_seed_clipboard_tag),
persistableWallet.seedPhrase.joinToString() persistableWallet.seedPhrase.joinToString()
) )
}, },
onBirthdayCopy = {
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.seed_recovery_birthday_clipboard_tag),
persistableWallet.birthday?.value.toString()
)
},
onDone = onDone
) )
} }
} }

View File

@ -0,0 +1,244 @@
package co.electriccoin.zcash.ui.screen.seedrecovery.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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.common.shouldSecureScreen
import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.BodySmall
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.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.collections.immutable.toPersistentList
@Preview(name = "SeedRecovery", device = Devices.PIXEL_4)
@Composable
private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
SeedRecovery(
PersistableWalletFixture.new(),
onBack = {},
onBirthdayCopy = {},
onDone = {},
onSeedCopy = {},
)
}
}
}
// TODO [#998]: Check and enhance screen dark mode
// TODO [#998]: https://github.com/Electric-Coin-Company/zashi-android/issues/998
/**
* @param onDone Callback when the user has confirmed viewing the seed phrase.
*/
@Composable
fun SeedRecovery(
wallet: PersistableWallet,
onBack: () -> Unit,
onBirthdayCopy: () -> Unit,
onDone: () -> Unit,
onSeedCopy: () -> Unit,
) {
Scaffold(
topBar = {
SeedRecoveryTopAppBar(
onBack = onBack,
onSeedCopy = onSeedCopy,
)
}
) { paddingValues ->
SeedRecoveryMainContent(
wallet = wallet,
onDone = onDone,
onSeedCopy = onSeedCopy,
onBirthdayCopy = onBirthdayCopy,
// Horizontal paddings will be part of each UI element to minimize a possible truncation on very
// small screens
modifier = Modifier.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding()
)
)
}
}
@Composable
private fun SeedRecoveryTopAppBar(
onBack: () -> Unit,
onSeedCopy: () -> Unit,
modifier: Modifier = Modifier,
) {
SmallTopAppBar(
modifier = modifier,
backText = stringResource(id = R.string.seed_recovery_back).uppercase(),
backContentDescriptionText = stringResource(R.string.seed_recovery_back_content_description),
onBack = onBack,
regularActions = {
SeedRecoveryCopyToBufferMenuItem(
onCopyToClipboard = onSeedCopy
)
}
)
}
@Composable
private fun SeedRecoveryCopyToBufferMenuItem(
modifier: Modifier = Modifier,
onCopyToClipboard: () -> Unit,
) {
Reference(
text = stringResource(id = R.string.seed_recovery_copy),
onClick = onCopyToClipboard,
textAlign = TextAlign.Center,
modifier = modifier.then(
Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
)
)
}
@Composable
private fun SeedRecoveryMainContent(
wallet: PersistableWallet,
onSeedCopy: () -> Unit,
onBirthdayCopy: () -> Unit,
onDone: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
TopScreenLogoTitle(
title = stringResource(R.string.seed_recovery_header),
logoContentDescription = stringResource(R.string.zcash_logo_content_description),
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingHuge)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
BodySmall(
text = stringResource(R.string.seed_recovery_description),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingHuge)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
SeedRecoverySeedPhrase(
persistableWallet = wallet,
onSeedCopy = onSeedCopy,
onBirthdayCopy = onBirthdayCopy,
)
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
SeedRecoveryBottomNav(
onDone = onDone,
modifier = Modifier
.padding(
bottom = ZcashTheme.dimens.spacingHuge
)
.fillMaxWidth(),
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SeedRecoverySeedPhrase(
persistableWallet: PersistableWallet,
onSeedCopy: () -> Unit,
onBirthdayCopy: () -> Unit,
) {
if (shouldSecureScreen) {
SecureScreen()
}
Column {
ChipGrid(
wordList = persistableWallet.seedPhrase.split.toPersistentList(),
onGridClick = onSeedCopy
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
persistableWallet.birthday?.let {
val interactionSource = remember { MutableInteractionSource() }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
BodySmall(
text = stringResource(R.string.seed_recovery_birthday_height, it.value),
modifier = Modifier
.testTag(WALLET_BIRTHDAY)
.padding(horizontal = ZcashTheme.dimens.spacingDefault)
.basicMarquee()
// Apply click callback to the text only as the wrapping layout can be much wider
.clickable(
interactionSource = interactionSource,
indication = null, // Disable ripple
onClick = onBirthdayCopy
)
)
}
}
}
}
@Suppress("LongParameterList")
@Composable
private fun SeedRecoveryBottomNav(
onDone: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryButton(onClick = onDone, text = stringResource(R.string.seed_recovery_button_finished))
}
}

View File

@ -20,12 +20,14 @@ internal fun MainActivity.WrapSettings(
goAbout: () -> Unit, goAbout: () -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
goExportPrivateData: () -> Unit, goExportPrivateData: () -> Unit,
goSeedRecovery: () -> Unit,
) { ) {
WrapSettings( WrapSettings(
activity = this, activity = this,
goAbout = goAbout, goAbout = goAbout,
goBack = goBack, goBack = goBack,
goExportPrivateData = goExportPrivateData goExportPrivateData = goExportPrivateData,
goSeedRecovery = goSeedRecovery
) )
} }
@ -35,6 +37,7 @@ private fun WrapSettings(
goBack: () -> Unit, goBack: () -> Unit,
goAbout: () -> Unit, goAbout: () -> Unit,
goExportPrivateData: () -> Unit, goExportPrivateData: () -> Unit,
goSeedRecovery: () -> Unit,
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>() val settingsViewModel by activity.viewModels<SettingsViewModel>()
@ -63,7 +66,7 @@ private fun WrapSettings(
isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current), isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current),
), ),
onBack = goBack, onBack = goBack,
onBackup = {}, onSeedRecovery = goSeedRecovery,
onDocumentation = {}, onDocumentation = {},
onPrivacyPolicy = {}, onPrivacyPolicy = {},
onFeedback = {}, onFeedback = {},

View File

@ -53,7 +53,7 @@ private fun PreviewSettings() {
isRescanEnabled = false isRescanEnabled = false
), ),
onBack = {}, onBack = {},
onBackup = {}, onSeedRecovery = {},
onDocumentation = {}, onDocumentation = {},
onPrivacyPolicy = {}, onPrivacyPolicy = {},
onFeedback = {}, onFeedback = {},
@ -73,7 +73,7 @@ private fun PreviewSettings() {
fun Settings( fun Settings(
troubleshootingParameters: TroubleshootingParameters, troubleshootingParameters: TroubleshootingParameters,
onBack: () -> Unit, onBack: () -> Unit,
onBackup: () -> Unit, onSeedRecovery: () -> Unit,
onDocumentation: () -> Unit, onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit, onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit, onFeedback: () -> Unit,
@ -105,7 +105,7 @@ fun Settings(
start = dimens.spacingHuge, start = dimens.spacingHuge,
end = dimens.spacingHuge end = dimens.spacingHuge
), ),
onBackup = onBackup, onSeedRecovery = onSeedRecovery,
onDocumentation = onDocumentation, onDocumentation = onDocumentation,
onPrivacyPolicy = onPrivacyPolicy, onPrivacyPolicy = onPrivacyPolicy,
onFeedback = onFeedback, onFeedback = onFeedback,
@ -229,7 +229,7 @@ private fun TroubleshootingMenu(
@Composable @Composable
@Suppress("LongParameterList", "LongMethod") @Suppress("LongParameterList", "LongMethod")
private fun SettingsMainContent( private fun SettingsMainContent(
onBackup: () -> Unit, onSeedRecovery: () -> Unit,
onDocumentation: () -> Unit, onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit, onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit, onFeedback: () -> Unit,
@ -245,7 +245,7 @@ private fun SettingsMainContent(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
PrimaryButton( PrimaryButton(
onClick = onBackup, onClick = onSeedRecovery,
text = stringResource(R.string.settings_backup_wallet), text = stringResource(R.string.settings_backup_wallet),
outerPaddingValues = PaddingValues( outerPaddingValues = PaddingValues(
horizontal = dimens.spacingNone, horizontal = dimens.spacingNone,

View File

@ -6,7 +6,7 @@
<string name="new_wallet_recovery_birthday_height" formatted="true">Wallet birthday height: <xliff:g example="419200" <string name="new_wallet_recovery_birthday_height" formatted="true">Wallet birthday height: <xliff:g example="419200"
id="birthday_height">%1$d</xliff:g></string> id="birthday_height">%1$d</xliff:g></string>
<string name="new_wallet_recovery_button_finished">I got it!</string> <string name="new_wallet_recovery_button_finished">I got it!</string>
<string name="new_wallet_copy">Tap to Copy</string> <string name="new_wallet_recovery_copy">Tap to Copy</string>
<string name="new_wallet_seed_clipboard_tag">Zcash Seed Phrase</string> <string name="new_wallet_recovery_seed_clipboard_tag">Zcash Seed Phrase</string>
<string name="new_wallet_birthday_clipboard_tag">Zcash Wallet Birthday</string> <string name="new_wallet_recovery_birthday_clipboard_tag">Zcash Wallet Birthday</string>
</resources> </resources>

View File

@ -1,9 +0,0 @@
<resources>
<string name="seed_title">My secret phrase</string>
<string name="seed_back_content_description">Back</string>
<string name="seed_header">Your Secret Recovery Phrase</string>
<string name="seed_body">These words represent your funds and the security used to protect them.</string>
<string name="seed_copy">Copy to buffer</string>
</resources>

View File

@ -0,0 +1,14 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="seed_recovery_back">Back</string>
<string name="seed_recovery_back_content_description">Back</string>
<string name="seed_recovery_header">Your secret recovery phrase</string>
<string name="seed_recovery_description">The following 24 words are the keys to your funds and are the only way to
recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a
place you trust and never share it with anyone!</string>
<string name="seed_recovery_birthday_height" formatted="true">Wallet birthday height: <xliff:g example="419200"
id="birthday_height">%1$d</xliff:g></string>
<string name="seed_recovery_button_finished">I got it!</string>
<string name="seed_recovery_copy">Tap to Copy</string>
<string name="seed_recovery_seed_clipboard_tag">Zcash Seed Phrase</string>
<string name="seed_recovery_birthday_clipboard_tag">Zcash Wallet Birthday</string>
</resources>

View File

@ -296,7 +296,7 @@ class ScreenshotTest : UiTestPrerequisites() {
// These are the hamburger menu items // These are the hamburger menu items
// We could manually click on each one, which is a better integration test but a worse screenshot test // We could manually click on each one, which is a better integration test but a worse screenshot test
navigateTo(NavigationTargets.SEED) navigateTo(NavigationTargets.SEED_RECOVERY)
seedScreenshots(resContext, tag, composeTestRule) seedScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.SETTINGS) navigateTo(NavigationTargets.SETTINGS)
@ -588,7 +588,7 @@ private fun aboutScreenshots(resContext: Context, tag: String, composeTestRule:
} }
private fun seedScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { private fun seedScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(resContext.getString(R.string.seed_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.seed_recovery_header))).also {
it.assertExists() it.assertExists()
} }