From cee77ea0f9d9dc373a27bf94fd15ee189d8acd95 Mon Sep 17 00:00:00 2001 From: Milan Cerovsky Date: Tue, 11 Mar 2025 19:53:30 +0100 Subject: [PATCH] Restore redesign --- .../zcash/ui/design/component/Checkbox.kt | 10 +- .../zcash/ui/design/component/TextField.kt | 144 --- .../zcash/ui/design/component/ZashiButton.kt | 6 +- .../ui/design/component/ZashiCheckbox.kt | 17 +- .../ui/design/component/ZashiSeedTextField.kt | 94 ++ .../component/ZashiSeedWordTextField.kt | 103 +++ .../ui/design/component/ZashiTextField.kt | 69 +- .../restore/view/RestoreViewAndroidTest.kt | 14 +- .../view/RestoreViewSecuredScreenTest.kt | 7 +- .../ui/screen/restore/view/RestoreViewTest.kt | 35 +- .../co/electriccoin/zcash/di/UseCaseModule.kt | 2 - .../electriccoin/zcash/di/ViewModelModule.kt | 8 +- .../co/electriccoin/zcash/ui/MainActivity.kt | 12 +- .../ui/common/repository/WalletRepository.kt | 36 +- .../GetBackupPersistableWalletUseCase.kt | 17 - .../usecase/GetPersistableWalletUseCase.kt | 6 +- .../usecase/ObserveSelectedEndpointUseCase.kt | 6 +- .../ui/common/viewmodel/WalletViewModel.kt | 4 +- .../ui/screen/onboarding/AndroidOnboarding.kt | 115 ++- .../zcash/ui/screen/onboarding/Onboarding.kt | 6 + .../viewmodel/OnboardingViewModel.kt | 27 - .../zcash/ui/screen/restore/AndroidRestore.kt | 61 -- .../ui/screen/restore/RestoreSeedDialog.kt | 131 +++ .../screen/restore/RestoreSeedDialogState.kt | 7 + .../restore/height/AndroidRestoreBDHeight.kt | 28 + .../restore/height/RestoreBDHeightState.kt | 13 + .../restore/height/RestoreBDHeightView.kt | 215 +++++ .../height/RestoreBDHeightViewModel.kt | 84 ++ .../ui/screen/restore/model/ParseResult.kt | 79 -- .../ui/screen/restore/model/RestoreStage.kt | 28 - .../ui/screen/restore/seed/AndroidRestore.kt | 24 + .../screen/restore/seed/RestoreSeedState.kt | 12 + .../{RestoreTag.kt => seed/RestoreSeedTag.kt} | 4 +- .../ui/screen/restore/seed/RestoreSeedView.kt | 142 +++ .../restore/seed/RestoreSeedViewModel.kt | 122 +++ .../ui/screen/restore/state/RestoreState.kt | 24 - .../zcash/ui/screen/restore/state/WordList.kt | 47 - .../ui/screen/restore/view/RestoreView.kt | 859 ------------------ .../restore/viewmodel/RestoreViewModel.kt | 111 --- .../restoresuccess/view/RestoreSuccessView.kt | 111 ++- .../res/ui/common/drawable-night/ic_info.xml | 17 + .../main/res/ui/common/drawable/ic_info.xml | 17 + .../ui/restore/drawable/ic_restore_dialog.xml | 13 + .../main/res/ui/restore/values-es/strings.xml | 12 - .../main/res/ui/restore/values/strings.xml | 33 +- .../ui/restore_success/values-es/strings.xml | 6 +- .../res/ui/restore_success/values/strings.xml | 9 +- .../zcash/ui/screenshot/ScreenshotTest.kt | 8 +- 48 files changed, 1356 insertions(+), 1599 deletions(-) delete mode 100644 ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt create mode 100644 ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedTextField.kt create mode 100644 ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedWordTextField.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetBackupPersistableWalletUseCase.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/Onboarding.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/viewmodel/OnboardingViewModel.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/AndroidRestore.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialog.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialogState.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/AndroidRestoreBDHeight.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightState.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightView.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightViewModel.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/ParseResult.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/RestoreStage.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/AndroidRestore.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedState.kt rename ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/{RestoreTag.kt => seed/RestoreSeedTag.kt} (80%) create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedView.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedViewModel.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/RestoreState.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/WordList.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt delete mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/viewmodel/RestoreViewModel.kt create mode 100644 ui-lib/src/main/res/ui/common/drawable-night/ic_info.xml create mode 100644 ui-lib/src/main/res/ui/common/drawable/ic_info.xml create mode 100644 ui-lib/src/main/res/ui/restore/drawable/ic_restore_dialog.xml diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Checkbox.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Checkbox.kt index 827c3aae8..45e0a5eac 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Checkbox.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Checkbox.kt @@ -13,8 +13,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import co.electriccoin.zcash.ui.design.theme.ZcashTheme @@ -66,7 +68,9 @@ fun LabeledCheckBox( text: String, modifier: Modifier = Modifier, checked: Boolean = false, - checkBoxTestTag: String? = null + checkBoxTestTag: String? = null, + color: Color = ZcashTheme.colors.textPrimary, + style: TextStyle = ZcashTheme.extendedTypography.checkboxText ) { val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) } @@ -114,8 +118,8 @@ fun LabeledCheckBox( ) Text( text = AnnotatedString(text), - color = ZcashTheme.colors.textPrimary, - style = ZcashTheme.extendedTypography.checkboxText + color = color, + style = style ) } } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt deleted file mode 100644 index 1c072752a..000000000 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt +++ /dev/null @@ -1,144 +0,0 @@ -package co.electriccoin.zcash.ui.design.component - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldColors -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusEvent -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.util.StringResource -import kotlinx.coroutines.launch - -@OptIn(ExperimentalFoundationApi::class) -@Suppress("LongParameterList", "LongMethod") -@Composable -fun FormTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - error: String? = null, - enabled: Boolean = true, - textStyle: TextStyle = ZcashTheme.extendedTypography.textFieldValue, - placeholder: @Composable (() -> Unit)? = null, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - colors: TextFieldColors = - TextFieldDefaults.colors( - cursorColor = ZcashTheme.colors.textPrimary, - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - errorContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), - keyboardActions: KeyboardActions = KeyboardActions.Default, - shape: Shape = TextFieldDefaults.shape, - // To enable border around the TextField - withBorder: Boolean = true, - bringIntoViewRequester: BringIntoViewRequester? = null, - minHeight: Dp = ZcashTheme.dimens.textFieldDefaultHeight, - testTag: String? = null -) { - val coroutineScope = rememberCoroutineScope() - - Column(modifier = Modifier.then(modifier)) { - TextField( - value = value, - onValueChange = onValueChange, - placeholder = - if (enabled) { - placeholder - } else { - null - }, - textStyle = textStyle, - keyboardOptions = keyboardOptions, - colors = colors, - modifier = - Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = minHeight) - .onFocusEvent { focusState -> - bringIntoViewRequester?.run { - if (focusState.isFocused) { - coroutineScope.launch { - bringIntoView() - } - } - } - } - .then( - if (withBorder) { - Modifier.border( - width = 1.dp, - color = - if (enabled) { - ZcashTheme.colors.textFieldFrame - } else { - ZcashTheme.colors.textDisabled - } - ) - } else { - Modifier - } - ) - .then( - if (testTag.isNullOrEmpty()) { - Modifier - } else { - Modifier.testTag(testTag) - } - ), - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - keyboardActions = keyboardActions, - shape = shape, - enabled = enabled - ) - - if (!error.isNullOrEmpty()) { - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) - - BodySmall( - text = error, - color = ZcashTheme.colors.textFieldWarning, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } -} - -@Immutable -data class TextFieldState( - val value: StringResource, - val error: StringResource? = null, - val isEnabled: Boolean = true, - val onValueChange: (String) -> Unit, -) { - val isError = error != null -} diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt index 0697d93ee..2419d7e1e 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt @@ -67,13 +67,13 @@ fun ZashiButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, - style: TextStyle = ZashiButtonDefaults.style, - shape: Shape = ZashiButtonDefaults.shape, - contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding, @DrawableRes icon: Int? = null, @DrawableRes trailingIcon: Int? = null, enabled: Boolean = true, isLoading: Boolean = false, + style: TextStyle = ZashiButtonDefaults.style, + shape: Shape = ZashiButtonDefaults.shape, + contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding, colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(), content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content ) { diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiCheckbox.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiCheckbox.kt index 9236239ef..ee431623c 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiCheckbox.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiCheckbox.kt @@ -21,7 +21,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import co.electriccoin.zcash.ui.design.R @@ -40,6 +42,9 @@ fun ZashiCheckbox( isChecked: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, + style: TextStyle = ZashiTypography.textSm, + fontWeight: FontWeight = FontWeight.Medium, + color: Color = ZashiColors.Text.textPrimary, ) { ZashiCheckbox( state = @@ -49,6 +54,9 @@ fun ZashiCheckbox( onClick = onClick, ), modifier = modifier, + style = style, + fontWeight = fontWeight, + color = color, ) } @@ -56,6 +64,9 @@ fun ZashiCheckbox( fun ZashiCheckbox( state: CheckboxState, modifier: Modifier = Modifier, + style: TextStyle = ZashiTypography.textSm, + fontWeight: FontWeight = FontWeight.Medium, + color: Color = ZashiColors.Text.textPrimary, ) { Row( modifier = @@ -70,9 +81,9 @@ fun ZashiCheckbox( Text( text = state.text.getValue(), - style = ZashiTypography.textSm, - fontWeight = FontWeight.Medium, - color = ZashiColors.Text.textPrimary, + style = style, + fontWeight = fontWeight, + color = color, ) } } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedTextField.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedTextField.kt new file mode 100644 index 000000000..6d6c75a5f --- /dev/null +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedTextField.kt @@ -0,0 +1,94 @@ +package co.electriccoin.zcash.ui.design.component + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.FlowRowOverflow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreenSizes +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.util.stringRes + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ZashiSeedTextField( + state: SeedTextFieldState, + modifier: Modifier = Modifier +) { + val focusRequesters = remember { state.values.map { FocusRequester() } } + val focusManager = LocalFocusManager.current + + FlowRow( + modifier = modifier.fillMaxWidth(), + maxItemsInEachRow = 3, + horizontalArrangement = spacedBy(4.dp), + verticalArrangement = spacedBy(4.dp), + overflow = FlowRowOverflow.Visible, + ) { + state.values.forEachIndexed { index, wordState -> + val focusRequester = remember { focusRequesters[index] } + ZashiSeedWordTextField( + modifier = + Modifier + .weight(1f) + .focusRequester(focusRequester), + prefix = (index + 1).toString(), + state = wordState, + keyboardActions = + KeyboardActions( + onDone = { + focusManager.clearFocus(true) + }, + onNext = { + if (index != state.values.lastIndex) { + focusRequesters[index + 1].requestFocus() + } + } + ), + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = if (index == state.values.lastIndex) ImeAction.Done else ImeAction.Next + ), + ) + } + } +} + +@Immutable +data class SeedTextFieldState( + val values: List, +) + +@PreviewScreenSizes +@Composable +private fun Preview() = + ZcashTheme { + BlankSurface { + ZashiSeedTextField( + state = + SeedTextFieldState( + values = + (1..24).map { + SeedWordTextFieldState( + value = stringRes("Word"), + onValueChange = { }, + isError = false + ) + } + ) + ) + } + } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedWordTextField.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedWordTextField.kt new file mode 100644 index 000000000..93a775c5b --- /dev/null +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiSeedWordTextField.kt @@ -0,0 +1,103 @@ +package co.electriccoin.zcash.ui.design.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.StringResource +import co.electriccoin.zcash.ui.design.util.stringRes + +@Composable +fun ZashiSeedWordTextField( + prefix: String, + state: SeedWordTextFieldState, + modifier: Modifier = Modifier, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + ZashiTextField( + modifier = modifier, + innerModifier = Modifier, + shape = RoundedCornerShape(12.dp), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = true, + maxLines = 1, + interactionSource = interactionSource, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + state = + TextFieldState( + value = state.value, + onValueChange = state.onValueChange, + ), + textStyle = ZashiTypography.textMd, + prefix = { + Box( + modifier = + Modifier + .size(22.dp) + .background(ZashiColors.Tags.tcCountBg, CircleShape) + .padding(end = 1.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = prefix, + style = ZashiTypography.textSm, + color = ZashiColors.Tags.tcCountFg, + fontWeight = FontWeight.Medium + ) + } + }, + colors = + ZashiTextFieldDefaults.defaultColors( + containerColor = ZashiColors.Surfaces.bgSecondary, + focusedContainerColor = ZashiColors.Surfaces.bgPrimary, + focusedBorderColor = ZashiColors.Accordion.focusStroke + ) + ) +} + +@Immutable +data class SeedWordTextFieldState( + val value: StringResource, + val isError: Boolean, + // val isFocused: Boolean, + // val onFocusChange: (Boolean) -> Unit, + val onValueChange: (String) -> Unit +) + +@Composable +@PreviewScreens +private fun Preview() = + ZcashTheme { + BlankSurface { + ZashiSeedWordTextField( + prefix = "12", + state = + SeedWordTextFieldState( + value = stringRes("asd"), + isError = false, + onValueChange = {}, + ) + ) + } + } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTextField.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTextField.kt index f13366d7e..ba244abcb 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTextField.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTextField.kt @@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.design.component import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -39,6 +40,7 @@ import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.StringResource import co.electriccoin.zcash.ui.design.util.getValue import co.electriccoin.zcash.ui.design.util.stringRes @@ -48,7 +50,7 @@ fun ZashiTextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, - innerModifier: Modifier = Modifier, + innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier, error: String? = null, isEnabled: Boolean = true, readOnly: Boolean = false, @@ -106,7 +108,7 @@ fun ZashiTextField( fun ZashiTextField( state: TextFieldState, modifier: Modifier = Modifier, - innerModifier: Modifier = Modifier, + innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier, readOnly: Boolean = false, textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium), label: @Composable (() -> Unit)? = null, @@ -124,6 +126,13 @@ fun ZashiTextField( minLines: Int = 1, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = ZashiTextFieldDefaults.shape, + contentPadding: PaddingValues = + PaddingValues( + start = if (leadingIcon != null) 8.dp else 14.dp, + end = if (suffix != null) 4.dp else 12.dp, + top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix), + bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix), + ), colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors() ) { TextFieldInternal( @@ -147,10 +156,20 @@ fun ZashiTextField( interactionSource = interactionSource, shape = shape, colors = colors, + contentPadding = contentPadding, innerModifier = innerModifier ) } +@Composable +fun ZashiTextFieldPlaceholder(res: StringResource) { + Text( + text = res.getValue(), + style = ZashiTypography.textMd, + color = ZashiColors.Inputs.Default.text + ) +} + @Suppress("LongParameterList", "LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -174,10 +193,13 @@ private fun TextFieldInternal( interactionSource: MutableInteractionSource, shape: Shape, colors: ZashiTextFieldColors, + contentPadding: PaddingValues, modifier: Modifier = Modifier, innerModifier: Modifier = Modifier, ) { - val borderColor by colors.borderColor(state) + val isFocused by interactionSource.collectIsFocusedAsState() + + val borderColor by colors.borderColor(state, isFocused) val androidColors = colors.toTextFieldColors() // If color is not provided via the text style, use content color as a default val textColor = @@ -193,16 +215,16 @@ private fun TextFieldInternal( BasicTextField( value = state.value.getValue(), modifier = - innerModifier.fillMaxWidth() then + innerModifier then if (borderColor == Color.Unspecified) { Modifier } else { Modifier.border( width = 1.dp, color = borderColor, - shape = ZashiTextFieldDefaults.shape + shape = shape ) - } then Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth), + }, onValueChange = state.onValueChange, enabled = state.isEnabled, readOnly = readOnly, @@ -243,13 +265,7 @@ private fun TextFieldInternal( isError = state.isError, interactionSource = interactionSource, colors = androidColors, - contentPadding = - PaddingValues( - start = if (leadingIcon != null) 8.dp else 14.dp, - end = if (suffix != null) 4.dp else 12.dp, - top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix), - bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix), - ) + contentPadding = contentPadding ) } ) @@ -303,7 +319,9 @@ data class ZashiTextFieldColors( val textColor: Color, val hintColor: Color, val borderColor: Color, + val focusedBorderColor: Color, val containerColor: Color, + val focusedContainerColor: Color, val placeholderColor: Color, val disabledTextColor: Color, val disabledHintColor: Color, @@ -317,11 +335,15 @@ data class ZashiTextFieldColors( val errorPlaceholderColor: Color, ) { @Composable - internal fun borderColor(state: TextFieldState): State { + internal fun borderColor( + state: TextFieldState, + isFocused: Boolean + ): State { val targetValue = when { !state.isEnabled -> disabledBorderColor state.isError -> errorBorderColor + isFocused -> focusedBorderColor.takeOrElse { borderColor } else -> borderColor } return rememberUpdatedState(targetValue) @@ -345,7 +367,7 @@ data class ZashiTextFieldColors( unfocusedTextColor = textColor, disabledTextColor = disabledTextColor, errorTextColor = errorTextColor, - focusedContainerColor = containerColor, + focusedContainerColor = focusedContainerColor.takeOrElse { containerColor }, unfocusedContainerColor = containerColor, disabledContainerColor = disabledContainerColor, errorContainerColor = errorContainerColor, @@ -391,13 +413,18 @@ object ZashiTextFieldDefaults { val shape: Shape get() = RoundedCornerShape(8.dp) + val innerModifier: Modifier + get() = Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth).fillMaxWidth() + @Suppress("LongParameterList") @Composable fun defaultColors( textColor: Color = ZashiColors.Inputs.Filled.text, hintColor: Color = ZashiColors.Inputs.Default.hint, borderColor: Color = Color.Unspecified, + focusedBorderColor: Color = ZashiColors.Inputs.Focused.stroke, containerColor: Color = ZashiColors.Inputs.Default.bg, + focusedContainerColor: Color = ZashiColors.Inputs.Focused.bg, placeholderColor: Color = ZashiColors.Inputs.Default.text, disabledTextColor: Color = ZashiColors.Inputs.Disabled.text, disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint, @@ -413,7 +440,9 @@ object ZashiTextFieldDefaults { textColor = textColor, hintColor = hintColor, borderColor = borderColor, + focusedBorderColor = focusedBorderColor, containerColor = containerColor, + focusedContainerColor = focusedContainerColor, placeholderColor = placeholderColor, disabledTextColor = disabledTextColor, disabledHintColor = disabledHintColor, @@ -428,6 +457,16 @@ object ZashiTextFieldDefaults { ) } +@Immutable +data class TextFieldState( + val value: StringResource, + val error: StringResource? = null, + val isEnabled: Boolean = true, + val onValueChange: (String) -> Unit, +) { + val isError = error != null +} + @PreviewScreens @Composable private fun DefaultPreview() = diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt index e3f7a8046..6bf263e93 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt @@ -25,8 +25,8 @@ import cash.z.ecc.sdk.fixture.SeedPhraseFixture import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.design.component.CommonTag -import co.electriccoin.zcash.ui.screen.restore.RestoreTag import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag import co.electriccoin.zcash.ui.test.getAppContext import co.electriccoin.zcash.ui.test.getStringResource import org.junit.Assert.assertEquals @@ -55,7 +55,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() { composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.assertIsFocused() } @@ -81,7 +81,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() { SeedPhraseFixture.SEED_PHRASE + " " + SeedPhraseFixture.SEED_PHRASE ) - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.performKeyInput { withKeyDown(Key.CtrlLeft) { pressKey(Key.V) @@ -94,11 +94,11 @@ class RestoreViewAndroidTest : UiTestPrerequisites() { assertEquals(SeedPhrase.SEED_PHRASE_SIZE, testSetup.getUserInputWords().size) - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.assertDoesNotExist() } - composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also { it.assertDoesNotExist() } @@ -116,7 +116,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() { composeTestRule.waitForIdle() // Insert uncompleted seed words - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.performTextInput("test") } @@ -139,7 +139,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() { composeTestRule.waitForIdle() // Insert complete seed words - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.performTextInput(SeedPhraseFixture.SEED_PHRASE) } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewSecuredScreenTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewSecuredScreenTest.kt index 4bf01bdeb..681b91dbf 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewSecuredScreenTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewSecuredScreenTest.kt @@ -10,7 +10,8 @@ import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity import co.electriccoin.zcash.ui.common.compose.ScreenSecurity import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.screen.restore.state.RestoreState +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedState +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedView import co.electriccoin.zcash.ui.screen.restore.state.WordList import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -43,9 +44,9 @@ class RestoreViewSecuredScreenTest : UiTestPrerequisites() { composeTestRule.setContent { CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) { ZcashTheme { - RestoreWallet( + RestoreSeedView( ZcashNetwork.Mainnet, - RestoreState(), + RestoreSeedState(), Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(), WordList(emptyList()), restoreHeight = null, diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewTest.kt index a4d8018d7..602feaa8b 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewTest.kt @@ -25,9 +25,10 @@ import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.design.component.CommonTag import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.screen.restore.RestoreTag 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.seed.RestoreSeedState +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedView import co.electriccoin.zcash.ui.screen.restore.state.WordList import co.electriccoin.zcash.ui.test.getStringResource import kotlinx.collections.immutable.toPersistentSet @@ -54,7 +55,7 @@ class RestoreViewTest : UiTestPrerequisites() { fun seed_autocomplete_suggestions_appear() { newTestSetup() - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.performTextInput("ab") // Make sure text isn't cleared @@ -62,13 +63,13 @@ class RestoreViewTest : UiTestPrerequisites() { } composeTestRule.onNode( - matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM) + matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM) ).also { it.assertExists() } composeTestRule.onNode( - matcher = hasText("able", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM) + matcher = hasText("able", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM) ).also { it.assertExists() } @@ -79,17 +80,17 @@ class RestoreViewTest : UiTestPrerequisites() { fun seed_choose_autocomplete() { newTestSetup() - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.performTextInput("ab") } composeTestRule.onNode( - matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM) + matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM) ).also { it.performClick() } - composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also { it.assertDoesNotExist() } @@ -97,7 +98,7 @@ class RestoreViewTest : UiTestPrerequisites() { it.assertExists() } - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.assertTextEquals("abandon ", includeEditableText = true) } } @@ -107,15 +108,15 @@ class RestoreViewTest : UiTestPrerequisites() { fun seed_type_full_word() { newTestSetup() - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.performTextInput("abandon") } - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.assertTextEquals("abandon ", includeEditableText = true) } - composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also { it.assertDoesNotExist() } @@ -209,7 +210,7 @@ class RestoreViewTest : UiTestPrerequisites() { initialWordsList = SeedPhraseFixture.new().split ) - composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also { it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString()) } @@ -241,7 +242,7 @@ class RestoreViewTest : UiTestPrerequisites() { it.assertIsEnabled() } - composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also { it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString()) } @@ -266,7 +267,7 @@ class RestoreViewTest : UiTestPrerequisites() { initialWordsList = SeedPhraseFixture.new().split ) - composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also { it.performTextInput("1.2") } @@ -353,7 +354,7 @@ class RestoreViewTest : UiTestPrerequisites() { initialStage: RestoreStage, initialWordsList: List ) { - private val state = RestoreState(initialStage) + private val state = RestoreSeedState(initialStage) private val wordList = WordList(initialWordsList) @@ -391,7 +392,7 @@ class RestoreViewTest : UiTestPrerequisites() { init { composeTestRule.setContent { ZcashTheme { - RestoreWallet( + RestoreSeedView( ZcashNetwork.Mainnet, state, Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(), diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt index 4939fc1a5..ad3ef9a27 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt @@ -16,7 +16,6 @@ import co.electriccoin.zcash.ui.common.usecase.DeleteTransactionNoteUseCase import co.electriccoin.zcash.ui.common.usecase.DeriveKeystoneAccountUnifiedAddressUseCase import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase import co.electriccoin.zcash.ui.common.usecase.FlipTransactionBookmarkUseCase -import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase @@ -121,7 +120,6 @@ val useCaseModule = factoryOf(::IsCoinbaseAvailableUseCase) factoryOf(::GetZashiSpendingKeyUseCase) factoryOf(::ObservePersistableWalletUseCase) - factoryOf(::GetBackupPersistableWalletUseCase) factoryOf(::GetSupportUseCase) factoryOf(::SendEmailUseCase) factoryOf(::SendSupportEmailUseCase) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt index c7d5b5185..6be36ab33 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt @@ -16,11 +16,11 @@ import co.electriccoin.zcash.ui.screen.feedback.viewmodel.FeedbackViewModel import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel import co.electriccoin.zcash.ui.screen.home.HomeViewModel import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel -import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel -import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel +import co.electriccoin.zcash.ui.screen.restore.height.RestoreBDHeightViewModel +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedViewModel import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransactionViewModel import co.electriccoin.zcash.ui.screen.scan.Scan @@ -57,9 +57,8 @@ val viewModelModule = viewModelOf(::WalletViewModel) viewModelOf(::AuthenticationViewModel) viewModelOf(::OldHomeViewModel) - viewModelOf(::OnboardingViewModel) viewModelOf(::StorageCheckViewModel) - viewModelOf(::RestoreViewModel) + viewModelOf(::RestoreSeedViewModel) viewModelOf(::ScreenBrightnessViewModel) viewModelOf(::SettingsViewModel) viewModelOf(::AdvancedSettingsViewModel) @@ -156,4 +155,5 @@ val viewModelModule = viewModelOf(::TaxExportViewModel) viewModelOf(::BalanceViewModel) viewModelOf(::HomeViewModel) + viewModelOf(::RestoreBDHeightViewModel) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt index 538fbc8dc..4a3428976 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt @@ -48,7 +48,7 @@ import co.electriccoin.zcash.ui.screen.authentication.RETRY_TRIGGER_DELAY import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart -import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding +import co.electriccoin.zcash.ui.screen.onboarding.RestoreNavigation import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs @@ -249,7 +249,7 @@ class MainActivity : FragmentActivity() { CompositionLocalProvider(RemoteConfig provides configuration) { when (secretState) { SecretState.None -> { - WrapOnboarding() + RestoreNavigation() } is SecretState.NeedsWarning -> { @@ -263,7 +263,11 @@ class MainActivity : FragmentActivity() { applicationContext, walletViewModel, SeedPhrase.new(WalletFixture.Alice.seedPhrase), - WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext)) + WalletFixture.Alice.getBirthday( + ZcashNetwork.fromResources( + applicationContext + ) + ) ) } else { walletViewModel.persistNewWalletAndRestoringState(WalletRestoringState.INITIATING) @@ -284,7 +288,7 @@ class MainActivity : FragmentActivity() { } else -> { - error("Unhandled secret state: $secretState") + // should not happen } } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt index 2e7cd5885..55f05e2dd 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/WalletRepository.kt @@ -70,7 +70,6 @@ interface WalletRepository { val synchronizer: StateFlow val secretState: StateFlow val fastestServers: StateFlow - val persistableWallet: Flow val onboardingState: Flow val allAccounts: Flow?> @@ -99,8 +98,6 @@ interface WalletRepository { suspend fun getSynchronizer(): Synchronizer - suspend fun getPersistableWallet(): PersistableWallet - fun persistExistingWalletWithSeedPhrase( network: ZcashNetwork, seedPhrase: SeedPhrase, @@ -110,7 +107,7 @@ interface WalletRepository { class WalletRepositoryImpl( accountDataSource: AccountDataSource, - persistableWalletProvider: PersistableWalletProvider, + private val persistableWalletProvider: PersistableWalletProvider, private val synchronizerProvider: SynchronizerProvider, private val application: Application, private val getDefaultServers: GetDefaultServersProvider, @@ -143,22 +140,12 @@ class WalletRepositoryImpl( override val allAccounts: StateFlow?> = accountDataSource.allAccounts override val secretState: StateFlow = - combine( - persistableWalletProvider.persistableWallet, - onboardingState - ) { persistableWallet: PersistableWallet?, onboardingState: OnboardingState -> - when { - onboardingState == OnboardingState.NONE -> SecretState.None - onboardingState == OnboardingState.NEEDS_WARN -> SecretState.NeedsWarning - onboardingState == OnboardingState.NEEDS_BACKUP && persistableWallet != null -> { - SecretState.NeedsBackup(persistableWallet) - } - - onboardingState == OnboardingState.READY && persistableWallet != null -> { - SecretState.Ready(persistableWallet) - } - - else -> SecretState.None + onboardingState.map { onboardingState: OnboardingState -> + when (onboardingState) { + OnboardingState.NONE -> SecretState.None + OnboardingState.NEEDS_WARN -> SecretState.NeedsWarning + OnboardingState.NEEDS_BACKUP -> SecretState.NeedsBackup + OnboardingState.READY -> SecretState.Ready } }.stateIn( scope = scope, @@ -204,11 +191,6 @@ class WalletRepositoryImpl( initialValue = FastestServersState(servers = emptyList(), isLoading = true) ) - override val persistableWallet: Flow = - secretState.map { - (it as? SecretState.Ready?)?.persistableWallet - } - @OptIn(ExperimentalCoroutinesApi::class) override val currentWalletSnapshot: StateFlow = combine(synchronizer, currentAccount) { synchronizer, currentAccount -> @@ -317,7 +299,7 @@ class WalletRepositoryImpl( } override suspend fun getSelectedServer(): LightWalletEndpoint { - return persistableWallet + return persistableWalletProvider.persistableWallet .map { it?.endpoint } @@ -338,8 +320,6 @@ class WalletRepositoryImpl( override suspend fun getSynchronizer(): Synchronizer = synchronizerProvider.getSynchronizer() - override suspend fun getPersistableWallet(): PersistableWallet = persistableWallet.filterNotNull().first() - override fun persistExistingWalletWithSeedPhrase( network: ZcashNetwork, seedPhrase: SeedPhrase, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetBackupPersistableWalletUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetBackupPersistableWalletUseCase.kt deleted file mode 100644 index 2f40c85b6..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetBackupPersistableWalletUseCase.kt +++ /dev/null @@ -1,17 +0,0 @@ -package co.electriccoin.zcash.ui.common.usecase - -import co.electriccoin.zcash.ui.common.repository.WalletRepository -import co.electriccoin.zcash.ui.common.viewmodel.SecretState -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map - -class GetBackupPersistableWalletUseCase( - private val walletRepository: WalletRepository -) { - suspend operator fun invoke() = - walletRepository.secretState - .map { (it as? SecretState.NeedsBackup)?.persistableWallet } - .filterNotNull() - .first() -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetPersistableWalletUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetPersistableWalletUseCase.kt index 7cd43ba07..52124f34c 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetPersistableWalletUseCase.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetPersistableWalletUseCase.kt @@ -1,9 +1,9 @@ package co.electriccoin.zcash.ui.common.usecase -import co.electriccoin.zcash.ui.common.repository.WalletRepository +import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider class GetPersistableWalletUseCase( - private val walletRepository: WalletRepository + private val persistableWalletProvider: PersistableWalletProvider ) { - suspend operator fun invoke() = walletRepository.getPersistableWallet() + suspend operator fun invoke() = persistableWalletProvider.getPersistableWallet() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveSelectedEndpointUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveSelectedEndpointUseCase.kt index 91f0d767c..0851ab33e 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveSelectedEndpointUseCase.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveSelectedEndpointUseCase.kt @@ -1,14 +1,14 @@ package co.electriccoin.zcash.ui.common.usecase -import co.electriccoin.zcash.ui.common.repository.WalletRepository +import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map class ObserveSelectedEndpointUseCase( - private val walletRepository: WalletRepository + private val persistableWalletProvider: PersistableWalletProvider ) { operator fun invoke() = - walletRepository.persistableWallet + persistableWalletProvider.persistableWallet .map { it?.endpoint } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt index 2b1aaa72a..cac9b1938 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt @@ -168,9 +168,9 @@ sealed class SecretState { object NeedsWarning : SecretState() - class NeedsBackup(val persistableWallet: PersistableWallet) : SecretState() + object NeedsBackup : SecretState() - class Ready(val persistableWallet: PersistableWallet) : SecretState() + object Ready : SecretState() } /** diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt index 1b6b07d87..f1f63637f 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt @@ -4,74 +4,103 @@ package co.electriccoin.zcash.ui.screen.onboarding import android.content.Context import androidx.compose.runtime.Composable -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable import cash.z.ecc.android.sdk.fixture.WalletFixture import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.SeedPhrase import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.sdk.type.fromResources -import co.electriccoin.zcash.di.koinActivityViewModel import co.electriccoin.zcash.spackle.FirebaseTestLabUtil +import co.electriccoin.zcash.ui.MainActivity +import co.electriccoin.zcash.ui.NavigationRouter +import co.electriccoin.zcash.ui.Navigator +import co.electriccoin.zcash.ui.NavigatorImpl import co.electriccoin.zcash.ui.common.compose.LocalActivity +import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel +import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition +import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition +import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition +import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popExitTransition +import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel import co.electriccoin.zcash.ui.screen.onboarding.view.Onboarding -import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel -import co.electriccoin.zcash.ui.screen.restore.WrapRestore +import co.electriccoin.zcash.ui.screen.restore.height.AndroidRestoreBDHeight +import co.electriccoin.zcash.ui.screen.restore.height.RestoreBDHeight +import co.electriccoin.zcash.ui.screen.restore.seed.AndroidRestoreSeed +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeed +import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject -@Suppress("LongMethod") @Composable -internal fun WrapOnboarding() { +fun MainActivity.RestoreNavigation() { val activity = LocalActivity.current - val walletViewModel = koinActivityViewModel() - val onboardingViewModel = koinActivityViewModel() - + val navigationRouter = koinInject() + val navController = LocalNavController.current + val flexaViewModel = koinViewModel() + val navigator: Navigator = remember { NavigatorImpl(this@RestoreNavigation, navController, flexaViewModel) } val versionInfo = VersionInfo.new(activity.applicationContext) - // TODO [#383]: https://github.com/Electric-Coin-Company/zashi-android/issues/383 - // TODO [#383]: Refactoring of UI state retention into rememberSaveable fields - - if (!onboardingViewModel.isImporting.collectAsStateWithLifecycle().value) { - val onCreateWallet = { - walletViewModel.persistOnboardingState(OnboardingState.NEEDS_WARN) - } - val onImportWallet = { - // In the case of the app currently being messed with by the robo test runner on - // Firebase Test Lab or Google Play pre-launch report, we want to skip creating - // a new or restoring an existing wallet screens by persisting an existing wallet - // with a mock seed. - if (FirebaseTestLabUtil.isFirebaseTestLab(activity.applicationContext)) { - persistExistingWalletWithSeedPhrase( - activity.applicationContext, - walletViewModel, - SeedPhrase.new(WalletFixture.Alice.seedPhrase), - birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext)) - ) - } else { - onboardingViewModel.setIsImporting(true) - } - } - - val onFixtureWallet: (String) -> Unit = { seed -> + val onCreateWallet = { + walletViewModel.persistOnboardingState(OnboardingState.NEEDS_WARN) + } + val onImportWallet = { + // In the case of the app currently being messed with by the robo test runner on + // Firebase Test Lab or Google Play pre-launch report, we want to skip creating + // a new or restoring an existing wallet screens by persisting an existing wallet + // with a mock seed. + if (FirebaseTestLabUtil.isFirebaseTestLab(activity.applicationContext)) { persistExistingWalletWithSeedPhrase( activity.applicationContext, walletViewModel, - SeedPhrase.new(seed), + SeedPhrase.new(WalletFixture.Alice.seedPhrase), birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext)) ) + } else { + navigationRouter.forward(RestoreSeed) } + } - Onboarding( - isDebugMenuEnabled = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService, - onImportWallet = onImportWallet, - onCreateWallet = onCreateWallet, - onFixtureWallet = onFixtureWallet + val onFixtureWallet: (String) -> Unit = { seed -> + persistExistingWalletWithSeedPhrase( + activity.applicationContext, + walletViewModel, + SeedPhrase.new(seed), + birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext)) ) + } - activity.reportFullyDrawn() - } else { - WrapRestore() + LaunchedEffect(Unit) { + navigationRouter.observePipeline().collect { + navigator.executeCommand(it) + } + } + NavHost( + navController = navController, + startDestination = Onboarding, + enterTransition = { enterTransition() }, + exitTransition = { exitTransition() }, + popEnterTransition = { popEnterTransition() }, + popExitTransition = { popExitTransition() } + ) { + composable { + Onboarding( + isDebugMenuEnabled = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService, + onImportWallet = onImportWallet, + onCreateWallet = onCreateWallet, + onFixtureWallet = onFixtureWallet + ) + } + composable { + AndroidRestoreSeed() + } + composable { + AndroidRestoreBDHeight() + } } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/Onboarding.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/Onboarding.kt new file mode 100644 index 000000000..5ebe459a5 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/Onboarding.kt @@ -0,0 +1,6 @@ +package co.electriccoin.zcash.ui.screen.onboarding + +import kotlinx.serialization.Serializable + +@Serializable +data object Onboarding diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/viewmodel/OnboardingViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/viewmodel/OnboardingViewModel.kt deleted file mode 100644 index a5199f748..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/viewmodel/OnboardingViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package co.electriccoin.zcash.ui.screen.onboarding.viewmodel - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle - -/* - * Android-specific ViewModel. This is used to save and restore state across Activity recreations - * outside of the Compose framework. - */ -class OnboardingViewModel( - application: Application, - private val savedStateHandle: SavedStateHandle -) : AndroidViewModel(application) { - // This is a bit weird being placed here, but onboarding currently is considered complete when - // the user has a persisted wallet. Also import allows the user to go back to onboarding, while - // creating a new wallet does not. - val isImporting = savedStateHandle.getStateFlow(KEY_IS_IMPORTING, false) - - fun setIsImporting(isImporting: Boolean) { - savedStateHandle[KEY_IS_IMPORTING] = isImporting - } - - companion object { - private const val KEY_IS_IMPORTING = "is_importing" // $NON-NLS - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/AndroidRestore.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/AndroidRestore.kt deleted file mode 100644 index 041f9b1de..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/AndroidRestore.kt +++ /dev/null @@ -1,61 +0,0 @@ -package co.electriccoin.zcash.ui.screen.restore - -import android.content.ClipboardManager -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cash.z.ecc.android.sdk.model.SeedPhrase -import cash.z.ecc.android.sdk.model.ZcashNetwork -import cash.z.ecc.sdk.type.fromResources -import co.electriccoin.zcash.di.koinActivityViewModel -import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel -import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase -import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel -import co.electriccoin.zcash.ui.screen.restore.view.RestoreWallet -import co.electriccoin.zcash.ui.screen.restore.viewmodel.CompleteWordSetState -import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel - -@Composable -fun WrapRestore() { - val walletViewModel = koinActivityViewModel() - val onboardingViewModel = koinActivityViewModel() - val restoreViewModel = koinActivityViewModel() - - val applicationContext = LocalContext.current.applicationContext - - when (val completeWordList = restoreViewModel.completeWordList.collectAsStateWithLifecycle().value) { - CompleteWordSetState.Loading -> { - // Although it might perform IO, it should be relatively fast. - // Consider whether to display indeterminate progress here. - // Another option would be to go straight to the restore screen with autocomplete - // disabled for a few milliseconds. Users would probably never notice due to the - // time it takes to re-orient on the new screen, unless users were doing this - // on a daily basis and become very proficient at our UI. The Therac-25 has - // historical precedent on how that could cause problems. - } - is CompleteWordSetState.Loaded -> { - RestoreWallet( - ZcashNetwork.fromResources(applicationContext), - restoreViewModel.restoreState, - completeWordList.list, - restoreViewModel.userWordList, - restoreViewModel.userBirthdayHeight.collectAsStateWithLifecycle().value, - setRestoreHeight = { restoreViewModel.userBirthdayHeight.value = it }, - onBack = { onboardingViewModel.setIsImporting(false) }, - paste = { - val clipboardManager = applicationContext.getSystemService(ClipboardManager::class.java) - return@RestoreWallet clipboardManager?.primaryClip?.toString() - }, - onFinished = { - persistExistingWalletWithSeedPhrase( - applicationContext, - walletViewModel, - SeedPhrase(restoreViewModel.userWordList.current.value), - restoreViewModel.userBirthdayHeight.value - ) - onboardingViewModel.setIsImporting(false) - } - ) - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialog.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialog.kt new file mode 100644 index 000000000..2fce05787 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialog.kt @@ -0,0 +1,131 @@ +package co.electriccoin.zcash.ui.screen.restore + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiInScreenModalBottomSheet +import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun RestoreSeedDialog( + state: RestoreSeedDialogState?, + sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), +) { + ZashiInScreenModalBottomSheet( + state = state, + sheetState = sheetState, + content = { + Content(it) + }, + ) +} + +@Composable +private fun Content(state: RestoreSeedDialogState) { + Column( + modifier = Modifier.padding(horizontal = 24.dp) + ) { + Text( + text = stringResource(R.string.integrations_dialog_more_options), + style = ZashiTypography.header6, + fontWeight = FontWeight.SemiBold, + color = ZashiColors.Text.textPrimary + ) + Spacer(Modifier.height(12.dp)) + + Info( + text = + buildAnnotatedString { + withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = ZashiColors.Text.textPrimary)) { + append(stringResource(id = R.string.restore_dialog_message_1_bold_part)) + } + append(" ") + append(stringResource(R.string.restore_dialog_message_1)) + } + ) + Spacer(modifier = Modifier.height(12.dp)) + Info( + text = + buildAnnotatedString { + withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = ZashiColors.Text.textPrimary)) { + append(stringResource(id = R.string.restore_dialog_message_2_bold_part)) + } + append(" ") + append(stringResource(R.string.restore_dialog_message_2)) + } + ) + + Spacer(modifier = Modifier.height(32.dp)) + + ZashiButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.restore_dialog_button), + onClick = state.onBack + ) + + Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } +} + +@Composable +private fun Info(text: AnnotatedString) { + Row { + Image( + painterResource(R.drawable.ic_info), + contentDescription = "" + ) + Spacer(Modifier.width(8.dp)) + Text( + text = text, + style = ZashiTypography.textSm, + fontWeight = FontWeight.Normal, + color = ZashiColors.Text.textTertiary + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@PreviewScreens +@Composable +private fun Preview() = + ZcashTheme { + RestoreSeedDialog( + sheetState = + rememberModalBottomSheetState( + skipPartiallyExpanded = true, + skipHiddenState = true, + initialValue = SheetValue.Expanded, + ), + state = RestoreSeedDialogState { }, + ) + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialogState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialogState.kt new file mode 100644 index 000000000..c23bb71ff --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreSeedDialogState.kt @@ -0,0 +1,7 @@ +package co.electriccoin.zcash.ui.screen.restore + +import co.electriccoin.zcash.ui.design.component.ModalBottomSheetState + +data class RestoreSeedDialogState( + override val onBack: () -> Unit +) : ModalBottomSheetState diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/AndroidRestoreBDHeight.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/AndroidRestoreBDHeight.kt new file mode 100644 index 000000000..8add0e50d --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/AndroidRestoreBDHeight.kt @@ -0,0 +1,28 @@ +package co.electriccoin.zcash.ui.screen.restore.height + +import androidx.activity.compose.BackHandler +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.electriccoin.zcash.ui.screen.restore.RestoreSeedDialog +import kotlinx.serialization.Serializable +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AndroidRestoreBDHeight() { + val vm = koinViewModel() + val state by vm.state.collectAsStateWithLifecycle() + val dialogState by vm.dialogState.collectAsStateWithLifecycle() + RestoreBDHeightView(state) + + BackHandler { + state.onBack() + } + + RestoreSeedDialog(dialogState) +} + +@Serializable +data object RestoreBDHeight diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightState.kt new file mode 100644 index 000000000..73b53191a --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightState.kt @@ -0,0 +1,13 @@ +package co.electriccoin.zcash.ui.screen.restore.height + +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.IconButtonState +import co.electriccoin.zcash.ui.design.component.TextFieldState + +data class RestoreBDHeightState( + val blockHeight: TextFieldState, + val estimate: ButtonState, + val restore: ButtonState, + val dialogButton: IconButtonState, + val onBack: () -> Unit +) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightView.kt new file mode 100644 index 000000000..389e194e8 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightView.kt @@ -0,0 +1,215 @@ +@file:Suppress("TooManyFunctions") + +package co.electriccoin.zcash.ui.screen.restore.height + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarTags +import co.electriccoin.zcash.ui.design.component.BlankBgScaffold +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.IconButtonState +import co.electriccoin.zcash.ui.design.component.TextFieldState +import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults +import co.electriccoin.zcash.ui.design.component.ZashiIconButton +import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar +import co.electriccoin.zcash.ui.design.component.ZashiTextField +import co.electriccoin.zcash.ui.design.component.ZashiTextFieldPlaceholder +import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.orDark +import co.electriccoin.zcash.ui.design.util.scaffoldPadding +import co.electriccoin.zcash.ui.design.util.stringRes +import java.text.DecimalFormat +import java.text.NumberFormat + +@Composable +fun RestoreBDHeightView(state: RestoreBDHeightState) { + BlankBgScaffold( + topBar = { AppBar(state) }, + bottomBar = {}, + content = { padding -> + Content( + state = state, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .scaffoldPadding(padding) + ) + } + ) +} + +@Composable +private fun Content( + state: RestoreBDHeightState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + ) { + Text( + text = stringResource(R.string.restore_bd_subtitle), + style = ZashiTypography.header6, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.restore_bd_message), + style = ZashiTypography.textSm, + color = ZashiColors.Text.textPrimary + ) + Spacer(Modifier.height(32.dp)) + Text( + text = stringResource(R.string.restore_bd_text_field_title), + style = ZashiTypography.textSm, + color = ZashiColors.Inputs.Default.label, + fontWeight = FontWeight.Medium + ) + Spacer(Modifier.height(6.dp)) + ZashiTextField( + state = state.blockHeight, + modifier = Modifier.fillMaxWidth(), + placeholder = { + ZashiTextFieldPlaceholder( + stringRes(R.string.restore_bd_text_field_hint) + ) + }, + keyboardOptions = + KeyboardOptions( + KeyboardCapitalization.None, + autoCorrectEnabled = false, + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number + ), + visualTransformation = ThousandSeparatorTransformation() + ) + Spacer(Modifier.height(6.dp)) + Text( + text = stringResource(R.string.restore_bd_text_field_note), + style = ZashiTypography.textXs, + color = ZashiColors.Text.textTertiary + ) + Spacer(Modifier.weight(1f)) + Spacer(Modifier.height(24.dp)) + + ZashiButton( + state.estimate, + modifier = Modifier.fillMaxWidth(), + colors = ZashiButtonDefaults.secondaryColors() + ) + + Spacer(Modifier.height(12.dp)) + + ZashiButton( + state.restore, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +private class ThousandSeparatorTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val symbols = DecimalFormat().decimalFormatSymbols + val decimalSeparator = symbols.decimalSeparator + + var outputText = "" + val integerPart: Long + val decimalPart: String + + if (text.text.isNotEmpty()) { + val number = text.text.toDouble() + integerPart = number.toLong() + outputText += NumberFormat.getIntegerInstance().format(integerPart) + if (text.text.contains(decimalSeparator)) { + decimalPart = text.text.substring(text.text.indexOf(decimalSeparator)) + if (decimalPart.isNotEmpty()) { + outputText += decimalPart + } + } + } + + val numberOffsetTranslator = + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return outputText.length + } + + override fun transformedToOriginal(offset: Int): Int { + return text.length + } + } + + return TransformedText( + text = AnnotatedString(outputText), + offsetMapping = numberOffsetTranslator + ) + } +} + +@Composable +private fun AppBar(state: RestoreBDHeightState) { + ZashiSmallTopAppBar( + title = stringResource(R.string.restore_title), + navigationAction = { + ZashiTopAppBarBackNavigation( + onBack = state.onBack, + modifier = Modifier.testTag(ZashiTopAppBarTags.BACK) + ) + }, + regularActions = { + ZashiIconButton(state.dialogButton, modifier = Modifier.size(40.dp)) + Spacer(Modifier.width(20.dp)) + }, + colors = + ZcashTheme.colors.topAppBarColors orDark + ZcashTheme.colors.topAppBarColors.copyColors( + containerColor = Color.Transparent + ), + ) +} + +@PreviewScreens +@Composable +private fun Preview() = + ZcashTheme { + RestoreBDHeightView( + state = + RestoreBDHeightState( + onBack = {}, + dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {}, + blockHeight = TextFieldState(stringRes("")) {}, + estimate = ButtonState(stringRes("Estimate")) {}, + restore = ButtonState(stringRes("Restore")) {} + ) + ) + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightViewModel.kt new file mode 100644 index 000000000..708566c93 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/height/RestoreBDHeightViewModel.kt @@ -0,0 +1,84 @@ +package co.electriccoin.zcash.ui.screen.restore.height + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.ui.NavigationRouter +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.IconButtonState +import co.electriccoin.zcash.ui.design.component.TextFieldState +import co.electriccoin.zcash.ui.design.util.stringRes +import co.electriccoin.zcash.ui.screen.restore.RestoreSeedDialogState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +class RestoreBDHeightViewModel( + private val navigationRouter: NavigationRouter +) : ViewModel() { + private val blockHeightText = MutableStateFlow("") + + private val isDialogVisible = MutableStateFlow(false) + + val dialogState = + isDialogVisible + .map { isDialogVisible -> + RestoreSeedDialogState( + ::onCloseDialogClick + ).takeIf { isDialogVisible } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = null + ) + + val state: StateFlow = + blockHeightText + .map { text -> + createState(text) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = createState(blockHeightText.value) + ) + + private fun createState(blockHeight: String) = + RestoreBDHeightState( + onBack = ::onBack, + dialogButton = IconButtonState(icon = R.drawable.ic_info, onClick = ::onInfoButtonClick), + restore = ButtonState(stringRes(R.string.restore_bd_restore_btn), onClick = ::onRestoreClick), + estimate = ButtonState(stringRes(R.string.restore_bd_height_btn), onClick = ::onEstimateClick), + blockHeight = TextFieldState(stringRes(blockHeight), onValueChange = ::onValueChanged) + ) + + private fun onEstimateClick() { + // do nothing + } + + private fun onRestoreClick() { + // do nothing + } + + private fun onBack() { + navigationRouter.back() + } + + private fun onInfoButtonClick() { + isDialogVisible.update { true } + } + + private fun onCloseDialogClick() { + isDialogVisible.update { false } + } + + private fun onValueChanged(string: String) { + blockHeightText.update { string } + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/ParseResult.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/ParseResult.kt deleted file mode 100644 index 33c5915c4..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/ParseResult.kt +++ /dev/null @@ -1,79 +0,0 @@ -package co.electriccoin.zcash.ui.screen.restore.model - -import cash.z.ecc.android.sdk.model.SeedPhrase -import co.electriccoin.zcash.ui.common.extension.first -import java.util.Locale - -internal sealed class ParseResult { - object Continue : ParseResult() { - override fun toString() = "Continue" - } - - data class Add(val words: List) : ParseResult() { - // Override to prevent logging of user secrets - override fun toString() = "Add" - } - - data class Autocomplete(val suggestions: List) : ParseResult() { - // Override to prevent logging of user secrets - override fun toString() = "Autocomplete" - } - - data class Warn(val suggestions: List) : ParseResult() { - // Override to prevent logging of user secrets - override fun toString() = "Warn" - } - - companion object { - @Suppress("ReturnCount") - fun new( - completeWordList: Set, - rawInput: String - ): ParseResult { - // Note: This assumes the word list is English words, thus the Locale.US is intended. - val trimmed = rawInput.lowercase(Locale.US).trim() - - if (trimmed.isBlank()) { - return Continue - } - - val autocomplete = completeWordList.filter { it.startsWith(trimmed) } - - // we accept the word only in case that there is no other available - if (completeWordList.contains(trimmed) && autocomplete.size == 1) { - return Add(listOf(trimmed)) - } - - if (autocomplete.isNotEmpty()) { - return Autocomplete(autocomplete) - } - - val multiple = - trimmed.split(SeedPhrase.DEFAULT_DELIMITER) - .filter { completeWordList.contains(it) } - .first(SeedPhrase.SEED_PHRASE_SIZE) - if (multiple.isNotEmpty()) { - return Add(multiple) - } - - return Warn(findSuggestions(trimmed, completeWordList)) - } - } - - override fun toString(): String { - return "ParseResult()" - } -} - -internal fun findSuggestions( - input: String, - completeWordList: Set -): List { - return if (input.isBlank()) { - emptyList() - } else { - completeWordList.filter { it.startsWith(input) }.ifEmpty { - findSuggestions(input.dropLast(1), completeWordList) - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/RestoreStage.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/RestoreStage.kt deleted file mode 100644 index 4aa14768d..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/model/RestoreStage.kt +++ /dev/null @@ -1,28 +0,0 @@ -package co.electriccoin.zcash.ui.screen.restore.model - -enum class RestoreStage { - // Note: the ordinal order is used to manage progression through each stage - // so be careful if reordering these - Seed, - Birthday; - - /** - * @see getPrevious - */ - fun hasPrevious() = ordinal > 0 - - /** - * @see getNext - */ - fun hasNext() = ordinal < entries.size - 1 - - /** - * @return Previous item in ordinal order. Returns the first item when it cannot go further back. - */ - fun getPrevious() = entries[maxOf(0, ordinal - 1)] - - /** - * @return Last item in ordinal order. Returns the last item when it cannot go further forward. - */ - fun getNext() = entries[minOf(entries.size - 1, ordinal + 1)] -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/AndroidRestore.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/AndroidRestore.kt new file mode 100644 index 000000000..351266045 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/AndroidRestore.kt @@ -0,0 +1,24 @@ +package co.electriccoin.zcash.ui.screen.restore.seed + +import androidx.activity.compose.BackHandler +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.electriccoin.zcash.ui.screen.restore.RestoreSeedDialog +import kotlinx.serialization.Serializable +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AndroidRestoreSeed() { + val vm = koinViewModel() + val state by vm.state.collectAsStateWithLifecycle() + val dialogState by vm.dialogState.collectAsStateWithLifecycle() + state?.let { RestoreSeedView(it) } + BackHandler { state?.onBack?.invoke() } + RestoreSeedDialog(dialogState) +} + +@Serializable +data object RestoreSeed diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedState.kt new file mode 100644 index 000000000..069e2f35c --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedState.kt @@ -0,0 +1,12 @@ +package co.electriccoin.zcash.ui.screen.restore.seed + +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.IconButtonState +import co.electriccoin.zcash.ui.design.component.SeedTextFieldState + +class RestoreSeedState( + val seed: SeedTextFieldState, + val onBack: () -> Unit, + val dialogButton: IconButtonState, + val nextButton: ButtonState +) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreTag.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedTag.kt similarity index 80% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreTag.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedTag.kt index 9cb6f4e62..06f1abf68 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/RestoreTag.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedTag.kt @@ -1,9 +1,9 @@ -package co.electriccoin.zcash.ui.screen.restore +package co.electriccoin.zcash.ui.screen.restore.seed /** * These are only used for automated testing. */ -object RestoreTag { +object RestoreSeedTag { const val SEED_WORD_TEXT_FIELD = "seed_text_field" const val BIRTHDAY_TEXT_FIELD = "birthday_text_field" const val CHIP_LAYOUT = "chip_group" diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedView.kt new file mode 100644 index 000000000..a03d61714 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedView.kt @@ -0,0 +1,142 @@ +@file:Suppress("TooManyFunctions") + +package co.electriccoin.zcash.ui.screen.restore.seed + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarTags +import co.electriccoin.zcash.ui.design.component.BlankBgScaffold +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.IconButtonState +import co.electriccoin.zcash.ui.design.component.SeedTextFieldState +import co.electriccoin.zcash.ui.design.component.SeedWordTextFieldState +import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiIconButton +import co.electriccoin.zcash.ui.design.component.ZashiSeedTextField +import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar +import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.orDark +import co.electriccoin.zcash.ui.design.util.scaffoldPadding +import co.electriccoin.zcash.ui.design.util.stringRes + +@Composable +fun RestoreSeedView(state: RestoreSeedState) { + BlankBgScaffold( + topBar = { AppBar(state) }, + bottomBar = {}, + content = { padding -> + Content( + state = state, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .scaffoldPadding(padding) + ) + } + ) +} + +@Composable +private fun Content( + state: RestoreSeedState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + ) { + Text( + text = stringResource(R.string.restore_subtitle), + style = ZashiTypography.header6, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.restore_message), + style = ZashiTypography.textSm, + color = ZashiColors.Text.textPrimary + ) + Spacer(Modifier.height(20.dp)) + ZashiSeedTextField( + state = state.seed + ) + Spacer(Modifier.weight(1f)) + Spacer(Modifier.height(24.dp)) + ZashiButton( + state.nextButton, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun AppBar(state: RestoreSeedState) { + ZashiSmallTopAppBar( + title = stringResource(R.string.restore_title), + navigationAction = { + ZashiTopAppBarBackNavigation( + onBack = state.onBack, + modifier = Modifier.testTag(ZashiTopAppBarTags.BACK) + ) + }, + regularActions = { + ZashiIconButton(state.dialogButton, modifier = Modifier.size(40.dp)) + Spacer(Modifier.width(20.dp)) + }, + colors = + ZcashTheme.colors.topAppBarColors orDark + ZcashTheme.colors.topAppBarColors.copyColors( + containerColor = Color.Transparent + ), + ) +} + +@PreviewScreens +@Composable +private fun Preview() = + ZcashTheme { + RestoreSeedView( + state = + RestoreSeedState( + seed = + SeedTextFieldState( + values = + (1..24).map { + SeedWordTextFieldState( + value = stringRes("Word"), + onValueChange = { }, + isError = false + ) + } + ), + onBack = {}, + dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {}, + nextButton = + ButtonState( + text = stringRes("Next"), + onClick = {} + ) + ) + ) + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedViewModel.kt new file mode 100644 index 000000000..f1e3bda13 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/seed/RestoreSeedViewModel.kt @@ -0,0 +1,122 @@ +package co.electriccoin.zcash.ui.screen.restore.seed + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.ui.NavigationRouter +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.IconButtonState +import co.electriccoin.zcash.ui.design.component.SeedTextFieldState +import co.electriccoin.zcash.ui.design.component.SeedWordTextFieldState +import co.electriccoin.zcash.ui.design.util.stringRes +import co.electriccoin.zcash.ui.screen.restore.RestoreSeedDialogState +import co.electriccoin.zcash.ui.screen.restore.height.RestoreBDHeight +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +class RestoreSeedViewModel( + private val navigationRouter: NavigationRouter +) : ViewModel() { + + @Suppress("MagicNumber") + private val seedWords = + MutableStateFlow( + (0..23).map { index -> + SeedWordTextFieldState( + value = stringRes(""), + onValueChange = { onValueChange(index, it) }, + isError = false + ) + } + ) + + private val isDialogVisible = MutableStateFlow(false) + + val dialogState = + isDialogVisible + .map { isDialogVisible -> + RestoreSeedDialogState( + ::onCloseDialogClick + ).takeIf { isDialogVisible } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = null + ) + + val state: StateFlow = + seedWords + .map { words -> + createState(words) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = null + ) + + // /** + // * The complete word list that the user can choose from; useful for autocomplete + // */ + // val completeWordList = + // // This is a hack to prevent disk IO on the main thread + // flow { + // // Using IO context because of https://github.com/Electric-Coin-Company/kotlin-bip39/issues/13 + // val completeWordList = + // withContext(Dispatchers.IO) { + // Mnemonics.getCachedWords(Locale.ENGLISH.language) + // } + // + // emit(CompleteWordSetState.Loaded(completeWordList.toPersistentSet())) + // }.stateIn( + // viewModelScope, + // SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + // CompleteWordSetState.Loading + // ) + + private fun createState(words: List) = + RestoreSeedState( + seed = SeedTextFieldState(values = words), + onBack = ::onBack, + dialogButton = IconButtonState(icon = R.drawable.ic_info, onClick = ::onInfoButtonClick), + nextButton = + ButtonState( + stringRes(R.string.restore_button), + onClick = ::onNextClicked + ) + ) + + private fun onBack() { + navigationRouter.back() + } + + private fun onInfoButtonClick() { + isDialogVisible.update { true } + } + + private fun onNextClicked() { + navigationRouter.forward(RestoreBDHeight) + } + + private fun onValueChange( + index: Int, + value: String + ) { + seedWords.update { + val newSeedWords = it.toMutableList() + newSeedWords[index] = newSeedWords[index].copy(value = stringRes(value)) + newSeedWords.toList() + } + } + + private fun onCloseDialogClick() { + isDialogVisible.update { false } + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/RestoreState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/RestoreState.kt deleted file mode 100644 index 384496ffa..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/RestoreState.kt +++ /dev/null @@ -1,24 +0,0 @@ -package co.electriccoin.zcash.ui.screen.restore.state - -import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -/** - * @param initialState Allows restoring the state from a different starting point. This is - * primarily useful on Android, for automated tests, and for iterative debugging with the Compose - * layout preview. The default constructor argument is generally fine for other platforms. - */ -class RestoreState(initialState: RestoreStage = RestoreStage.values().first()) { - private val mutableState = MutableStateFlow(initialState) - - val current: StateFlow = mutableState - - fun goNext() { - mutableState.value = current.value.getNext() - } - - fun goPrevious() { - mutableState.value = current.value.getPrevious() - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/WordList.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/WordList.kt deleted file mode 100644 index a8a48bc28..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/state/WordList.kt +++ /dev/null @@ -1,47 +0,0 @@ -package co.electriccoin.zcash.ui.screen.restore.state - -import cash.z.ecc.android.sdk.model.SeedPhrase -import cash.z.ecc.sdk.model.SeedPhraseValidation -import co.electriccoin.zcash.ui.common.extension.first -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map - -class WordList(initial: List = emptyList()) { - private val mutableState: MutableStateFlow> = MutableStateFlow(initial.toPersistentList()) - - val current: StateFlow> = mutableState - - fun set(list: List) { - mutableState.value = list.toPersistentList() - } - - fun append(words: List) { - val newList = - (current.value + words) - .first(SeedPhrase.SEED_PHRASE_SIZE) // Prevent pasting too many words - .toPersistentList() - - 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 - override fun toString() = "WordList" -} - -fun WordList.wordValidation() = - current - .map { SeedPhraseValidation.new(it) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt deleted file mode 100644 index 97323effd..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt +++ /dev/null @@ -1,859 +0,0 @@ -@file:Suppress("TooManyFunctions") - -package co.electriccoin.zcash.ui.screen.restore.view - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cash.z.ecc.android.sdk.model.BlockHeight -import cash.z.ecc.android.sdk.model.ZcashNetwork -import cash.z.ecc.sdk.model.SeedPhraseValidation -import co.electriccoin.zcash.spackle.Twig -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.SecureScreen -import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen -import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT -import co.electriccoin.zcash.ui.design.component.BlankBgScaffold -import co.electriccoin.zcash.ui.design.component.Body -import co.electriccoin.zcash.ui.design.component.ChipOnSurface -import co.electriccoin.zcash.ui.design.component.FormTextField -import co.electriccoin.zcash.ui.design.component.Reference -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation -import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle -import co.electriccoin.zcash.ui.design.component.ZashiButton -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors -import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions -import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography -import co.electriccoin.zcash.ui.design.util.scaffoldPadding -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.RestoreStage -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.wordValidation -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.persistentHashSetOf -import kotlinx.coroutines.delay - -@Preview -@Composable -private fun RestoreSeedPreview() { - ZcashTheme(forceDarkMode = false) { - RestoreWallet( - ZcashNetwork.Mainnet, - restoreState = RestoreState(RestoreStage.Seed), - completeWordList = - persistentHashSetOf( - "abandon", - "ability", - "able", - "about", - "above", - "absent", - "absorb", - "abstract", - "rib", - "ribbon" - ), - userWordList = WordList(listOf("abandon", "absorb")), - restoreHeight = null, - setRestoreHeight = {}, - onBack = {}, - paste = { "" }, - onFinished = {} - ) - } -} - -@Preview -@Composable -private fun RestoreSeedDarkPreview() { - ZcashTheme(forceDarkMode = true) { - RestoreWallet( - ZcashNetwork.Mainnet, - restoreState = RestoreState(RestoreStage.Seed), - completeWordList = - persistentHashSetOf( - "abandon", - "ability", - "able", - "about", - "above", - "absent", - "absorb", - "abstract", - "rib", - "ribbon" - ), - userWordList = WordList(listOf("abandon", "absorb")), - restoreHeight = null, - setRestoreHeight = {}, - onBack = {}, - paste = { "" }, - onFinished = {} - ) - } -} - -@Preview -@Composable -private fun RestoreBirthdayPreview() { - ZcashTheme(forceDarkMode = false) { - RestoreWallet( - 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 = {} - ) - } -} - -@Preview -@Composable -private fun RestoreBirthdayDarkPreview() { - ZcashTheme(forceDarkMode = true) { - RestoreWallet( - 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 = {} - ) - } -} - -/** - * Note that the restore review doesn't allow the user to go back once the seed is entered correctly. - * - * @param restoreHeight A null height indicates no user input. - */ -@Suppress("LongParameterList", "LongMethod") -@Composable -fun RestoreWallet( - zcashNetwork: ZcashNetwork, - restoreState: RestoreState, - completeWordList: ImmutableSet, - userWordList: WordList, - restoreHeight: BlockHeight?, - setRestoreHeight: (BlockHeight?) -> Unit, - onBack: () -> Unit, - paste: () -> String?, - onFinished: () -> Unit -) { - var text by rememberSaveable { mutableStateOf("") } - val parseResult = ParseResult.new(completeWordList, text) - - val currentStage = restoreState.current.collectAsStateWithLifecycle().value - - var isSeedValid by rememberSaveable { mutableStateOf(false) } - - BackHandler { - onBack() - } - - // To avoid unnecessary recompositions that this flow produces - LaunchedEffect(Unit) { - userWordList.wordValidation().collect { - if (it is SeedPhraseValidation.Valid) { - // TODO [#1522]: temporary fix to wait for other states to update first - // TODO [#1522]: https://github.com/Electric-Coin-Company/zashi-android/issues/1522 - @Suppress("MagicNumber") - delay(100) - isSeedValid = true - } else { - isSeedValid = false - } - } - } - - BlankBgScaffold( - modifier = Modifier.navigationBarsPadding(), - topBar = { - when (currentStage) { - RestoreStage.Seed -> { - RestoreSeedTopAppBar( - onBack = onBack, - onClear = { - userWordList.set(emptyList()) - text = "" - } - ) - } - - RestoreStage.Birthday -> { - RestoreSeedBirthdayTopAppBar( - onBack = { - if (currentStage.hasPrevious()) { - restoreState.goPrevious() - } else { - onBack() - } - } - ) - } - } - }, - bottomBar = { - when (currentStage) { - RestoreStage.Seed -> { - RestoreSeedBottomBar( - userWordList = userWordList, - isSeedValid = isSeedValid, - parseResult = parseResult, - setText = { text = it }, - modifier = - Modifier - .imePadding() - .navigationBarsPadding() - .animateContentSize() - .fillMaxWidth() - ) - } - - RestoreStage.Birthday -> { - // No content. The action button is part of scrollable content. - } - } - }, - content = { paddingValues -> - val commonModifier = - Modifier - .scaffoldPadding(paddingValues) - - when (currentStage) { - RestoreStage.Seed -> { - if (shouldSecureScreen) { - SecureScreen() - } - RestoreSeedMainContent( - userWordList = userWordList, - isSeedValid = isSeedValid, - text = text, - setText = { text = it }, - parseResult = parseResult, - paste = paste, - goNext = { restoreState.goNext() }, - modifier = commonModifier - ) - } - - RestoreStage.Birthday -> { - RestoreBirthdayMainContent( - zcashNetwork = zcashNetwork, - initialRestoreHeight = restoreHeight, - setRestoreHeight = setRestoreHeight, - onDone = onFinished, - modifier = - commonModifier - .imePadding() - .navigationBarsPadding() - ) - } - } - } - ) -} - -@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.padding(all = ZcashTheme.dimens.spacingDefault) - ) -} - -@Composable -private fun RestoreSeedTopAppBar( - onBack: () -> Unit, - onClear: () -> Unit, - modifier: Modifier = Modifier, -) { - SmallTopAppBar( - modifier = modifier, - navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation).uppercase(), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), - onBack = onBack - ) - }, - regularActions = { - ClearSeedMenuItem( - onSeedClear = onClear - ) - }, - ) -} - -@Composable -private fun RestoreSeedBirthdayTopAppBar( - onBack: () -> Unit, - modifier: Modifier = Modifier, -) { - SmallTopAppBar( - modifier = modifier, - navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation).uppercase(), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), - onBack = onBack - ) - }, - ) -} - -@Suppress("UNUSED_PARAMETER", "LongParameterList", "LongMethod") -@Composable -private fun RestoreSeedMainContent( - userWordList: WordList, - isSeedValid: Boolean, - text: String, - setText: (String) -> Unit, - parseResult: ParseResult, - paste: () -> String?, - goNext: () -> Unit, - modifier: Modifier = Modifier, -) { - val focusManager = LocalFocusManager.current - val scrollState = rememberScrollState() - val focusRequester = remember { FocusRequester() } - val textFieldScrollToHeight = rememberSaveable { mutableIntStateOf(0) } - - if (parseResult is ParseResult.Add) { - setText("") - userWordList.append(parseResult.words) - } - - Column( - Modifier - .fillMaxHeight() - .verticalScroll(scrollState) - .then(modifier), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Used to calculate necessary scroll to have the seed TextField 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(ZcashTheme.dimens.spacingDefault)) - - Body( - text = stringResource(id = R.string.restore_seed_instructions), - textAlign = TextAlign.Center - ) - } - - Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) - - SeedGridWithText( - text = text, - userWordList = userWordList, - focusRequester = focusRequester, - parseResult = parseResult, - setText = setText - ) - - Spacer( - modifier = - Modifier - .fillMaxHeight() - .weight(MINIMAL_WEIGHT) - ) - - Spacer(Modifier.height(ZcashTheme.dimens.spacingLarge)) - - ZashiButton( - onClick = goNext, - enabled = isSeedValid, - text = stringResource(id = R.string.restore_seed_button_next), - modifier = Modifier.fillMaxWidth() - ) - } - - LaunchedEffect(isSeedValid) { - if (isSeedValid) { - // Clear focus and hide keyboard to make it easier for users to see the next button - focusManager.clearFocus() - } - } - - LaunchedEffect(parseResult) { - // Causes the TextField to refocus - if (!isSeedValid) { - focusRequester.requestFocus() - } - if (text.isNotEmpty() && userWordList.current.value.isEmpty()) { - scrollState.animateScrollTo(textFieldScrollToHeight.intValue) - } - } -} - -@Composable -private fun RestoreSeedBottomBar( - userWordList: WordList, - isSeedValid: Boolean, - parseResult: ParseResult, - setText: (String) -> Unit, - modifier: Modifier = Modifier, -) { - // 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 - if (!isSeedValid) { - Column( - modifier = modifier - ) { - Warn( - parseResult = parseResult, - modifier = - Modifier - .fillMaxWidth() - .padding( - horizontal = ZcashTheme.dimens.spacingDefault, - vertical = ZcashTheme.dimens.spacingSmall - ) - ) - Autocomplete(parseResult = parseResult, { - setText("") - userWordList.append(listOf(it)) - }) - } - } -} - -@Composable -@Suppress("LongParameterList", "LongMethod") -private fun SeedGridWithText( - text: String, - setText: (String) -> Unit, - userWordList: WordList, - focusRequester: FocusRequester, - parseResult: ParseResult, - modifier: Modifier = Modifier -) { - val currentUserWordList = userWordList.current.collectAsStateWithLifecycle().value - - val currentSeedText = - currentUserWordList.run { - if (isEmpty()) { - text - } else { - joinToString(separator = " ", postfix = " ").plus(text) - } - } - - Surface( - modifier = modifier, - shape = RoundedCornerShape(ZashiDimensions.Radius.radius2xl), - border = BorderStroke(1.dp, ZashiColors.Modals.surfaceStroke), - color = ZashiColors.Modals.surfacePrimary - ) { - Column( - modifier = - Modifier - .border( - border = - BorderStroke( - width = ZcashTheme.dimens.layoutStroke, - color = ZcashTheme.colors.layoutStroke - ) - ) - .fillMaxWidth() - .defaultMinSize(minHeight = ZcashTheme.dimens.textFieldSeedPanelDefaultHeight) - .then(modifier) - .testTag(RestoreTag.CHIP_LAYOUT) - ) { - /* - * Treat the user input as a password for more secure input, but disable the transformation - * to obscure typing. - */ - TextField( - textStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium), - modifier = - Modifier - .fillMaxWidth() - .testTag(RestoreTag.SEED_WORD_TEXT_FIELD) - .focusRequester(focusRequester), - value = - TextFieldValue( - text = currentSeedText, - selection = TextRange(index = currentSeedText.length) - ), - placeholder = { - Text( - text = stringResource(id = R.string.restore_seed_hint), - style = ZashiTypography.textMd, - color = ZashiColors.Inputs.Default.text - ) - }, - onValueChange = { - processTextInput( - currentSeedText = currentSeedText, - updateSeedText = it.text, - userWordList = userWordList, - setText = setText - ) - }, - keyboardOptions = - KeyboardOptions( - KeyboardCapitalization.None, - autoCorrectEnabled = false, - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Password - ), - keyboardActions = KeyboardActions(onAny = {}), - isError = parseResult is ParseResult.Warn, - colors = - TextFieldDefaults.colors( - cursorColor = ZcashTheme.colors.textPrimary, - disabledContainerColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedTextColor = ZashiColors.Inputs.Filled.text, - unfocusedTextColor = ZashiColors.Inputs.Filled.text, - ) - ) - } - } -} - -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 -@Suppress("UNUSED_VARIABLE") -private fun Autocomplete( - parseResult: ParseResult, - onSuggestionSelected: (String) -> Unit, - 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) { - is ParseResult.Autocomplete -> { - Pair(false, parseResult.suggestions) - } - - is ParseResult.Warn -> { - return - } - - else -> { - Pair(false, null) - } - } - suggestions?.let { - LazyRow( - modifier = - modifier - .testTag(RestoreTag.AUTOCOMPLETE_LAYOUT) - .fillMaxWidth(), - contentPadding = PaddingValues(all = ZcashTheme.dimens.spacingSmall), - horizontalArrangement = Arrangement.Absolute.Center - ) { - items(it) { - ChipOnSurface( - text = it, - onClick = { onSuggestionSelected(it) }, - modifier = Modifier.testTag(RestoreTag.AUTOCOMPLETE_ITEM) - ) - } - } - } -} - -@Composable -private fun Warn( - parseResult: ParseResult, - modifier: Modifier = Modifier -) { - if (parseResult is ParseResult.Warn) { - Surface( - shape = RoundedCornerShape(size = ZcashTheme.dimens.tinyRippleEffectCorner), - modifier = - modifier.then( - Modifier.border( - border = - BorderStroke( - width = ZcashTheme.dimens.chipStroke, - color = ZcashTheme.colors.layoutStrokeSecondary - ), - shape = RoundedCornerShape(size = ZcashTheme.dimens.tinyRippleEffectCorner), - ) - ), - color = ZcashTheme.colors.primaryColor, - shadowElevation = ZcashTheme.dimens.chipShadowElevation - ) { - Text( - color = ZcashTheme.colors.textPrimary, - modifier = - Modifier - .fillMaxWidth() - .padding(ZcashTheme.dimens.spacingSmall), - textAlign = TextAlign.Center, - text = - if (parseResult.suggestions.isEmpty()) { - stringResource(id = R.string.restore_seed_warning_no_suggestions) - } else { - stringResource(id = R.string.restore_seed_warning_suggestions) - } - ) - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -@Suppress("LongMethod") -private fun RestoreBirthdayMainContent( - zcashNetwork: ZcashNetwork, - initialRestoreHeight: BlockHeight?, - setRestoreHeight: (BlockHeight?) -> Unit, - onDone: () -> Unit, - modifier: Modifier = Modifier -) { - val scrollState = rememberScrollState() - val focusRequester = remember { FocusRequester() } - - val (height, setHeight) = - rememberSaveable { - mutableStateOf(initialRestoreHeight?.value?.toString() ?: "") - } - - Column( - Modifier - .fillMaxHeight() - .verticalScroll(scrollState) - .then(modifier), - horizontalAlignment = Alignment.CenterHorizontally - ) { - TopScreenLogoTitle( - title = stringResource(R.string.restore_birthday_header), - logoContentDescription = stringResource(R.string.zcash_logo_content_description), - ) - - Body(stringResource(R.string.restore_birthday_sub_header)) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) - - FormTextField( - value = height, - onValueChange = { heightString -> - val filteredHeightString = heightString.filter { it.isDigit() } - setHeight(filteredHeightString) - }, - colors = - TextFieldDefaults.colors( - cursorColor = ZcashTheme.colors.textPrimary, - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - errorContainerColor = Color.Transparent, - focusedIndicatorColor = ZcashTheme.colors.secondaryDividerColor, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), - modifier = - Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - textStyle = ZcashTheme.extendedTypography.textFieldBirthday, - keyboardOptions = - KeyboardOptions( - KeyboardCapitalization.None, - autoCorrectEnabled = false, - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number - ), - keyboardActions = KeyboardActions(onAny = {}), - withBorder = false, - testTag = RestoreTag.BIRTHDAY_TEXT_FIELD - ) - - Spacer( - modifier = - Modifier - .fillMaxHeight() - .weight(MINIMAL_WEIGHT) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) - - // 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 - } ?: false - - val isEmptyBirthday = height.isEmpty() - - ZashiButton( - onClick = { - if (isEmptyBirthday) { - setRestoreHeight(null) - } else if (isBirthdayValid) { - setRestoreHeight(BlockHeight.new(height.toLong())) - } else { - error("The restore button should not expect click events") - } - onDone() - }, - text = stringResource(R.string.restore_birthday_button_restore), - enabled = isBirthdayValid, - modifier = Modifier.fillMaxWidth() - ) - } - - LaunchedEffect(Unit) { - // Causes the TextField to focus on the first screen visit - focusRequester.requestFocus() - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/viewmodel/RestoreViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/viewmodel/RestoreViewModel.kt deleted file mode 100644 index c7fe875e5..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/viewmodel/RestoreViewModel.kt +++ /dev/null @@ -1,111 +0,0 @@ -package co.electriccoin.zcash.ui.screen.restore.viewmodel - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import cash.z.ecc.android.bip39.Mnemonics -import cash.z.ecc.android.sdk.ext.collectWith -import cash.z.ecc.android.sdk.model.BlockHeight -import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT -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.WordList -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.WhileSubscribed -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext -import java.util.Locale - -class RestoreViewModel(application: Application, savedStateHandle: SavedStateHandle) : AndroidViewModel(application) { - val restoreState: RestoreState = - run { - val initialValue = - if (savedStateHandle.contains(KEY_STAGE)) { - savedStateHandle.get(KEY_STAGE) - } else { - null - } - - if (null == initialValue) { - RestoreState() - } else { - RestoreState(initialValue) - } - } - - /** - * The complete word list that the user can choose from; useful for autocomplete - */ - val completeWordList = - // This is a hack to prevent disk IO on the main thread - flow { - // Using IO context because of https://github.com/Electric-Coin-Company/kotlin-bip39/issues/13 - val completeWordList = - withContext(Dispatchers.IO) { - Mnemonics.getCachedWords(Locale.ENGLISH.language) - } - - emit(CompleteWordSetState.Loaded(completeWordList.toPersistentSet())) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), - CompleteWordSetState.Loading - ) - - val userWordList: WordList = - run { - val initialValue = - if (savedStateHandle.contains(KEY_WORD_LIST)) { - savedStateHandle.get>(KEY_WORD_LIST) - } else { - null - } - - if (null == initialValue) { - WordList() - } else { - WordList(initialValue) - } - } - - val userBirthdayHeight: MutableStateFlow = - run { - val initialValue: BlockHeight? = - savedStateHandle.get(KEY_BIRTHDAY_HEIGHT)?.let { - BlockHeight.new(it) - } - MutableStateFlow(initialValue) - } - - init { - // viewModelScope is constructed with Dispatchers.Main.immediate, so this will - // update the save state as soon as a change occurs. - userWordList.current.collectWith(viewModelScope) { - savedStateHandle[KEY_WORD_LIST] = ArrayList(it) - } - - userBirthdayHeight.collectWith(viewModelScope) { - savedStateHandle[KEY_BIRTHDAY_HEIGHT] = it?.value - } - } - - companion object { - private const val KEY_STAGE = "stage" // $NON-NLS - - private const val KEY_WORD_LIST = "word_list" // $NON-NLS - - private const val KEY_BIRTHDAY_HEIGHT = "birthday_height" // $NON-NLS - } -} - -sealed class CompleteWordSetState { - object Loading : CompleteWordSetState() - - data class Loaded(val list: ImmutableSet) : CompleteWordSetState() -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restoresuccess/view/RestoreSuccessView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restoresuccess/view/RestoreSuccessView.kt index 5ae1a5b8d..65b941275 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restoresuccess/view/RestoreSuccessView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restoresuccess/view/RestoreSuccessView.kt @@ -11,29 +11,41 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankSurface -import co.electriccoin.zcash.ui.design.component.LabeledCheckBox +import co.electriccoin.zcash.ui.design.component.GradientBgScaffold import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiCheckbox import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.util.scaffoldPadding +import co.electriccoin.zcash.ui.design.util.stringRes @Composable fun RestoreSuccess(state: RestoreSuccessViewState) { - BlankBgScaffold { paddingValues -> + GradientBgScaffold( + startColor = ZashiColors.Utility.WarningYellow.utilityOrange100, + endColor = ZashiColors.Surfaces.bgPrimary, + ) { paddingValues -> RestoreSuccessContent( state = state, modifier = @@ -53,16 +65,8 @@ private fun RestoreSuccessContent( ) { Column( modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig)) - - Text( - text = stringResource(id = R.string.restore_success_title), - style = ZcashTheme.typography.secondary.headlineMedium - ) - - Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge)) + Spacer(Modifier.height(64.dp)) Image( painter = painterResource(id = R.drawable.img_success_dialog), @@ -70,32 +74,74 @@ private fun RestoreSuccessContent( colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge)) + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(id = R.string.restore_success_title), + style = ZashiTypography.header6, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold + ) + + Spacer(Modifier.height(8.dp)) Text( text = stringResource(id = R.string.restore_success_subtitle), textAlign = TextAlign.Center, - style = ZcashTheme.typography.secondary.headlineSmall, - fontWeight = FontWeight.SemiBold + style = ZashiTypography.textMd, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.Medium ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge)) + Spacer(Modifier.height(16.dp)) Text( text = stringResource(id = R.string.restore_success_description), - style = ZcashTheme.typography.secondary.bodySmall, + style = ZashiTypography.textSm, + color = ZashiColors.Text.textPrimary, ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingLarge)) + val bulletString = "\u2022 " + val bulletTextStyle = ZashiTypography.textSm + val bulletTextMeasurer = rememberTextMeasurer() + val bulletStringWidth = + remember(bulletTextStyle, bulletTextMeasurer) { + bulletTextMeasurer.measure(text = bulletString, style = bulletTextStyle).size.width + } + val bulletRestLine = with(LocalDensity.current) { bulletStringWidth.toSp() } + val bulletParagraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = bulletRestLine)) - LabeledCheckBox( - modifier = Modifier.align(Alignment.Start), - checked = state.isKeepScreenOnChecked, - onCheckedChange = { state.onCheckboxClick() }, - text = stringResource(id = R.string.restoring_initial_dialog_checkbox) + Spacer(Modifier.height(4.dp)) + + val bulletText1 = stringResource(R.string.restore_success_bullet_1) + Text( + text = + buildAnnotatedString { + withStyle(style = bulletParagraphStyle) { + append(bulletString) + append(bulletText1) + } + }, + style = ZashiTypography.textSm, + color = ZashiColors.Text.textPrimary, ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingLarge)) + Spacer(Modifier.height(2.dp)) + + val bulletText2 = stringResource(R.string.restore_success_bullet_2) + Text( + text = + buildAnnotatedString { + withStyle(style = bulletParagraphStyle) { + append(bulletString) + append(bulletText2) + } + }, + style = ZashiTypography.textSm, + color = ZashiColors.Text.textPrimary, + ) + + Spacer(Modifier.weight(1f)) Text( text = @@ -106,12 +152,23 @@ private fun RestoreSuccessContent( append(" ") append(stringResource(id = R.string.restore_success_note_part_2)) }, - style = ZcashTheme.extendedTypography.footnote, + style = ZashiTypography.textXs, + color = ZashiColors.Text.textPrimary, ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingBig)) + Spacer(Modifier.height(14.dp)) - Spacer(Modifier.weight(1f)) + ZashiCheckbox( + modifier = Modifier.align(Alignment.Start), + isChecked = state.isKeepScreenOnChecked, + onClick = state.onCheckboxClick, + text = stringRes(R.string.restoring_initial_dialog_checkbox), + style = ZashiTypography.textMd, + fontWeight = FontWeight.Medium, + color = ZashiColors.Text.textPrimary, + ) + + Spacer(Modifier.height(14.dp)) ZashiButton( modifier = Modifier.fillMaxWidth(), diff --git a/ui-lib/src/main/res/ui/common/drawable-night/ic_info.xml b/ui-lib/src/main/res/ui/common/drawable-night/ic_info.xml new file mode 100644 index 000000000..3bc99a688 --- /dev/null +++ b/ui-lib/src/main/res/ui/common/drawable-night/ic_info.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/ui-lib/src/main/res/ui/common/drawable/ic_info.xml b/ui-lib/src/main/res/ui/common/drawable/ic_info.xml new file mode 100644 index 000000000..3b4ce8863 --- /dev/null +++ b/ui-lib/src/main/res/ui/common/drawable/ic_info.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/ui-lib/src/main/res/ui/restore/drawable/ic_restore_dialog.xml b/ui-lib/src/main/res/ui/restore/drawable/ic_restore_dialog.xml new file mode 100644 index 000000000..222f46289 --- /dev/null +++ b/ui-lib/src/main/res/ui/restore/drawable/ic_restore_dialog.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui-lib/src/main/res/ui/restore/values-es/strings.xml b/ui-lib/src/main/res/ui/restore/values-es/strings.xml index 98450d7d2..d781ec5f1 100644 --- a/ui-lib/src/main/res/ui/restore/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/restore/values-es/strings.xml @@ -1,16 +1,4 @@ - Borrar Semilla - Ingresa la frase secreta de recuperación - Ingresa tu frase de semilla de 24 palabras para restaurar la billetera asociada. - privacidad dignidad libertad … - Siguiente - - Esta palabra no está en el diccionario de frases semilla. Por favor, selecciona la correcta de las sugerencias. - Esta palabra no está en el diccionario de frases semilla. - - Altura de cumpleaños de la billetera - (opcional) - Restaurar diff --git a/ui-lib/src/main/res/ui/restore/values/strings.xml b/ui-lib/src/main/res/ui/restore/values/strings.xml index fcc694277..7206eb999 100644 --- a/ui-lib/src/main/res/ui/restore/values/strings.xml +++ b/ui-lib/src/main/res/ui/restore/values/strings.xml @@ -1,16 +1,29 @@ - Clear Seed + Restore + Seed Recovery Phrase + Please type in your 24-word secret recovery phrase in the correct order. + Next - Enter secret recovery phrase - Enter your 24-word seed phrase to restore the associated wallet. - privacy dignity freedom … - Next + Need to know more? + The Secret Recovery Phrase + is a unique set of 24 words, appearing in a + precise order. It can be used to gain full control of your funds from any device via any Zcash wallet app. + Think of it as the master key to your wallet. It is stored in Zashi’s Advanced Settings. + The Wallet Birthday Height + is the block height (block # in the + blockchain) at which your wallet was created. If you ever lose access to your Zashi app and need to recover + your funds, providing the block height along with your recovery phrase can significantly speed up the process. + + Got it! - This word is not in the seed phrase dictionary. Please select the correct one from the suggestions. - This word is not in the seed phrase dictionary. + Estimate my block height + Restore + + Wallet Birthday Height + Entering your Wallet Birthday Height helps speed up the restore process. + Block Height + Enter number + Wallet Birthday Height is the point in time when your wallet was created. - Wallet birthday height - (optional) - Restore diff --git a/ui-lib/src/main/res/ui/restore_success/values-es/strings.xml b/ui-lib/src/main/res/ui/restore_success/values-es/strings.xml index 4f036d782..88c743c31 100644 --- a/ui-lib/src/main/res/ui/restore_success/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/restore_success/values-es/strings.xml @@ -1,10 +1,10 @@ ¡Mantén Zashi abierto! - Tu billetera ha sido restaurada con éxito y ahora se está sincronizando - Para evitar interrupciones, mantén la pantalla encendida, conecta tu dispositivo a una fuente de energía y guárdalo en un lugar seguro. + Your wallet is being restored. + Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption: Mantener la pantalla encendida mientras se restaura. Nota: - Durante la sincronización inicial, tus fondos no se pueden enviar ni gastar. Dependiendo de la antigüedad de tu billetera, la sincronización completa puede tomar algunas horas. + Your funds cannot be spent with Zashi until your wallet is fully restored. ¡Entendido! diff --git a/ui-lib/src/main/res/ui/restore_success/values/strings.xml b/ui-lib/src/main/res/ui/restore_success/values/strings.xml index a8e1b9f85..e353bd127 100644 --- a/ui-lib/src/main/res/ui/restore_success/values/strings.xml +++ b/ui-lib/src/main/res/ui/restore_success/values/strings.xml @@ -1,11 +1,12 @@ Keep Zashi open! - Your wallet has been successfully restored and is now syncing - To prevent interruption, keep your screen awake, plug your device into a power source, and keep it in a secure place. + Your wallet is being restored. + Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption: Keep screen on while restoring. Note: - During the initial sync your funds cannot be sent or - spent. Depending on the age of your wallet, it may take a few hours to fully sync. + Your funds cannot be spent with Zashi until your wallet is fully restored. Got it! + Keep the Zashi app open on an active phone screen. + To prevent your phone screen from going dark, turn off power-saving mode and keep your phone plugged in. diff --git a/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt b/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt index fe5700d8c..c18879357 100644 --- a/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt +++ b/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt @@ -46,8 +46,8 @@ import co.electriccoin.zcash.ui.design.component.UiMode import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants.WELCOME_ANIM_TEST_TAG import co.electriccoin.zcash.ui.screen.balances.BalanceTag import co.electriccoin.zcash.ui.screen.home.HomeTags -import co.electriccoin.zcash.ui.screen.restore.RestoreTag -import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag +import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedViewModel import co.electriccoin.zcash.ui.screen.securitywarning.view.SecurityScreenTag.ACKNOWLEDGE_CHECKBOX_TAG import co.electriccoin.zcash.ui.screen.send.SendTag import kotlinx.coroutines.Dispatchers @@ -210,7 +210,7 @@ class ScreenshotTest : UiTestPrerequisites() { val seedPhraseSplitLength = SeedPhraseFixture.new().split.size SeedPhraseFixture.new().split.forEachIndexed { index, string -> - composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also { + composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also { it.performTextInput(string) // Take a screenshot half-way through filling in the seed phrase @@ -221,7 +221,7 @@ class ScreenshotTest : UiTestPrerequisites() { } composeTestRule.waitUntil { - composeTestRule.activity.viewModels().value.userWordList.current.value.size == + composeTestRule.activity.viewModels().value.userWordList.current.value.size == SeedPhrase.SEED_PHRASE_SIZE }