[#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
- [ ] Update the [**manual tests**](../blob/main/docs/testing/manual_testing)[^2] as appropriate
- [ ] 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
- [ ] 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]
- [ ] Review the **automated 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]
[^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
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
@ -143,21 +146,25 @@ fun Reference(
textAlign: TextAlign = TextAlign.Start,
onClick: () -> Unit
) {
ClickableText(
text = AnnotatedString(text),
style = MaterialTheme.typography.bodyLarge
.merge(
TextStyle(
color = ZcashTheme.colors.reference,
textAlign = textAlign,
textDecoration = TextDecoration.Underline
)
),
modifier = modifier,
onClick = {
onClick()
}
)
Box(
modifier = Modifier
.wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clickable { onClick() }
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge
.merge(
TextStyle(
color = ZcashTheme.colors.reference,
textAlign = textAlign,
textDecoration = TextDecoration.Underline
)
),
modifier = modifier
)
}
}
/**

View File

@ -1,10 +1,17 @@
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.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
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.filled.ArrowBack
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -209,22 +217,23 @@ fun SmallTopAppBar(
},
navigationIcon = {
backText?.let {
Row(
verticalAlignment = Alignment.CenterVertically
Box(
modifier = Modifier
.wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clickable { onBack?.run { onBack() } }
) {
IconButton(
onClick = if (onBack != null) {
onBack
} else {
{}
}
Row(
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingDefault),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Image(
imageVector = Icons.Filled.ArrowBack,
contentDescription = backContentDescriptionText
)
Spacer(modifier = Modifier.size(size = ZcashTheme.dimens.spacingSmall))
Text(text = backText.uppercase())
}
Text(text = backText.uppercase())
}
}
},

View File

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

View File

@ -40,7 +40,7 @@ android {
"src/main/res/ui/request",
"src/main/res/ui/restore",
"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/settings",
"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.fixture.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.AboutTag
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule
import kotlin.test.Test

View File

@ -9,6 +9,7 @@ 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
@ -34,7 +35,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBirthdayCopyCount())
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()
}
@ -57,7 +58,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
it.assertExists()
}
composeTestRule.onNodeWithTag(NewWalletRecoveryTag.WALLET_BIRTHDAY).also {
composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also {
it.performScrollTo()
it.assertExists()
}
@ -80,7 +81,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
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()
}
@ -109,7 +110,7 @@ class NewWalletRecoveryViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBirthdayCopyCount())
composeTestRule.onNodeWithTag(NewWalletRecoveryTag.WALLET_BIRTHDAY).also {
composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also {
it.performScrollTo()
it.performClick()
}

View File

@ -158,7 +158,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
private fun copyToClipboard(context: Context, text: String) {
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText(
context.getString(R.string.new_wallet_seed_clipboard_tag),
context.getString(R.string.new_wallet_recovery_seed_clipboard_tag),
text
)
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.ui.test.junit4.ComposeContentTestRule
@ -14,31 +14,38 @@ import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class SeedViewSecuredScreenTest : UiTestPrerequisites() {
class SeedRecoveryViewsSecuredScreenTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup() =
TestSetup(composeTestRule).apply {
setContentView()
}
@Test
@MediumTest
fun acquireScreenSecurity() = runTest {
val testSetup = TestSetup(composeTestRule)
val testSetup = newTestSetup()
assertEquals(1, testSetup.getSecureScreenCount())
}
private class TestSetup(composeTestRule: ComposeContentTestRule) {
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
private val screenSecurity = ScreenSecurity()
fun getSecureScreenCount() = screenSecurity.referenceCount.value
init {
fun setContentView() {
composeTestRule.setContent {
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
ZcashTheme {
Seed(
persistableWallet = PersistableWalletFixture.new(),
SeedRecovery(
PersistableWalletFixture.new(),
onBack = {},
onCopyToClipboard = {}
onSeedCopy = {},
onBirthdayCopy = {},
onDone = {}
)
}
}

View File

@ -85,7 +85,7 @@ class SettingsViewTestSetup(
onBack = {
onBackCount.incrementAndGet()
},
onBackup = {
onSeedRecovery = {
onBackupCount.incrementAndGet()
},
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.REQUEST
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.SETTINGS
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.request.WrapRequest
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.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
@ -55,7 +55,7 @@ internal fun MainActivity.Navigation() {
goAbout = { navController.navigateJustOnce(ABOUT) },
goHistory = { navController.navigateJustOnce(HISTORY) },
goReceive = { navController.navigateJustOnce(RECEIVE) },
goSeedPhrase = { navController.navigateJustOnce(SEED) },
goSeedPhrase = { navController.navigateJustOnce(SEED_RECOVERY) },
goSend = { navController.navigateJustOnce(SEND) },
goSettings = { navController.navigateJustOnce(SETTINGS) },
goSupport = { navController.navigateJustOnce(SUPPORT) },
@ -82,13 +82,19 @@ internal fun MainActivity.Navigation() {
},
goExportPrivateData = {
navController.navigateJustOnce(EXPORT_PRIVATE_DATA)
},
goSeedRecovery = {
navController.navigateJustOnce(SEED_RECOVERY)
}
)
}
composable(SEED) {
WrapSeed(
composable(SEED_RECOVERY) {
WrapSeedRecovery(
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 REQUEST = "request"
const val SCAN = "scan"
const val SEED = "seed"
const val SEED_RECOVERY = "seed_recovery"
const val SEND = "send"
const val SETTINGS = "settings"
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.

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.fixture.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.about.AboutTag
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@Preview("About")

View File

@ -16,45 +16,28 @@ fun MainActivity.WrapNewWalletRecovery(
WrapNewWalletRecovery(this, persistableWallet, onBackupComplete)
}
// This layer of indirection allows for activity re-creation tests
@Composable
private fun WrapNewWalletRecovery(
activity: ComponentActivity,
persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit
) {
WrapNewWalletRecovery(
NewWalletRecovery(
persistableWallet,
onSeedCopyToClipboard = {
onSeedCopy = {
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.new_wallet_seed_clipboard_tag),
activity.getString(R.string.new_wallet_recovery_seed_clipboard_tag),
persistableWallet.seedPhrase.joinToString()
)
},
onBirthdayCopyToClipboard = {
onBirthdayCopy = {
ClipboardManagerUtil.copyToClipboard(
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()
)
},
onNewWalletComplete = 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,
onComplete = onBackupComplete
)
}

View File

@ -1,5 +1,3 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.newwalletrecovery.view
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.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
@ -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.TopScreenLogoTitle
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.newwalletrecovery.view.NewWalletRecoveryTag.WALLET_BIRTHDAY
import kotlinx.collections.immutable.toPersistentList
@Preview(name = "NewWalletRecovery", device = Devices.PIXEL_4)
@ -115,7 +113,7 @@ private fun NewWalletRecoveryCopyToBufferMenuItem(
onCopyToClipboard: () -> Unit,
) {
Reference(
text = stringResource(id = R.string.new_wallet_copy),
text = stringResource(id = R.string.new_wallet_recovery_copy),
onClick = onCopyToClipboard,
textAlign = TextAlign.Center,
modifier = modifier.then(
@ -224,7 +222,6 @@ private fun NewWalletRecoverySeedPhrase(
}
}
@Suppress("LongParameterList")
@Composable
private fun NewWalletRecoveryBottomNav(
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.seed
package co.electriccoin.zcash.ui.screen.seedrecovery
import androidx.activity.ComponentActivity
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.screen.home.viewmodel.SecretState
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
internal fun MainActivity.WrapSeed(
goBack: () -> Unit
internal fun MainActivity.WrapSeedRecovery(
goBack: () -> Unit,
onDone: () -> Unit
) {
WrapSeed(this, goBack)
WrapSeedRecovery(this, goBack, onDone)
}
@Composable
private fun WrapSeed(
private fun WrapSeedRecovery(
activity: ComponentActivity,
goBack: () -> Unit
goBack: () -> Unit,
onDone: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
@ -40,16 +40,24 @@ private fun WrapSeed(
if (null == synchronizer || null == persistableWallet) {
// Display loading indicator
} else {
Seed(
persistableWallet = persistableWallet,
SeedRecovery(
persistableWallet,
onBack = goBack,
onCopyToClipboard = {
onSeedCopy = {
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.new_wallet_seed_clipboard_tag),
activity.getString(R.string.seed_recovery_seed_clipboard_tag),
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,
goBack: () -> Unit,
goExportPrivateData: () -> Unit,
goSeedRecovery: () -> Unit,
) {
WrapSettings(
activity = this,
goAbout = goAbout,
goBack = goBack,
goExportPrivateData = goExportPrivateData
goExportPrivateData = goExportPrivateData,
goSeedRecovery = goSeedRecovery
)
}
@ -35,6 +37,7 @@ private fun WrapSettings(
goBack: () -> Unit,
goAbout: () -> Unit,
goExportPrivateData: () -> Unit,
goSeedRecovery: () -> Unit,
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
@ -63,7 +66,7 @@ private fun WrapSettings(
isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current),
),
onBack = goBack,
onBackup = {},
onSeedRecovery = goSeedRecovery,
onDocumentation = {},
onPrivacyPolicy = {},
onFeedback = {},

View File

@ -53,7 +53,7 @@ private fun PreviewSettings() {
isRescanEnabled = false
),
onBack = {},
onBackup = {},
onSeedRecovery = {},
onDocumentation = {},
onPrivacyPolicy = {},
onFeedback = {},
@ -73,7 +73,7 @@ private fun PreviewSettings() {
fun Settings(
troubleshootingParameters: TroubleshootingParameters,
onBack: () -> Unit,
onBackup: () -> Unit,
onSeedRecovery: () -> Unit,
onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit,
@ -105,7 +105,7 @@ fun Settings(
start = dimens.spacingHuge,
end = dimens.spacingHuge
),
onBackup = onBackup,
onSeedRecovery = onSeedRecovery,
onDocumentation = onDocumentation,
onPrivacyPolicy = onPrivacyPolicy,
onFeedback = onFeedback,
@ -229,7 +229,7 @@ private fun TroubleshootingMenu(
@Composable
@Suppress("LongParameterList", "LongMethod")
private fun SettingsMainContent(
onBackup: () -> Unit,
onSeedRecovery: () -> Unit,
onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit,
@ -245,7 +245,7 @@ private fun SettingsMainContent(
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryButton(
onClick = onBackup,
onClick = onSeedRecovery,
text = stringResource(R.string.settings_backup_wallet),
outerPaddingValues = PaddingValues(
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"
id="birthday_height">%1$d</xliff:g></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_seed_clipboard_tag">Zcash Seed Phrase</string>
<string name="new_wallet_birthday_clipboard_tag">Zcash Wallet Birthday</string>
<string name="new_wallet_recovery_copy">Tap to Copy</string>
<string name="new_wallet_recovery_seed_clipboard_tag">Zcash Seed Phrase</string>
<string name="new_wallet_recovery_birthday_clipboard_tag">Zcash Wallet Birthday</string>
</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
// 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)
navigateTo(NavigationTargets.SETTINGS)
@ -588,7 +588,7 @@ private fun aboutScreenshots(resContext: Context, tag: String, 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()
}