[#1059] Restore Seed Screen
* [#1059] Restore Seed Screen - Reworked UI according to Figma design - Closes #1059 * Changelog update
This commit is contained in:
parent
cb9d3cf70b
commit
b15c1e9063
|
@ -10,5 +10,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The user interface for both the Recovery Seed screen within the New Wallet Screens flow and the one accessible from
|
- Updated user interface of these screens:
|
||||||
Settings has been updated.
|
- New Wallet Recovery Seed screen accessible from onboarding
|
||||||
|
- Seed Recovery screen accessible from Settings
|
||||||
|
- Restore existing wallet accessible from onboarding
|
|
@ -76,15 +76,15 @@ fun PrimaryButton(
|
||||||
contentColor = textColor,
|
contentColor = textColor,
|
||||||
strokeColor = buttonColor,
|
strokeColor = buttonColor,
|
||||||
strokeWidth = 1.dp,
|
strokeWidth = 1.dp,
|
||||||
offsetX = ZcashTheme.dimens.shadowOffsetX,
|
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
||||||
offsetY = ZcashTheme.dimens.shadowOffsetY,
|
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
||||||
spread = ZcashTheme.dimens.shadowSpread,
|
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||||
)
|
)
|
||||||
.translationClick(
|
.translationClick(
|
||||||
translationX = ZcashTheme.dimens.shadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
||||||
translationY = ZcashTheme.dimens.shadowOffsetX + 6.dp
|
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||||
)
|
)
|
||||||
.defaultMinSize(ZcashTheme.dimens.defaultButtonWidth, ZcashTheme.dimens.defaultButtonHeight)
|
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||||
.border(1.dp, Color.Black),
|
.border(1.dp, Color.Black),
|
||||||
colors = buttonColors(
|
colors = buttonColors(
|
||||||
containerColor = buttonColor,
|
containerColor = buttonColor,
|
||||||
|
@ -124,15 +124,15 @@ fun SecondaryButton(
|
||||||
.shadow(
|
.shadow(
|
||||||
contentColor = textColor,
|
contentColor = textColor,
|
||||||
strokeColor = textColor,
|
strokeColor = textColor,
|
||||||
offsetX = ZcashTheme.dimens.shadowOffsetX,
|
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
||||||
offsetY = ZcashTheme.dimens.shadowOffsetY,
|
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
||||||
spread = ZcashTheme.dimens.shadowSpread,
|
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||||
)
|
)
|
||||||
.translationClick(
|
.translationClick(
|
||||||
translationX = ZcashTheme.dimens.shadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
||||||
translationY = ZcashTheme.dimens.shadowOffsetX + 6.dp
|
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||||
)
|
)
|
||||||
.defaultMinSize(ZcashTheme.dimens.defaultButtonWidth, ZcashTheme.dimens.defaultButtonHeight)
|
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||||
.border(1.dp, Color.Black),
|
.border(1.dp, Color.Black),
|
||||||
colors = buttonColors(
|
colors = buttonColors(
|
||||||
containerColor = buttonColor,
|
containerColor = buttonColor,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package co.electriccoin.zcash.ui.design.component
|
package co.electriccoin.zcash.ui.design.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
@ -10,7 +12,6 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import co.electriccoin.zcash.spackle.model.Index
|
import co.electriccoin.zcash.spackle.model.Index
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
|
||||||
|
@ -18,7 +19,15 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
@Composable
|
@Composable
|
||||||
private fun ComposableChipPreview() {
|
private fun ComposableChipPreview() {
|
||||||
ZcashTheme(forceDarkMode = false) {
|
ZcashTheme(forceDarkMode = false) {
|
||||||
Chip(Index(0), "edict")
|
Chip("route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ComposableChipIndexedPreview() {
|
||||||
|
ZcashTheme(forceDarkMode = false) {
|
||||||
|
ChipIndexed(Index(0), "edict")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +35,35 @@ private fun ComposableChipPreview() {
|
||||||
@Composable
|
@Composable
|
||||||
private fun ComposableLongChipPreview() {
|
private fun ComposableLongChipPreview() {
|
||||||
ZcashTheme(forceDarkMode = false) {
|
ZcashTheme(forceDarkMode = false) {
|
||||||
Chip(Index(1), "a_very_long_seed_word_that_does_not_fit_into_the_chip_and_thus_needs_to_be_truncated")
|
ChipIndexed(Index(1), "a_very_long_seed_word_that_does_not_fit_into_the_chip_and_thus_needs_to_be_truncated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ComposableChipOnSurfacePreview() {
|
||||||
|
ZcashTheme(forceDarkMode = false) {
|
||||||
|
ChipOnSurface("ribbon")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Chip(
|
fun Chip(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = modifier.then(Modifier.testTag(CommonTag.CHIP))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChipIndexed(
|
||||||
index: Index,
|
index: Index,
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
@ -47,22 +79,32 @@ fun Chip(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Chip(
|
fun ChipOnSurface(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
modifier = modifier.padding(4.dp),
|
modifier = modifier
|
||||||
|
.padding(horizontal = ZcashTheme.dimens.spacingTiny)
|
||||||
|
.border(
|
||||||
|
border = BorderStroke(
|
||||||
|
width = ZcashTheme.dimens.chipStroke,
|
||||||
|
color = ZcashTheme.colors.layoutStroke
|
||||||
|
)
|
||||||
|
),
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
shadowElevation = 8.dp
|
shadowElevation = ZcashTheme.dimens.chipShadowElevation,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSecondary,
|
color = MaterialTheme.colorScheme.onSecondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
|
.padding(
|
||||||
|
vertical = ZcashTheme.dimens.spacingSmall,
|
||||||
|
horizontal = ZcashTheme.dimens.spacingDefault
|
||||||
|
)
|
||||||
.testTag(CommonTag.CHIP)
|
.testTag(CommonTag.CHIP)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ fun ChipGrid(
|
||||||
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
|
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
|
||||||
) {
|
) {
|
||||||
chunk.forEachIndexed { subIndex, word ->
|
chunk.forEachIndexed { subIndex, word ->
|
||||||
Chip(
|
ChipIndexed(
|
||||||
index = Index(chunkIndex * CHIP_GRID_COLUMN_SIZE + subIndex),
|
index = Index(chunkIndex * CHIP_GRID_COLUMN_SIZE + subIndex),
|
||||||
text = word,
|
text = word,
|
||||||
modifier = Modifier.padding(ZcashTheme.dimens.spacingXtiny)
|
modifier = Modifier.padding(ZcashTheme.dimens.spacingXtiny)
|
||||||
|
|
|
@ -22,21 +22,31 @@ data class Dimens(
|
||||||
// List of custom spacings:
|
// List of custom spacings:
|
||||||
|
|
||||||
// Button:
|
// Button:
|
||||||
val shadowOffsetX: Dp,
|
val buttonShadowOffsetX: Dp,
|
||||||
val shadowOffsetY: Dp,
|
val buttonShadowOffsetY: Dp,
|
||||||
val shadowSpread: Dp,
|
val buttonShadowSpread: Dp,
|
||||||
val defaultButtonWidth: Dp,
|
val buttonWidth: Dp,
|
||||||
val defaultButtonHeight: Dp,
|
val buttonHeight: Dp,
|
||||||
|
|
||||||
|
// Chip
|
||||||
|
val chipShadowElevation: Dp,
|
||||||
|
val chipStroke: Dp,
|
||||||
|
|
||||||
// TopAppBar:
|
// TopAppBar:
|
||||||
val topAppBarZcashLogoHeight: Dp,
|
val topAppBarZcashLogoHeight: Dp,
|
||||||
val topAppBarActionRippleCorner: Dp,
|
val topAppBarActionRippleCorner: Dp,
|
||||||
|
|
||||||
|
// TextField:
|
||||||
|
val textFieldDefaultHeight: Dp,
|
||||||
|
|
||||||
|
// Any Layout:
|
||||||
|
val layoutStroke: Dp,
|
||||||
|
|
||||||
// Screen custom spacings:
|
// Screen custom spacings:
|
||||||
val inScreenZcashLogoHeight: Dp,
|
val inScreenZcashLogoHeight: Dp,
|
||||||
val inScreenZcashLogoWidth: Dp,
|
val inScreenZcashLogoWidth: Dp,
|
||||||
val inScreenZcashTextLogoHeight: Dp,
|
val inScreenZcashTextLogoHeight: Dp,
|
||||||
val screenHorizontalSpacing: Dp
|
val screenHorizontalSpacing: Dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val defaultDimens = Dimens(
|
private val defaultDimens = Dimens(
|
||||||
|
@ -48,13 +58,17 @@ private val defaultDimens = Dimens(
|
||||||
spacingLarge = 24.dp,
|
spacingLarge = 24.dp,
|
||||||
spacingXlarge = 32.dp,
|
spacingXlarge = 32.dp,
|
||||||
spacingHuge = 64.dp,
|
spacingHuge = 64.dp,
|
||||||
shadowOffsetX = 20.dp,
|
buttonShadowOffsetX = 20.dp,
|
||||||
shadowOffsetY = 20.dp,
|
buttonShadowOffsetY = 20.dp,
|
||||||
shadowSpread = 10.dp,
|
buttonShadowSpread = 10.dp,
|
||||||
defaultButtonWidth = 230.dp,
|
buttonWidth = 230.dp,
|
||||||
defaultButtonHeight = 50.dp,
|
buttonHeight = 50.dp,
|
||||||
|
chipShadowElevation = 4.dp,
|
||||||
|
chipStroke = 0.5.dp,
|
||||||
topAppBarZcashLogoHeight = 24.dp,
|
topAppBarZcashLogoHeight = 24.dp,
|
||||||
topAppBarActionRippleCorner = 28.dp,
|
topAppBarActionRippleCorner = 28.dp,
|
||||||
|
textFieldDefaultHeight = 215.dp,
|
||||||
|
layoutStroke = 1.dp,
|
||||||
inScreenZcashLogoHeight = 100.dp,
|
inScreenZcashLogoHeight = 100.dp,
|
||||||
inScreenZcashLogoWidth = 60.dp,
|
inScreenZcashLogoWidth = 60.dp,
|
||||||
inScreenZcashTextLogoHeight = 30.dp,
|
inScreenZcashTextLogoHeight = 30.dp,
|
||||||
|
|
|
@ -18,6 +18,8 @@ data class ExtendedColors(
|
||||||
val progressEnd: Color,
|
val progressEnd: Color,
|
||||||
val progressBackground: Color,
|
val progressBackground: Color,
|
||||||
val chipIndex: Color,
|
val chipIndex: Color,
|
||||||
|
val textFieldHint: Color,
|
||||||
|
val layoutStroke: Color,
|
||||||
val overlay: Color,
|
val overlay: Color,
|
||||||
val highlight: Color,
|
val highlight: Color,
|
||||||
val addressHighlightBorder: Color,
|
val addressHighlightBorder: Color,
|
||||||
|
|
|
@ -8,6 +8,9 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import co.electriccoin.zcash.ui.design.theme.ExtendedColors
|
import co.electriccoin.zcash.ui.design.theme.ExtendedColors
|
||||||
|
|
||||||
|
// TODO [#998]: Check and enhance screen dark mode
|
||||||
|
// TODO [#998]: https://github.com/Electric-Coin-Company/zashi-android/issues/998
|
||||||
|
|
||||||
internal object Dark {
|
internal object Dark {
|
||||||
val backgroundStart = Color(0xFF000000)
|
val backgroundStart = Color(0xFF000000)
|
||||||
val backgroundEnd = Color(0xFF000000)
|
val backgroundEnd = Color(0xFF000000)
|
||||||
|
@ -20,6 +23,8 @@ internal object Dark {
|
||||||
val textNavigationButton = Color.Black
|
val textNavigationButton = Color.Black
|
||||||
val textCaption = Color(0xFFFFFFFF)
|
val textCaption = Color(0xFFFFFFFF)
|
||||||
val textChipIndex = Color(0xFFFFB900)
|
val textChipIndex = Color(0xFFFFB900)
|
||||||
|
val textFieldHint = Color(0xFFB7B7B7)
|
||||||
|
val layoutStroke = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
val primaryButton = Color(0xFFFFFFFF)
|
val primaryButton = Color(0xFFFFFFFF)
|
||||||
val primaryButtonPressed = Color(0xFFFFFFFF)
|
val primaryButtonPressed = Color(0xFFFFFFFF)
|
||||||
|
@ -80,6 +85,8 @@ internal object Light {
|
||||||
val textTertiaryButton = Color(0xFF000000)
|
val textTertiaryButton = Color(0xFF000000)
|
||||||
val textCaption = Color(0xFF000000)
|
val textCaption = Color(0xFF000000)
|
||||||
val textChipIndex = Color(0xFFEE8592)
|
val textChipIndex = Color(0xFFEE8592)
|
||||||
|
val textFieldHint = Color(0xFFB7B7B7)
|
||||||
|
val layoutStroke = Color(0xFF000000)
|
||||||
|
|
||||||
// TODO [#159]: The button colors are wrong for light
|
// TODO [#159]: The button colors are wrong for light
|
||||||
// TODO [#159]: https://github.com/Electric-Coin-Company/zashi-android/issues/159
|
// TODO [#159]: https://github.com/Electric-Coin-Company/zashi-android/issues/159
|
||||||
|
@ -163,6 +170,8 @@ internal val DarkExtendedColorPalette = ExtendedColors(
|
||||||
progressEnd = Dark.progressEnd,
|
progressEnd = Dark.progressEnd,
|
||||||
progressBackground = Dark.progressBackground,
|
progressBackground = Dark.progressBackground,
|
||||||
chipIndex = Dark.textChipIndex,
|
chipIndex = Dark.textChipIndex,
|
||||||
|
textFieldHint = Dark.textFieldHint,
|
||||||
|
layoutStroke = Dark.layoutStroke,
|
||||||
overlay = Dark.overlay,
|
overlay = Dark.overlay,
|
||||||
highlight = Dark.highlight,
|
highlight = Dark.highlight,
|
||||||
addressHighlightBorder = Dark.addressHighlightBorder,
|
addressHighlightBorder = Dark.addressHighlightBorder,
|
||||||
|
@ -191,6 +200,8 @@ internal val LightExtendedColorPalette = ExtendedColors(
|
||||||
progressEnd = Light.progressEnd,
|
progressEnd = Light.progressEnd,
|
||||||
progressBackground = Light.progressBackground,
|
progressBackground = Light.progressBackground,
|
||||||
chipIndex = Light.textChipIndex,
|
chipIndex = Light.textChipIndex,
|
||||||
|
textFieldHint = Light.textFieldHint,
|
||||||
|
layoutStroke = Light.layoutStroke,
|
||||||
overlay = Light.overlay,
|
overlay = Light.overlay,
|
||||||
highlight = Light.highlight,
|
highlight = Light.highlight,
|
||||||
addressHighlightBorder = Light.addressHighlightBorder,
|
addressHighlightBorder = Light.addressHighlightBorder,
|
||||||
|
@ -221,6 +232,8 @@ internal val LocalExtendedColors = staticCompositionLocalOf {
|
||||||
progressEnd = Color.Unspecified,
|
progressEnd = Color.Unspecified,
|
||||||
progressBackground = Color.Unspecified,
|
progressBackground = Color.Unspecified,
|
||||||
chipIndex = Color.Unspecified,
|
chipIndex = Color.Unspecified,
|
||||||
|
textFieldHint = Color.Unspecified,
|
||||||
|
layoutStroke = Color.Unspecified,
|
||||||
overlay = Color.Unspecified,
|
overlay = Color.Unspecified,
|
||||||
highlight = Color.Unspecified,
|
highlight = Color.Unspecified,
|
||||||
addressHighlightBorder = Color.Unspecified,
|
addressHighlightBorder = Color.Unspecified,
|
||||||
|
|
|
@ -121,7 +121,9 @@ data class ExtendedTypography(
|
||||||
val aboutText: TextStyle,
|
val aboutText: TextStyle,
|
||||||
val buttonText: TextStyle,
|
val buttonText: TextStyle,
|
||||||
val checkboxText: TextStyle,
|
val checkboxText: TextStyle,
|
||||||
val securityWarningText: TextStyle
|
val securityWarningText: TextStyle,
|
||||||
|
val textFieldHint: TextStyle,
|
||||||
|
val textFieldValue: TextStyle,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("CompositionLocalAllowlist")
|
@Suppress("CompositionLocalAllowlist")
|
||||||
|
@ -160,6 +162,14 @@ val LocalExtendedTypography = staticCompositionLocalOf {
|
||||||
),
|
),
|
||||||
securityWarningText = PrimaryTypography.bodySmall.copy(
|
securityWarningText = PrimaryTypography.bodySmall.copy(
|
||||||
lineHeight = 22.32.sp
|
lineHeight = 22.32.sp
|
||||||
)
|
),
|
||||||
|
textFieldHint = PrimaryTypography.bodySmall.copy(
|
||||||
|
fontSize = 13.sp,
|
||||||
|
lineHeight = 15.73.sp,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
),
|
||||||
|
textFieldValue = PrimaryTypography.bodyLarge.copy(
|
||||||
|
fontSize = 17.sp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,15 +92,12 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(matcher = hasText("abandon", substring = true)).also {
|
||||||
matcher = hasText("abandon", substring = true) and hasTestTag(CommonTag.CHIP),
|
|
||||||
useUnmergedTree = true
|
|
||||||
).also {
|
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertTextEquals("")
|
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,24 +111,17 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertTextEquals("")
|
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(matcher = hasText(text = "abandon", substring = true))
|
||||||
matcher = hasText(text = "abandon", substring = true) and hasTestTag(CommonTag.CHIP),
|
|
||||||
useUnmergedTree = true
|
|
||||||
)
|
|
||||||
.also {
|
.also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
|
||||||
it.assertTextEquals("")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -140,7 +130,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
newTestSetup(initialWordsList = generateSequence { "abandon" }.take(SeedPhrase.SEED_PHRASE_SIZE).toList())
|
newTestSetup(initialWordsList = generateSequence { "abandon" }.take(SeedPhrase.SEED_PHRASE_SIZE).toList())
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(
|
composeTestRule.onNodeWithText(
|
||||||
text = getStringResource(R.string.restore_seed_button_restore),
|
text = getStringResource(R.string.restore_seed_button_next),
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
).also {
|
).also {
|
||||||
it.assertIsNotEnabled()
|
it.assertIsNotEnabled()
|
||||||
|
@ -156,7 +146,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
newTestSetup(initialWordsList = SeedPhraseFixture.new().split)
|
newTestSetup(initialWordsList = SeedPhraseFixture.new().split)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(
|
composeTestRule.onNodeWithText(
|
||||||
text = getStringResource(R.string.restore_seed_button_restore),
|
text = getStringResource(R.string.restore_seed_button_next),
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
).also {
|
).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
|
@ -169,7 +159,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
newTestSetup(initialWordsList = listOf("abandon"))
|
newTestSetup(initialWordsList = listOf("abandon"))
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(
|
||||||
matcher = hasText(text = "abandon", substring = true) and hasTestTag(CommonTag.CHIP),
|
matcher = hasText(text = "abandon", substring = true),
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
).also {
|
).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
|
|
|
@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.spackle.model.Index
|
import co.electriccoin.zcash.spackle.model.Index
|
||||||
import co.electriccoin.zcash.spackle.model.Progress
|
import co.electriccoin.zcash.spackle.model.Progress
|
||||||
import co.electriccoin.zcash.ui.design.component.Body
|
import co.electriccoin.zcash.ui.design.component.Body
|
||||||
import co.electriccoin.zcash.ui.design.component.Chip
|
import co.electriccoin.zcash.ui.design.component.ChipIndexed
|
||||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.Header
|
import co.electriccoin.zcash.ui.design.component.Header
|
||||||
import co.electriccoin.zcash.ui.design.component.NavigationButton
|
import co.electriccoin.zcash.ui.design.component.NavigationButton
|
||||||
|
@ -50,7 +50,7 @@ fun DesignGuide() {
|
||||||
Callout(Icons.Filled.Person, contentDescription = "Person")
|
Callout(Icons.Filled.Person, contentDescription = "Person")
|
||||||
Callout(Icons.Filled.List, contentDescription = "List")
|
Callout(Icons.Filled.List, contentDescription = "List")
|
||||||
PinkProgress(progress = Progress(Index(1), Index(4)), Modifier.fillMaxWidth())
|
PinkProgress(progress = Progress(Index(1), Index(4)), Modifier.fillMaxWidth())
|
||||||
Chip(Index(1), "edict")
|
ChipIndexed(Index(1), "edict")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,10 @@ import co.electriccoin.zcash.ui.common.first
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
internal sealed class ParseResult {
|
internal sealed class ParseResult {
|
||||||
object Continue : ParseResult()
|
object Continue : ParseResult() {
|
||||||
|
override fun toString() = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
data class Add(val words: List<String>) : ParseResult() {
|
data class Add(val words: List<String>) : ParseResult() {
|
||||||
// Override to prevent logging of user secrets
|
// Override to prevent logging of user secrets
|
||||||
override fun toString() = "Add"
|
override fun toString() = "Add"
|
||||||
|
@ -52,6 +55,10 @@ internal sealed class ParseResult {
|
||||||
return Warn(findSuggestions(trimmed, completeWordList))
|
return Warn(findSuggestions(trimmed, completeWordList))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "ParseResult()"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun findSuggestions(input: String, completeWordList: Set<String>): List<String> {
|
internal fun findSuggestions(input: String, completeWordList: Set<String>): List<String> {
|
||||||
|
|
|
@ -26,6 +26,16 @@ class WordList(initial: List<String> = emptyList()) {
|
||||||
mutableState.value = newList
|
mutableState.value = newList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeLast() {
|
||||||
|
val newList = if (mutableState.value.isNotEmpty()) {
|
||||||
|
current.value.subList(0, current.value.size - 1)
|
||||||
|
} else {
|
||||||
|
current.value
|
||||||
|
}.toPersistentList()
|
||||||
|
|
||||||
|
mutableState.value = newList
|
||||||
|
}
|
||||||
|
|
||||||
// Custom toString to prevent leaking word list
|
// Custom toString to prevent leaking word list
|
||||||
override fun toString() = "WordList"
|
override fun toString() = "WordList"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,14 @@
|
||||||
package co.electriccoin.zcash.ui.screen.restore.view
|
package co.electriccoin.zcash.ui.screen.restore.view
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
@ -18,75 +20,70 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
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.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
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.Alignment.Companion.CenterVertically
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
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
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.sdk.model.SeedPhraseValidation
|
import cash.z.ecc.sdk.model.SeedPhraseValidation
|
||||||
import co.electriccoin.zcash.spackle.model.Index
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
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.design.MINIMAL_WEIGHT
|
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||||
import co.electriccoin.zcash.ui.design.component.Body
|
import co.electriccoin.zcash.ui.design.component.Body
|
||||||
import co.electriccoin.zcash.ui.design.component.Chip
|
import co.electriccoin.zcash.ui.design.component.ChipOnSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.FormTextField
|
import co.electriccoin.zcash.ui.design.component.FormTextField
|
||||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.Header
|
import co.electriccoin.zcash.ui.design.component.Header
|
||||||
import co.electriccoin.zcash.ui.design.component.NavigationButton
|
|
||||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
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.TertiaryButton
|
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||||
|
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.design.theme.ZcashTheme.dimens
|
|
||||||
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
||||||
import co.electriccoin.zcash.ui.screen.restore.model.ParseResult
|
import co.electriccoin.zcash.ui.screen.restore.model.ParseResult
|
||||||
import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage
|
import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.RestoreState
|
import co.electriccoin.zcash.ui.screen.restore.state.RestoreState
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.wordValidation
|
import co.electriccoin.zcash.ui.screen.restore.state.wordValidation
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.persistentHashSetOf
|
import kotlinx.collections.immutable.persistentHashSetOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -151,12 +148,22 @@ fun RestoreWallet(
|
||||||
paste: () -> String?,
|
paste: () -> String?,
|
||||||
onFinished: () -> Unit
|
onFinished: () -> Unit
|
||||||
) {
|
) {
|
||||||
var textState by rememberSaveable { mutableStateOf("") }
|
val scope = rememberCoroutineScope()
|
||||||
val focusRequester = remember { FocusRequester() }
|
var text by rememberSaveable { mutableStateOf("") }
|
||||||
val parseResult = ParseResult.new(completeWordList, textState)
|
val parseResult = ParseResult.new(completeWordList, text)
|
||||||
|
|
||||||
val currentStage = restoreState.current.collectAsStateWithLifecycle().value
|
val currentStage = restoreState.current.collectAsStateWithLifecycle().value
|
||||||
|
|
||||||
|
var isSeedValid by rememberSaveable { mutableStateOf(false) }
|
||||||
|
// To avoid unnecessary recompositions that this flow produces
|
||||||
|
SideEffect {
|
||||||
|
scope.launch {
|
||||||
|
userWordList.wordValidation().collect {
|
||||||
|
isSeedValid = it is SeedPhraseValidation.Valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.navigationBarsPadding(),
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -169,7 +176,10 @@ fun RestoreWallet(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isShowClear = currentStage == RestoreStage.Seed,
|
isShowClear = currentStage == RestoreStage.Seed,
|
||||||
onClear = { userWordList.set(emptyList()) }
|
onClear = {
|
||||||
|
userWordList.set(emptyList())
|
||||||
|
text = ""
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
@ -177,9 +187,9 @@ fun RestoreWallet(
|
||||||
RestoreStage.Seed -> {
|
RestoreStage.Seed -> {
|
||||||
RestoreSeedBottomBar(
|
RestoreSeedBottomBar(
|
||||||
userWordList = userWordList,
|
userWordList = userWordList,
|
||||||
|
isSeedValid = isSeedValid,
|
||||||
parseResult = parseResult,
|
parseResult = parseResult,
|
||||||
setTextState = { textState = it },
|
setText = { text = it },
|
||||||
focusRequester = focusRequester,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
|
@ -197,12 +207,11 @@ fun RestoreWallet(
|
||||||
},
|
},
|
||||||
content = { paddingValues ->
|
content = { paddingValues ->
|
||||||
val commonModifier = Modifier
|
val commonModifier = Modifier
|
||||||
// We intentionally set the bottom smaller to save space in case of the software keyboard is visible
|
|
||||||
.padding(
|
.padding(
|
||||||
top = paddingValues.calculateTopPadding() + dimens.spacingDefault,
|
top = paddingValues.calculateTopPadding(),
|
||||||
bottom = paddingValues.calculateBottomPadding() + dimens.spacingSmall,
|
bottom = paddingValues.calculateBottomPadding(),
|
||||||
start = dimens.screenHorizontalSpacing,
|
start = ZcashTheme.dimens.screenHorizontalSpacing,
|
||||||
end = dimens.screenHorizontalSpacing
|
end = ZcashTheme.dimens.screenHorizontalSpacing
|
||||||
)
|
)
|
||||||
|
|
||||||
when (currentStage) {
|
when (currentStage) {
|
||||||
|
@ -210,12 +219,11 @@ fun RestoreWallet(
|
||||||
if (shouldSecureScreen) {
|
if (shouldSecureScreen) {
|
||||||
SecureScreen()
|
SecureScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
RestoreSeedMainContent(
|
RestoreSeedMainContent(
|
||||||
userWordList = userWordList,
|
userWordList = userWordList,
|
||||||
textState = textState,
|
isSeedValid = isSeedValid,
|
||||||
setTextState = { textState = it },
|
text = text,
|
||||||
focusRequester = focusRequester,
|
setText = { text = it },
|
||||||
parseResult = parseResult,
|
parseResult = parseResult,
|
||||||
paste = paste,
|
paste = paste,
|
||||||
goNext = { restoreState.goNext() },
|
goNext = { restoreState.goNext() },
|
||||||
|
@ -229,6 +237,9 @@ fun RestoreWallet(
|
||||||
setRestoreHeight = setRestoreHeight,
|
setRestoreHeight = setRestoreHeight,
|
||||||
onNext = { restoreState.goNext() },
|
onNext = { restoreState.goNext() },
|
||||||
modifier = commonModifier
|
modifier = commonModifier
|
||||||
|
.imePadding()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.animateContentSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RestoreStage.Complete -> {
|
RestoreStage.Complete -> {
|
||||||
|
@ -247,59 +258,72 @@ fun RestoreWallet(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
private fun RestoreTopAppBar(
|
private fun RestoreTopAppBar(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
onClear: () -> Unit,
|
||||||
isShowClear: Boolean,
|
isShowClear: Boolean,
|
||||||
onClear: () -> Unit
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
SmallTopAppBar(
|
||||||
title = { Text(text = stringResource(id = R.string.restore_title)) },
|
backText = stringResource(id = R.string.restore_back).uppercase(),
|
||||||
navigationIcon = {
|
backContentDescriptionText = stringResource(R.string.restore_back_content_description),
|
||||||
IconButton(
|
onBack = onBack,
|
||||||
onClick = onBack
|
regularActions = if (isShowClear) { {
|
||||||
) {
|
ClearSeedMenuItem(
|
||||||
Icon(
|
onSeedClear = onClear
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.restore_back_content_description)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
},
|
},
|
||||||
actions = {
|
modifier = modifier,
|
||||||
if (isShowClear) {
|
)
|
||||||
NavigationButton(onClick = onClear, stringResource(R.string.restore_button_clear))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ClearSeedMenuItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onSeedClear: () -> Unit,
|
||||||
|
) {
|
||||||
|
Reference(
|
||||||
|
text = stringResource(id = R.string.restore_button_clear),
|
||||||
|
onClick = onSeedClear,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = modifier.then(
|
||||||
|
Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO [#672]: Implement custom seed phrase pasting for wallet import
|
// TODO [#672]: Implement custom seed phrase pasting for wallet import
|
||||||
// TODO [#672]: https://github.com/Electric-Coin-Company/zashi-android/issues/672
|
// TODO [#672]: https://github.com/Electric-Coin-Company/zashi-android/issues/672
|
||||||
|
// TODO [#1060]: https://github.com/Electric-Coin-Company/zashi-android/issues/1060
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Suppress("UNUSED_PARAMETER", "LongParameterList")
|
@Suppress("UNUSED_PARAMETER", "LongParameterList", "LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
private fun RestoreSeedMainContent(
|
private fun RestoreSeedMainContent(
|
||||||
userWordList: WordList,
|
userWordList: WordList,
|
||||||
textState: String,
|
isSeedValid: Boolean,
|
||||||
setTextState: (String) -> Unit,
|
text: String,
|
||||||
focusRequester: FocusRequester,
|
setText: (String) -> Unit,
|
||||||
parseResult: ParseResult,
|
parseResult: ParseResult,
|
||||||
paste: () -> String?,
|
paste: () -> String?,
|
||||||
goNext: () -> Unit,
|
goNext: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val currentUserWordList = userWordList.current.collectAsStateWithLifecycle().value
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val textFieldScrollToHeight = rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
Twig.error { "TEST: $parseResult, $text" }
|
||||||
|
|
||||||
if (parseResult is ParseResult.Add) {
|
if (parseResult is ParseResult.Add) {
|
||||||
setTextState("")
|
setText("")
|
||||||
userWordList.append(parseResult.words)
|
userWordList.append(parseResult.words)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isSeedValid = userWordList.wordValidation().collectAsState(null).value is SeedPhraseValidation.Valid
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
@ -307,63 +331,84 @@ private fun RestoreSeedMainContent(
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Body(text = stringResource(id = R.string.restore_seed_instructions))
|
// Used to calculate necessary scroll to have the seed TextFiled visible
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.onSizeChanged { size ->
|
||||||
|
textFieldScrollToHeight.intValue = size.height
|
||||||
|
Twig.debug { "TextField scroll height: ${textFieldScrollToHeight.intValue}" }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
TopScreenLogoTitle(
|
||||||
|
title = stringResource(R.string.restore_title),
|
||||||
|
logoContentDescription = stringResource(R.string.zcash_logo_content_description),
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(dimens.spacingSmall))
|
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
ChipGridWithText(currentUserWordList)
|
Body(
|
||||||
|
text = stringResource(id = R.string.restore_seed_instructions),
|
||||||
if (!isSeedValid) {
|
textAlign = TextAlign.Center
|
||||||
NextWordTextField(
|
|
||||||
parseResult = parseResult,
|
|
||||||
text = textState,
|
|
||||||
setText = { setTextState(it) },
|
|
||||||
modifier = Modifier.focusRequester(focusRequester)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
|
SeedGridWithText(
|
||||||
|
text = text,
|
||||||
|
userWordList = userWordList,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
parseResult = parseResult,
|
||||||
|
setText = setText
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.weight(MINIMAL_WEIGHT)
|
.weight(MINIMAL_WEIGHT)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = goNext,
|
onClick = goNext,
|
||||||
text = stringResource(id = R.string.restore_seed_button_restore),
|
|
||||||
enabled = isSeedValid,
|
enabled = isSeedValid,
|
||||||
outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
|
text = stringResource(id = R.string.restore_seed_button_next),
|
||||||
|
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSeedValid) {
|
if (isSeedValid) {
|
||||||
// Hides the keyboard, making it easier for users to see the next button
|
// Clear focus and hide keyboard to make it easier for users to see the next button
|
||||||
LocalSoftwareKeyboardController.current?.hide()
|
LocalSoftwareKeyboardController.current?.hide()
|
||||||
|
LocalFocusManager.current.clearFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cause text field to refocus
|
|
||||||
DisposableEffect(parseResult) {
|
DisposableEffect(parseResult) {
|
||||||
|
// Causes the TextFiled to refocus
|
||||||
if (!isSeedValid) {
|
if (!isSeedValid) {
|
||||||
|
Twig.error { "NUT" }
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
// Causes scroll to the TextField after the first type action
|
||||||
|
if (text.isNotEmpty() && userWordList.current.value.isEmpty()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
scrollState.scrollTo(scrollState.maxValue)
|
scrollState.animateScrollTo(textFieldScrollToHeight.intValue)
|
||||||
}
|
}
|
||||||
onDispose { }
|
}
|
||||||
|
onDispose { /* Nothing to dispose */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RestoreSeedBottomBar(
|
private fun RestoreSeedBottomBar(
|
||||||
userWordList: WordList,
|
userWordList: WordList,
|
||||||
|
isSeedValid: Boolean,
|
||||||
parseResult: ParseResult,
|
parseResult: ParseResult,
|
||||||
setTextState: (String) -> Unit,
|
setText: (String) -> Unit,
|
||||||
focusRequester: FocusRequester,
|
modifier: Modifier = Modifier,
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
val isSeedValid = userWordList.wordValidation().collectAsState(null).value is SeedPhraseValidation.Valid
|
|
||||||
// Hide the field once the user has completed the seed phrase; if they need the field back then
|
// Hide the field once the user has completed the seed phrase; if they need the field back then
|
||||||
// the user can hit the clear button
|
// the user can hit the clear button
|
||||||
if (!isSeedValid) {
|
if (!isSeedValid) {
|
||||||
|
@ -374,81 +419,82 @@ private fun RestoreSeedBottomBar(
|
||||||
parseResult = parseResult,
|
parseResult = parseResult,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
// Note we don't set the top, as it's set by the confirm button above
|
|
||||||
.padding(
|
.padding(
|
||||||
bottom = dimens.spacingDefault,
|
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||||
start = dimens.spacingDefault,
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
end = dimens.spacingDefault
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Autocomplete(parseResult = parseResult, {
|
Autocomplete(parseResult = parseResult, {
|
||||||
setTextState("")
|
setText("")
|
||||||
userWordList.append(listOf(it))
|
userWordList.append(listOf(it))
|
||||||
focusRequester.requestFocus()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val CHIP_GRID_ROW_SIZE = 3
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChipGridWithText(
|
@Suppress("LongParameterList", "LongMethod")
|
||||||
userWordList: ImmutableList<String>
|
private fun SeedGridWithText(
|
||||||
) {
|
|
||||||
Column(Modifier.testTag(RestoreTag.CHIP_LAYOUT)) {
|
|
||||||
userWordList.chunked(CHIP_GRID_ROW_SIZE).forEachIndexed { chunkIndex, chunk ->
|
|
||||||
Row(Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
|
|
||||||
val remainder = (chunk.size % CHIP_GRID_ROW_SIZE)
|
|
||||||
|
|
||||||
val singleItemWeight = 1f / CHIP_GRID_ROW_SIZE
|
|
||||||
chunk.forEachIndexed { subIndex, word ->
|
|
||||||
Chip(
|
|
||||||
index = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex),
|
|
||||||
text = word,
|
|
||||||
modifier = Modifier.weight(singleItemWeight)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0 != remainder) {
|
|
||||||
Spacer(modifier = Modifier.weight((CHIP_GRID_ROW_SIZE - chunk.size) * singleItemWeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NextWordTextField(
|
|
||||||
parseResult: ParseResult,
|
|
||||||
text: String,
|
text: String,
|
||||||
setText: (String) -> Unit,
|
setText: (String) -> Unit,
|
||||||
|
userWordList: WordList,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
parseResult: ParseResult,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Surface(
|
val currentUserWordList = userWordList.current.collectAsStateWithLifecycle().value
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(dimens.spacingTiny)
|
|
||||||
.shadow(
|
|
||||||
elevation = 12.dp,
|
|
||||||
ambientColor = MaterialTheme.colorScheme.primary,
|
|
||||||
spotColor = MaterialTheme.colorScheme.primary
|
|
||||||
),
|
|
||||||
shape = RectangleShape,
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
|
|
||||||
|
val currentSeedText = currentUserWordList.run {
|
||||||
|
if (isEmpty()) {
|
||||||
|
text
|
||||||
|
} else {
|
||||||
|
joinToString(separator = " ", postfix = " ").plus(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.border(
|
||||||
|
border = BorderStroke(
|
||||||
|
width = ZcashTheme.dimens.layoutStroke,
|
||||||
|
color = ZcashTheme.colors.layoutStroke
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.defaultMinSize(minHeight = ZcashTheme.dimens.textFieldDefaultHeight)
|
||||||
|
.then(modifier)
|
||||||
|
.testTag(RestoreTag.CHIP_LAYOUT)
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
* Treat the user input as a password for more secure input, but disable the transformation
|
* Treat the user input as a password for more secure input, but disable the transformation
|
||||||
* to obscure typing.
|
* to obscure typing.
|
||||||
*/
|
*/
|
||||||
TextField(
|
TextField(
|
||||||
|
textStyle = ZcashTheme.extendedTypography.textFieldValue,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimens.spacingTiny)
|
.padding(ZcashTheme.dimens.spacingTiny)
|
||||||
.testTag(RestoreTag.SEED_WORD_TEXT_FIELD),
|
.testTag(RestoreTag.SEED_WORD_TEXT_FIELD)
|
||||||
value = text,
|
.focusRequester(focusRequester),
|
||||||
onValueChange = setText,
|
value = TextFieldValue(
|
||||||
|
text = currentSeedText,
|
||||||
|
selection = TextRange(index = currentSeedText.length)
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.restore_seed_hint),
|
||||||
|
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||||
|
color = ZcashTheme.colors.textFieldHint
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onValueChange = {
|
||||||
|
processTextInput(
|
||||||
|
currentSeedText = currentSeedText,
|
||||||
|
updateSeedText = it.text,
|
||||||
|
userWordList = userWordList,
|
||||||
|
setText = setText
|
||||||
|
)
|
||||||
|
},
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
KeyboardCapitalization.None,
|
KeyboardCapitalization.None,
|
||||||
autoCorrect = false,
|
autoCorrect = false,
|
||||||
|
@ -456,7 +502,6 @@ private fun NextWordTextField(
|
||||||
keyboardType = KeyboardType.Password
|
keyboardType = KeyboardType.Password
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(onAny = {}),
|
keyboardActions = KeyboardActions(onAny = {}),
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
isError = parseResult is ParseResult.Warn,
|
isError = parseResult is ParseResult.Warn,
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
|
@ -471,45 +516,82 @@ private fun NextWordTextField(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val pasteSeedWordRegex by lazy { Regex("\\s\\S") } // $NON-NLS
|
||||||
|
val whiteSpaceRegex by lazy { "\\s".toRegex() } // $NON-NLS
|
||||||
|
|
||||||
|
// TODO [#1061]: Restore screen input validation refactoring and adding tests
|
||||||
|
// TODO [#1061]: https://github.com/Electric-Coin-Company/zashi-android/issues/1061
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function processes the text from user input after every change. It compares with what is already typed in. It
|
||||||
|
* does a simple validation as well.
|
||||||
|
*
|
||||||
|
* @param currentSeedText Previously typed in text
|
||||||
|
* @param updateSeedText Updated text after every user input
|
||||||
|
* @param userWordList Validated type-safe list of seed words
|
||||||
|
* @param setText New text callback
|
||||||
|
*/
|
||||||
|
fun processTextInput(
|
||||||
|
currentSeedText: String,
|
||||||
|
updateSeedText: String,
|
||||||
|
userWordList: WordList,
|
||||||
|
setText: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val textDifference = if (updateSeedText.length > currentSeedText.length) {
|
||||||
|
updateSeedText.substring(currentSeedText.length)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
Twig.debug { "Text difference: $textDifference" }
|
||||||
|
|
||||||
|
if (whiteSpaceRegex.matches(textDifference)) {
|
||||||
|
// User tried to type a white space without confirming a valid seed word
|
||||||
|
} else if (pasteSeedWordRegex.containsMatchIn(textDifference)) {
|
||||||
|
// User pasted their seed from the device buffer
|
||||||
|
setText(updateSeedText)
|
||||||
|
} else if (updateSeedText < currentSeedText &&
|
||||||
|
whiteSpaceRegex.matches(currentSeedText.last().toString()) &&
|
||||||
|
currentSeedText.isNotEmpty()
|
||||||
|
) {
|
||||||
|
// User backspaced to a previously confirmed word - remove it
|
||||||
|
userWordList.removeLast()
|
||||||
|
} else {
|
||||||
|
// User typed in a character
|
||||||
|
setText(updateSeedText.split(whiteSpaceRegex).last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
private fun Autocomplete(
|
private fun Autocomplete(
|
||||||
parseResult: ParseResult,
|
parseResult: ParseResult,
|
||||||
onSuggestionSelected: (String) -> Unit,
|
onSuggestionSelected: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
// TODO [#1061]: Restore screen input validation refactoring and adding tests
|
||||||
|
// TODO [#1061]: https://github.com/Electric-Coin-Company/zashi-android/issues/1061
|
||||||
|
// Note that we currently do not use the highlighting of the suggestion bar
|
||||||
val (isHighlight, suggestions) = when (parseResult) {
|
val (isHighlight, suggestions) = when (parseResult) {
|
||||||
is ParseResult.Autocomplete -> {
|
is ParseResult.Autocomplete -> {
|
||||||
Pair(false, parseResult.suggestions)
|
Pair(false, parseResult.suggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
is ParseResult.Warn -> {
|
is ParseResult.Warn -> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Pair(false, null)
|
Pair(false, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suggestions?.let {
|
suggestions?.let {
|
||||||
val highlightModifier = if (isHighlight) {
|
|
||||||
modifier.border(2.dp, ZcashTheme.colors.highlight)
|
|
||||||
} else {
|
|
||||||
modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ModifierReused")
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = highlightModifier.testTag(RestoreTag.AUTOCOMPLETE_LAYOUT),
|
modifier = modifier
|
||||||
// Note we don't set the top, as it's set by the confirm button above
|
.testTag(RestoreTag.AUTOCOMPLETE_LAYOUT)
|
||||||
// And we also set the bottom smaller, as the keyboard will be always visible
|
.fillMaxWidth(),
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(all = ZcashTheme.dimens.spacingSmall),
|
||||||
bottom = dimens.spacingDefault,
|
horizontalArrangement = Arrangement.Absolute.Center
|
||||||
start = dimens.spacingDefault,
|
|
||||||
end = dimens.spacingSmall
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
items(it) {
|
items(it) {
|
||||||
Chip(
|
ChipOnSurface(
|
||||||
text = it,
|
text = it,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
.testTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
||||||
|
@ -527,15 +609,22 @@ private fun Warn(
|
||||||
) {
|
) {
|
||||||
if (parseResult is ParseResult.Warn) {
|
if (parseResult is ParseResult.Warn) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier.then(
|
||||||
|
Modifier.border(
|
||||||
|
border = BorderStroke(
|
||||||
|
width = ZcashTheme.dimens.chipStroke,
|
||||||
|
color = ZcashTheme.colors.layoutStroke
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
shadowElevation = 4.dp
|
shadowElevation = ZcashTheme.dimens.chipShadowElevation
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimens.spacingTiny),
|
.padding(ZcashTheme.dimens.spacingSmall),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
text = if (parseResult.suggestions.isEmpty()) {
|
text = if (parseResult.suggestions.isEmpty()) {
|
||||||
stringResource(id = R.string.restore_seed_warning_no_suggestions)
|
stringResource(id = R.string.restore_seed_warning_no_suggestions)
|
||||||
|
@ -569,11 +658,11 @@ private fun RestoreBirthday(
|
||||||
) {
|
) {
|
||||||
Header(stringResource(R.string.restore_birthday_header))
|
Header(stringResource(R.string.restore_birthday_header))
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
Body(stringResource(R.string.restore_birthday_body))
|
Body(stringResource(R.string.restore_birthday_body))
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
FormTextField(
|
FormTextField(
|
||||||
value = height,
|
value = height,
|
||||||
|
@ -583,7 +672,7 @@ private fun RestoreBirthday(
|
||||||
},
|
},
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(dimens.spacingTiny)
|
.padding(ZcashTheme.dimens.spacingTiny)
|
||||||
.testTag(RestoreTag.BIRTHDAY_TEXT_FIELD),
|
.testTag(RestoreTag.BIRTHDAY_TEXT_FIELD),
|
||||||
label = { Text(stringResource(id = R.string.restore_birthday_hint)) },
|
label = { Text(stringResource(id = R.string.restore_birthday_hint)) },
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
|
@ -602,6 +691,8 @@ private fun RestoreBirthday(
|
||||||
.weight(MINIMAL_WEIGHT)
|
.weight(MINIMAL_WEIGHT)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
val isBirthdayValid = height.toLongOrNull()?.let {
|
val isBirthdayValid = height.toLongOrNull()?.let {
|
||||||
it >= zcashNetwork.saplingActivationHeight.value
|
it >= zcashNetwork.saplingActivationHeight.value
|
||||||
} ?: false
|
} ?: false
|
||||||
|
@ -613,7 +704,7 @@ private fun RestoreBirthday(
|
||||||
},
|
},
|
||||||
text = stringResource(R.string.restore_birthday_button_restore),
|
text = stringResource(R.string.restore_birthday_button_restore),
|
||||||
enabled = isBirthdayValid,
|
enabled = isBirthdayValid,
|
||||||
outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
|
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
||||||
)
|
)
|
||||||
|
|
||||||
TertiaryButton(
|
TertiaryButton(
|
||||||
|
@ -622,7 +713,7 @@ private fun RestoreBirthday(
|
||||||
onNext()
|
onNext()
|
||||||
},
|
},
|
||||||
text = stringResource(R.string.restore_birthday_button_skip),
|
text = stringResource(R.string.restore_birthday_button_skip),
|
||||||
outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
|
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingDefault)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
@ -643,11 +734,11 @@ private fun RestoreComplete(
|
||||||
) {
|
) {
|
||||||
Header(stringResource(R.string.restore_complete_header))
|
Header(stringResource(R.string.restore_complete_header))
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
Body(stringResource(R.string.restore_complete_info))
|
Body(stringResource(R.string.restore_complete_info))
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -658,7 +749,7 @@ private fun RestoreComplete(
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onComplete,
|
onClick = onComplete,
|
||||||
text = stringResource(R.string.restore_button_see_wallet),
|
text = stringResource(R.string.restore_button_see_wallet),
|
||||||
outerPaddingValues = PaddingValues(top = dimens.spacingSmall)
|
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="restore_header">Create a wallet</string>
|
<string name="restore_back">Back</string>
|
||||||
<string name="restore_title">Wallet import</string>
|
|
||||||
<string name="restore_back_content_description">Back</string>
|
<string name="restore_back_content_description">Back</string>
|
||||||
|
<string name="restore_button_clear">Clear Seed</string>
|
||||||
|
|
||||||
|
<string name="restore_title">Enter secret recovery phrase</string>
|
||||||
|
<string name="restore_seed_instructions">Enter your 24-word seed phrase to restore the associated wallet.</string>
|
||||||
|
<string name="restore_seed_hint">Enter private seed here…</string>
|
||||||
|
<string name="restore_seed_button_next">Next</string>
|
||||||
|
|
||||||
<string name="restore_button_clear">Clear</string>
|
|
||||||
<string name="restore_seed_instructions">You can import your backed up wallet by entering your backup recovery phrase (aka seed phrase) now.</string>
|
|
||||||
<string name="restore_seed_button_restore">Restore wallet</string>
|
|
||||||
<string name="restore_seed_warning_suggestions">This word is not in the seed phrase dictionary. Please select the correct one from the suggestions.</string>
|
<string name="restore_seed_warning_suggestions">This word is not in the seed phrase dictionary. Please select the correct one from the suggestions.</string>
|
||||||
<string name="restore_seed_warning_no_suggestions">This word is not in the seed phrase dictionary.</string>
|
<string name="restore_seed_warning_no_suggestions">This word is not in the seed phrase dictionary.</string>
|
||||||
|
|
||||||
|
|
|
@ -211,7 +211,7 @@ class ScreenshotTest : UiTestPrerequisites() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_seed_button_restore)).also {
|
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_seed_button_next)).also {
|
||||||
it.performScrollTo()
|
it.performScrollTo()
|
||||||
|
|
||||||
// Even with waiting for the word list in the view model, there's some latency before the button is enabled
|
// Even with waiting for the word list in the view model, there's some latency before the button is enabled
|
||||||
|
|
Loading…
Reference in New Issue