[#1064] Restore Seed-Birthday Screen
* [#1064] Restore Seed-Birthday Screen - Reworked Restore Seed-Birthday height screen - Contains UI, screen logic and tests - Closes #1064 * Update changelog * [#859] Fix failing Restore screenshot test Closes #859
This commit is contained in:
parent
0054588ec0
commit
3d33bb8ca5
|
@ -11,6 +11,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Updated user interface of these screens:
|
- Updated user interface of these screens:
|
||||||
- New Wallet Recovery Seed screen accessible from onboarding
|
- New Wallet Recovery Seed screen (accessible from onboarding)
|
||||||
- Seed Recovery screen accessible from Settings
|
- Seed Recovery screen (accessible from Settings)
|
||||||
- Restore existing wallet accessible from onboarding
|
- Restore Seed screen for an existing wallet (accessible from onboarding)
|
||||||
|
- Restore Seed Birthday Height screen for an existing wallet (accessible from onboarding)
|
|
@ -11,8 +11,10 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -20,6 +22,7 @@ fun FormTextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
textStyle: TextStyle = ZcashTheme.extendedTypography.textFieldValue,
|
||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable (() -> Unit)? = null,
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
@ -38,13 +41,16 @@ fun FormTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
label = label,
|
label = label,
|
||||||
|
textStyle = textStyle,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
modifier = if (withBorder) {
|
modifier = modifier.then(
|
||||||
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
|
if (withBorder) {
|
||||||
} else {
|
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
|
||||||
modifier
|
} else {
|
||||||
},
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
leadingIcon = leadingIcon,
|
leadingIcon = leadingIcon,
|
||||||
trailingIcon = trailingIcon,
|
trailingIcon = trailingIcon,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
|
|
|
@ -124,6 +124,7 @@ data class ExtendedTypography(
|
||||||
val securityWarningText: TextStyle,
|
val securityWarningText: TextStyle,
|
||||||
val textFieldHint: TextStyle,
|
val textFieldHint: TextStyle,
|
||||||
val textFieldValue: TextStyle,
|
val textFieldValue: TextStyle,
|
||||||
|
val textFieldBirthday: TextStyle,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("CompositionLocalAllowlist")
|
@Suppress("CompositionLocalAllowlist")
|
||||||
|
@ -171,5 +172,6 @@ val LocalExtendedTypography = staticCompositionLocalOf {
|
||||||
textFieldValue = PrimaryTypography.bodyLarge.copy(
|
textFieldValue = PrimaryTypography.bodyLarge.copy(
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
),
|
),
|
||||||
|
textFieldBirthday = SecondaryTypography.headlineMedium.copy(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,12 +185,16 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
initialWordsList = SeedPhraseFixture.new().split
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
|
composeTestRule.onNodeWithText(
|
||||||
|
text = getStringResource(R.string.restore_birthday_button_restore),
|
||||||
|
ignoreCase = true
|
||||||
|
).also {
|
||||||
|
it.assertIsEnabled()
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(testSetup.getRestoreHeight(), null)
|
assertEquals(testSetup.getRestoreHeight(), null)
|
||||||
assertEquals(testSetup.getStage(), RestoreStage.Complete)
|
assertEquals(1, testSetup.getOnFinishedCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -201,14 +205,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
initialWordsList = SeedPhraseFixture.new().split
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(
|
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
||||||
text = getStringResource(R.string.restore_birthday_button_restore),
|
|
||||||
ignoreCase = true
|
|
||||||
).also {
|
|
||||||
it.assertIsNotEnabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
|
|
||||||
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
|
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,34 +218,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(testSetup.getRestoreHeight(), ZcashNetwork.Mainnet.saplingActivationHeight)
|
assertEquals(testSetup.getRestoreHeight(), ZcashNetwork.Mainnet.saplingActivationHeight)
|
||||||
assertEquals(testSetup.getStage(), RestoreStage.Complete)
|
assertEquals(1, testSetup.getOnFinishedCount())
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@MediumTest
|
|
||||||
fun height_set_valid_but_skip() {
|
|
||||||
val testSetup = newTestSetup(
|
|
||||||
initialStage = RestoreStage.Birthday,
|
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
|
||||||
)
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(
|
|
||||||
text = getStringResource(R.string.restore_birthday_button_restore),
|
|
||||||
ignoreCase = true
|
|
||||||
).also {
|
|
||||||
it.assertIsNotEnabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
|
|
||||||
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
|
|
||||||
it.performClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNull(testSetup.getRestoreHeight())
|
|
||||||
assertEquals(testSetup.getStage(), RestoreStage.Complete)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -263,10 +233,10 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
text = getStringResource(R.string.restore_birthday_button_restore),
|
text = getStringResource(R.string.restore_birthday_button_restore),
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
).also {
|
).also {
|
||||||
it.assertIsNotEnabled()
|
it.assertIsEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
|
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
||||||
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
|
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,14 +245,11 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
).also {
|
).also {
|
||||||
it.assertIsNotEnabled()
|
it.assertIsNotEnabled()
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
|
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNull(testSetup.getRestoreHeight())
|
assertNull(testSetup.getRestoreHeight())
|
||||||
assertEquals(testSetup.getStage(), RestoreStage.Complete)
|
assertEquals(0, testSetup.getOnFinishedCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -293,14 +260,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
initialWordsList = SeedPhraseFixture.new().split
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(
|
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
||||||
text = getStringResource(R.string.restore_birthday_button_restore),
|
|
||||||
ignoreCase = true
|
|
||||||
).also {
|
|
||||||
it.assertIsNotEnabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
|
|
||||||
it.performTextInput("1.2")
|
it.performTextInput("1.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,27 +269,27 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
).also {
|
).also {
|
||||||
it.assertIsNotEnabled()
|
it.assertIsNotEnabled()
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
|
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNull(testSetup.getRestoreHeight())
|
assertNull(testSetup.getRestoreHeight())
|
||||||
assertEquals(testSetup.getStage(), RestoreStage.Complete)
|
assertEquals(0, testSetup.getOnFinishedCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun complete_click_take_to_wallet() {
|
fun complete_click_take_to_wallet() {
|
||||||
val testSetup = newTestSetup(
|
val testSetup = newTestSetup(
|
||||||
initialStage = RestoreStage.Complete,
|
initialStage = RestoreStage.Birthday,
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
initialWordsList = SeedPhraseFixture.new().split
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(0, testSetup.getOnFinishedCount())
|
assertEquals(0, testSetup.getOnFinishedCount())
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.restore_button_see_wallet), ignoreCase = true).also {
|
composeTestRule.onNodeWithText(
|
||||||
|
text = getStringResource(R.string.restore_birthday_button_restore),
|
||||||
|
ignoreCase = true
|
||||||
|
).also {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,26 +335,6 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
assertEquals(0, testSetup.getOnBackCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@MediumTest
|
|
||||||
fun back_from_complete() {
|
|
||||||
val testSetup = newTestSetup(
|
|
||||||
initialStage = RestoreStage.Complete,
|
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription(
|
|
||||||
getStringResource(R.string.restore_back_content_description)
|
|
||||||
).also {
|
|
||||||
it.performClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(testSetup.getStage(), RestoreStage.Birthday)
|
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newTestSetup(
|
private fun newTestSetup(
|
||||||
initialStage: RestoreStage = RestoreStage.Seed,
|
initialStage: RestoreStage = RestoreStage.Seed,
|
||||||
initialWordsList: List<String> = emptyList()
|
initialWordsList: List<String> = emptyList()
|
||||||
|
|
|
@ -4,8 +4,7 @@ enum class RestoreStage {
|
||||||
// Note: the ordinal order is used to manage progression through each stage
|
// Note: the ordinal order is used to manage progression through each stage
|
||||||
// so be careful if reordering these
|
// so be careful if reordering these
|
||||||
Seed,
|
Seed,
|
||||||
Birthday,
|
Birthday;
|
||||||
Complete;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see getPrevious
|
* @see getPrevious
|
||||||
|
@ -15,15 +14,15 @@ enum class RestoreStage {
|
||||||
/**
|
/**
|
||||||
* @see getNext
|
* @see getNext
|
||||||
*/
|
*/
|
||||||
fun hasNext() = ordinal < values().size - 1
|
fun hasNext() = ordinal < entries.size - 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Previous item in ordinal order. Returns the first item when it cannot go further back.
|
* @return Previous item in ordinal order. Returns the first item when it cannot go further back.
|
||||||
*/
|
*/
|
||||||
fun getPrevious() = values()[maxOf(0, ordinal - 1)]
|
fun getPrevious() = entries[maxOf(0, ordinal - 1)]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
|
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
|
||||||
*/
|
*/
|
||||||
fun getNext() = values()[minOf(values().size - 1, ordinal + 1)]
|
fun getNext() = entries[minOf(entries.size - 1, ordinal + 1)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
@ -71,11 +72,9 @@ import co.electriccoin.zcash.ui.design.component.Body
|
||||||
import co.electriccoin.zcash.ui.design.component.ChipOnSurface
|
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.PrimaryButton
|
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||||
import co.electriccoin.zcash.ui.design.component.Reference
|
import co.electriccoin.zcash.ui.design.component.Reference
|
||||||
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
|
||||||
import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle
|
import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
||||||
|
@ -88,9 +87,9 @@ import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.persistentHashSetOf
|
import kotlinx.collections.immutable.persistentHashSetOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Preview("Restore Wallet")
|
@Preview("Restore Seed")
|
||||||
@Composable
|
@Composable
|
||||||
private fun PreviewRestore() {
|
private fun PreviewRestoreSeed() {
|
||||||
ZcashTheme(forceDarkMode = false) {
|
ZcashTheme(forceDarkMode = false) {
|
||||||
GradientSurface {
|
GradientSurface {
|
||||||
RestoreWallet(
|
RestoreWallet(
|
||||||
|
@ -119,12 +118,31 @@ private fun PreviewRestore() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview("Restore Complete")
|
@Preview("Restore Seed Birthday")
|
||||||
@Composable
|
@Composable
|
||||||
private fun PreviewRestoreComplete() {
|
private fun PreviewRestoreBirthday() {
|
||||||
ZcashTheme(forceDarkMode = false) {
|
ZcashTheme(forceDarkMode = false) {
|
||||||
RestoreComplete(
|
RestoreWallet(
|
||||||
onComplete = {}
|
ZcashNetwork.Mainnet,
|
||||||
|
restoreState = RestoreState(RestoreStage.Birthday),
|
||||||
|
completeWordList = persistentHashSetOf(
|
||||||
|
"abandon",
|
||||||
|
"ability",
|
||||||
|
"able",
|
||||||
|
"about",
|
||||||
|
"above",
|
||||||
|
"absent",
|
||||||
|
"absorb",
|
||||||
|
"abstract",
|
||||||
|
"rib",
|
||||||
|
"ribbon"
|
||||||
|
),
|
||||||
|
userWordList = WordList(listOf("abandon", "absorb")),
|
||||||
|
restoreHeight = null,
|
||||||
|
setRestoreHeight = {},
|
||||||
|
onBack = {},
|
||||||
|
paste = { "" },
|
||||||
|
onFinished = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,7 +153,6 @@ private fun PreviewRestoreComplete() {
|
||||||
* @param restoreHeight A null height indicates no user input.
|
* @param restoreHeight A null height indicates no user input.
|
||||||
*/
|
*/
|
||||||
@Suppress("LongParameterList", "LongMethod")
|
@Suppress("LongParameterList", "LongMethod")
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RestoreWallet(
|
fun RestoreWallet(
|
||||||
zcashNetwork: ZcashNetwork,
|
zcashNetwork: ZcashNetwork,
|
||||||
|
@ -167,20 +184,28 @@ fun RestoreWallet(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.navigationBarsPadding(),
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
topBar = {
|
topBar = {
|
||||||
RestoreTopAppBar(
|
when (currentStage) {
|
||||||
onBack = {
|
RestoreStage.Seed -> {
|
||||||
if (currentStage.hasPrevious()) {
|
RestoreSeedTopAppBar(
|
||||||
restoreState.goPrevious()
|
onBack = onBack,
|
||||||
} else {
|
onClear = {
|
||||||
onBack()
|
userWordList.set(emptyList())
|
||||||
}
|
text = ""
|
||||||
},
|
}
|
||||||
isShowClear = currentStage == RestoreStage.Seed,
|
)
|
||||||
onClear = {
|
|
||||||
userWordList.set(emptyList())
|
|
||||||
text = ""
|
|
||||||
}
|
}
|
||||||
)
|
RestoreStage.Birthday -> {
|
||||||
|
RestoreSeedBirthdayTopAppBar(
|
||||||
|
onBack = {
|
||||||
|
if (currentStage.hasPrevious()) {
|
||||||
|
restoreState.goPrevious()
|
||||||
|
} else {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
when (currentStage) {
|
when (currentStage) {
|
||||||
|
@ -198,10 +223,7 @@ fun RestoreWallet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RestoreStage.Birthday -> {
|
RestoreStage.Birthday -> {
|
||||||
// No content
|
// No content. The action button is part of scrollable content.
|
||||||
}
|
|
||||||
RestoreStage.Complete -> {
|
|
||||||
// No content
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -231,25 +253,14 @@ fun RestoreWallet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RestoreStage.Birthday -> {
|
RestoreStage.Birthday -> {
|
||||||
RestoreBirthday(
|
RestoreBirthdayMainContent(
|
||||||
zcashNetwork = zcashNetwork,
|
zcashNetwork = zcashNetwork,
|
||||||
initialRestoreHeight = restoreHeight,
|
initialRestoreHeight = restoreHeight,
|
||||||
setRestoreHeight = setRestoreHeight,
|
setRestoreHeight = setRestoreHeight,
|
||||||
onNext = { restoreState.goNext() },
|
onDone = onFinished,
|
||||||
modifier = commonModifier
|
modifier = commonModifier
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.animateContentSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RestoreStage.Complete -> {
|
|
||||||
// In some cases we need to hide the software keyboard manually, as it stays shown after
|
|
||||||
// input on prior screens
|
|
||||||
LocalSoftwareKeyboardController.current?.hide()
|
|
||||||
|
|
||||||
RestoreComplete(
|
|
||||||
onComplete = onFinished,
|
|
||||||
modifier = commonModifier
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,29 +268,6 @@ fun RestoreWallet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RestoreTopAppBar(
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onClear: () -> Unit,
|
|
||||||
isShowClear: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
SmallTopAppBar(
|
|
||||||
backText = stringResource(id = R.string.restore_back).uppercase(),
|
|
||||||
backContentDescriptionText = stringResource(R.string.restore_back_content_description),
|
|
||||||
onBack = onBack,
|
|
||||||
regularActions = if (isShowClear) { {
|
|
||||||
ClearSeedMenuItem(
|
|
||||||
onSeedClear = onClear
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ClearSeedMenuItem(
|
private fun ClearSeedMenuItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -295,6 +283,38 @@ private fun ClearSeedMenuItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RestoreSeedTopAppBar(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onClear: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
SmallTopAppBar(
|
||||||
|
backText = stringResource(id = R.string.restore_back).uppercase(),
|
||||||
|
backContentDescriptionText = stringResource(R.string.restore_back_content_description),
|
||||||
|
onBack = onBack,
|
||||||
|
regularActions = {
|
||||||
|
ClearSeedMenuItem(
|
||||||
|
onSeedClear = onClear
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RestoreSeedBirthdayTopAppBar(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
SmallTopAppBar(
|
||||||
|
backText = stringResource(id = R.string.restore_back).uppercase(),
|
||||||
|
backContentDescriptionText = stringResource(R.string.restore_back_content_description),
|
||||||
|
onBack = onBack,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
// TODO [#1060]: https://github.com/Electric-Coin-Company/zashi-android/issues/1060
|
||||||
|
@ -317,8 +337,6 @@ private fun RestoreSeedMainContent(
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val textFieldScrollToHeight = rememberSaveable { mutableIntStateOf(0) }
|
val textFieldScrollToHeight = rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
|
||||||
Twig.error { "TEST: $parseResult, $text" }
|
|
||||||
|
|
||||||
if (parseResult is ParseResult.Add) {
|
if (parseResult is ParseResult.Add) {
|
||||||
setText("")
|
setText("")
|
||||||
userWordList.append(parseResult.words)
|
userWordList.append(parseResult.words)
|
||||||
|
@ -388,7 +406,6 @@ private fun RestoreSeedMainContent(
|
||||||
DisposableEffect(parseResult) {
|
DisposableEffect(parseResult) {
|
||||||
// Causes the TextFiled to refocus
|
// 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
|
// Causes scroll to the TextField after the first type action
|
||||||
|
@ -638,13 +655,16 @@ private fun Warn(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
private fun RestoreBirthday(
|
private fun RestoreBirthdayMainContent(
|
||||||
zcashNetwork: ZcashNetwork,
|
zcashNetwork: ZcashNetwork,
|
||||||
initialRestoreHeight: BlockHeight?,
|
initialRestoreHeight: BlockHeight?,
|
||||||
setRestoreHeight: (BlockHeight?) -> Unit,
|
setRestoreHeight: (BlockHeight?) -> Unit,
|
||||||
onNext: () -> Unit,
|
onDone: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val (height, setHeight) = rememberSaveable {
|
val (height, setHeight) = rememberSaveable {
|
||||||
mutableStateOf(initialRestoreHeight?.value?.toString() ?: "")
|
mutableStateOf(initialRestoreHeight?.value?.toString() ?: "")
|
||||||
}
|
}
|
||||||
|
@ -652,15 +672,16 @@ private fun RestoreBirthday(
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(scrollState)
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Header(stringResource(R.string.restore_birthday_header))
|
TopScreenLogoTitle(
|
||||||
|
title = stringResource(R.string.restore_birthday_header),
|
||||||
|
logoContentDescription = stringResource(R.string.zcash_logo_content_description),
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Body(stringResource(R.string.restore_birthday_sub_header))
|
||||||
|
|
||||||
Body(stringResource(R.string.restore_birthday_body))
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
|
@ -670,11 +691,12 @@ private fun RestoreBirthday(
|
||||||
val filteredHeightString = heightString.filter { it.isDigit() }
|
val filteredHeightString = heightString.filter { it.isDigit() }
|
||||||
setHeight(filteredHeightString)
|
setHeight(filteredHeightString)
|
||||||
},
|
},
|
||||||
Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(ZcashTheme.dimens.spacingTiny)
|
.padding(ZcashTheme.dimens.spacingTiny)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
.testTag(RestoreTag.BIRTHDAY_TEXT_FIELD),
|
.testTag(RestoreTag.BIRTHDAY_TEXT_FIELD),
|
||||||
label = { Text(stringResource(id = R.string.restore_birthday_hint)) },
|
textStyle = ZcashTheme.extendedTypography.textFieldBirthday,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
KeyboardCapitalization.None,
|
KeyboardCapitalization.None,
|
||||||
autoCorrect = false,
|
autoCorrect = false,
|
||||||
|
@ -682,7 +704,7 @@ private fun RestoreBirthday(
|
||||||
keyboardType = KeyboardType.Number
|
keyboardType = KeyboardType.Number
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(onAny = {}),
|
keyboardActions = KeyboardActions(onAny = {}),
|
||||||
shape = RectangleShape,
|
withBorder = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
|
@ -693,65 +715,33 @@ private fun RestoreBirthday(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
val isBirthdayValid = height.toLongOrNull()?.let {
|
// Empty birthday value is a valid birthday height too, thus run validation only in case of non-empty heights.
|
||||||
|
val isBirthdayValid = height.isEmpty() || height.toLongOrNull()?.let {
|
||||||
it >= zcashNetwork.saplingActivationHeight.value
|
it >= zcashNetwork.saplingActivationHeight.value
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
|
val isEmptyBirthday = height.isEmpty()
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
setRestoreHeight(BlockHeight.new(zcashNetwork, height.toLong()))
|
if (isEmptyBirthday) {
|
||||||
onNext()
|
setRestoreHeight(null)
|
||||||
|
} else if (isBirthdayValid) {
|
||||||
|
setRestoreHeight(BlockHeight.new(zcashNetwork, height.toLong()))
|
||||||
|
} else {
|
||||||
|
error("The restore button should not expect click events")
|
||||||
|
}
|
||||||
|
onDone()
|
||||||
},
|
},
|
||||||
text = stringResource(R.string.restore_birthday_button_restore),
|
text = stringResource(R.string.restore_birthday_button_restore),
|
||||||
enabled = isBirthdayValid,
|
enabled = isBirthdayValid
|
||||||
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
TertiaryButton(
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||||
onClick = {
|
}
|
||||||
setRestoreHeight(null)
|
|
||||||
onNext()
|
|
||||||
},
|
|
||||||
text = stringResource(R.string.restore_birthday_button_skip),
|
|
||||||
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingDefault)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
LaunchedEffect(Unit) {
|
||||||
}
|
// Causes the TextFiled to focus on the first screen visit
|
||||||
}
|
focusRequester.requestFocus()
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RestoreComplete(
|
|
||||||
onComplete: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.then(modifier),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Header(stringResource(R.string.restore_complete_header))
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
|
||||||
|
|
||||||
Body(stringResource(R.string.restore_complete_info))
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.weight(MINIMAL_WEIGHT)
|
|
||||||
)
|
|
||||||
|
|
||||||
PrimaryButton(
|
|
||||||
onClick = onComplete,
|
|
||||||
text = stringResource(R.string.restore_button_see_wallet),
|
|
||||||
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,8 @@
|
||||||
<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>
|
||||||
|
|
||||||
<string name="restore_birthday_header">Do you know the wallet’s birthday?</string>
|
<string name="restore_birthday_header">Wallet birthday height</string>
|
||||||
<string name="restore_birthday_body">This will allow a faster sync. If you don’t know the wallet’s birthday, don’t worry.</string>
|
<string name="restore_birthday_sub_header">(optional)</string>
|
||||||
<string name="restore_birthday_hint">Birthday height</string>
|
<string name="restore_birthday_button_restore">Restore</string>
|
||||||
<string name="restore_birthday_button_restore">Restore with this birthday</string>
|
|
||||||
<string name="restore_birthday_button_skip">I don’t know the birthday</string>
|
|
||||||
|
|
||||||
<string name="restore_complete_header">Seed phrase imported!</string>
|
|
||||||
<string name="restore_complete_info">We will now scan the blockchain to find your transactions and balance.</string>
|
|
||||||
<string name="restore_button_see_wallet">Take me to my wallet</string>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -141,11 +141,6 @@ class ScreenshotTest : UiTestPrerequisites() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO [#859]: Screenshot tests fail on Firebase Test Lab
|
|
||||||
// TODO [#859]: https://github.com/Electric-Coin-Company/zashi-android/issues/859
|
|
||||||
// Some of the restore screenshots broke with the Compose 1.4 update and we don't yet know why.
|
|
||||||
private val isRestoreScreenshotsEnabled = false
|
|
||||||
|
|
||||||
@Suppress("LongMethod", "FunctionNaming", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "FunctionNaming", "CyclomaticComplexMethod")
|
||||||
private fun take_screenshots_for_restore_wallet(resContext: Context, tag: String) {
|
private fun take_screenshots_for_restore_wallet(resContext: Context, tag: String) {
|
||||||
// TODO [#286]: Screenshot tests fail on Firebase Test Lab
|
// TODO [#286]: Screenshot tests fail on Firebase Test Lab
|
||||||
|
@ -207,39 +202,31 @@ class ScreenshotTest : UiTestPrerequisites() {
|
||||||
SeedPhrase.SEED_PHRASE_SIZE
|
SeedPhrase.SEED_PHRASE_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRestoreScreenshotsEnabled.not()) {
|
composeTestRule.onNodeWithText(
|
||||||
return
|
text = resContext.getString(R.string.restore_seed_button_next),
|
||||||
}
|
ignoreCase = true
|
||||||
|
).also {
|
||||||
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_seed_button_next)).also {
|
|
||||||
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
|
||||||
composeTestRule.waitUntil(5.seconds.inWholeMilliseconds) {
|
composeTestRule.waitUntil(5.seconds.inWholeMilliseconds) {
|
||||||
runCatching { it.assertIsEnabled() }.isSuccess
|
runCatching { it.assertIsEnabled() }.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
it.performClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_birthday_button_restore)).also {
|
|
||||||
composeTestRule.waitUntil(5.seconds.inWholeMilliseconds) {
|
|
||||||
kotlin.runCatching { it.assertExists() }.isSuccess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
takeScreenshot(tag, "Import 3")
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_birthday_button_skip)).also {
|
|
||||||
it.performScrollTo()
|
it.performScrollTo()
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_complete_header)).also {
|
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_birthday_header)).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
takeScreenshot(tag, "Import 4")
|
takeScreenshot(tag, "Import 3")
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText(
|
||||||
|
text = resContext.getString(R.string.restore_birthday_button_restore),
|
||||||
|
ignoreCase = true
|
||||||
|
).also {
|
||||||
|
it.performScrollTo()
|
||||||
|
it.performClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in New Issue