Restore redesign
This commit is contained in:
parent
8c6d873d04
commit
cee77ea0f9
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SeedWordTextFieldState>,
|
||||
)
|
||||
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
private fun Preview() =
|
||||
ZcashTheme {
|
||||
BlankSurface {
|
||||
ZashiSeedTextField(
|
||||
state =
|
||||
SeedTextFieldState(
|
||||
values =
|
||||
(1..24).map {
|
||||
SeedWordTextFieldState(
|
||||
value = stringRes("Word"),
|
||||
onValueChange = { },
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 = {},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<Color> {
|
||||
internal fun borderColor(
|
||||
state: TextFieldState,
|
||||
isFocused: Boolean
|
||||
): State<Color> {
|
||||
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() =
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<String>
|
||||
) {
|
||||
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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,6 @@ interface WalletRepository {
|
|||
val synchronizer: StateFlow<Synchronizer?>
|
||||
val secretState: StateFlow<SecretState>
|
||||
val fastestServers: StateFlow<FastestServersState>
|
||||
val persistableWallet: Flow<PersistableWallet?>
|
||||
val onboardingState: Flow<OnboardingState>
|
||||
|
||||
val allAccounts: Flow<List<WalletAccount>?>
|
||||
|
@ -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<List<WalletAccount>?> = accountDataSource.allAccounts
|
||||
|
||||
override val secretState: StateFlow<SecretState> =
|
||||
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<PersistableWallet?> =
|
||||
secretState.map {
|
||||
(it as? SecretState.Ready?)?.persistableWallet
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
||||
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,
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<WalletViewModel>()
|
||||
val onboardingViewModel = koinActivityViewModel<OnboardingViewModel>()
|
||||
|
||||
val navigationRouter = koinInject<NavigationRouter>()
|
||||
val navController = LocalNavController.current
|
||||
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||
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> {
|
||||
Onboarding(
|
||||
isDebugMenuEnabled = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService,
|
||||
onImportWallet = onImportWallet,
|
||||
onCreateWallet = onCreateWallet,
|
||||
onFixtureWallet = onFixtureWallet
|
||||
)
|
||||
}
|
||||
composable<RestoreSeed> {
|
||||
AndroidRestoreSeed()
|
||||
}
|
||||
composable<RestoreBDHeight> {
|
||||
AndroidRestoreBDHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.screen.onboarding
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object Onboarding
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<WalletViewModel>()
|
||||
val onboardingViewModel = koinActivityViewModel<OnboardingViewModel>()
|
||||
val restoreViewModel = koinActivityViewModel<RestoreViewModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 { },
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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<RestoreBDHeightViewModel>()
|
||||
val state by vm.state.collectAsStateWithLifecycle()
|
||||
val dialogState by vm.dialogState.collectAsStateWithLifecycle()
|
||||
RestoreBDHeightView(state)
|
||||
|
||||
BackHandler {
|
||||
state.onBack()
|
||||
}
|
||||
|
||||
RestoreSeedDialog(dialogState)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object RestoreBDHeight
|
|
@ -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
|
||||
)
|
|
@ -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")) {}
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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<RestoreBDHeightState> =
|
||||
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 }
|
||||
}
|
||||
}
|
|
@ -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<String>) : ParseResult() {
|
||||
// Override to prevent logging of user secrets
|
||||
override fun toString() = "Add"
|
||||
}
|
||||
|
||||
data class Autocomplete(val suggestions: List<String>) : ParseResult() {
|
||||
// Override to prevent logging of user secrets
|
||||
override fun toString() = "Autocomplete"
|
||||
}
|
||||
|
||||
data class Warn(val suggestions: List<String>) : ParseResult() {
|
||||
// Override to prevent logging of user secrets
|
||||
override fun toString() = "Warn"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("ReturnCount")
|
||||
fun new(
|
||||
completeWordList: Set<String>,
|
||||
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<String>
|
||||
): List<String> {
|
||||
return if (input.isBlank()) {
|
||||
emptyList()
|
||||
} else {
|
||||
completeWordList.filter { it.startsWith(input) }.ifEmpty {
|
||||
findSuggestions(input.dropLast(1), completeWordList)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
}
|
|
@ -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<RestoreSeedViewModel>()
|
||||
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
|
|
@ -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
|
||||
)
|
|
@ -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"
|
|
@ -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 = {}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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<RestoreSeedState?> =
|
||||
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<CompleteWordSetState> {
|
||||
// // 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<SeedWordTextFieldState>) =
|
||||
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 }
|
||||
}
|
||||
}
|
|
@ -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<RestoreStage> = mutableState
|
||||
|
||||
fun goNext() {
|
||||
mutableState.value = current.value.getNext()
|
||||
}
|
||||
|
||||
fun goPrevious() {
|
||||
mutableState.value = current.value.getPrevious()
|
||||
}
|
||||
}
|
|
@ -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<String> = emptyList()) {
|
||||
private val mutableState: MutableStateFlow<ImmutableList<String>> = MutableStateFlow(initial.toPersistentList())
|
||||
|
||||
val current: StateFlow<ImmutableList<String>> = mutableState
|
||||
|
||||
fun set(list: List<String>) {
|
||||
mutableState.value = list.toPersistentList()
|
||||
}
|
||||
|
||||
fun append(words: List<String>) {
|
||||
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) }
|
|
@ -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<String>,
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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<RestoreStage>(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<CompleteWordSetState> {
|
||||
// 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<ArrayList<String>>(KEY_WORD_LIST)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (null == initialValue) {
|
||||
WordList()
|
||||
} else {
|
||||
WordList(initialValue)
|
||||
}
|
||||
}
|
||||
|
||||
val userBirthdayHeight: MutableStateFlow<BlockHeight?> =
|
||||
run {
|
||||
val initialValue: BlockHeight? =
|
||||
savedStateHandle.get<Long>(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<String>) : CompleteWordSetState()
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h20v20h-20z"/>
|
||||
<path
|
||||
android:pathData="M10,13.333V10M10,6.667H10.008M18.333,10C18.333,14.602 14.602,18.333 10,18.333C5.398,18.333 1.667,14.602 1.667,10C1.667,5.397 5.398,1.666 10,1.666C14.602,1.666 18.333,5.397 18.333,10Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.66667"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E8E8E8"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h20v20h-20z"/>
|
||||
<path
|
||||
android:pathData="M10,13.333V10M10,6.667H10.008M18.333,10C18.333,14.602 14.602,18.333 10,18.333C5.398,18.333 1.667,14.602 1.667,10C1.667,5.398 5.398,1.667 10,1.667C14.602,1.667 18.333,5.398 18.333,10Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.66667"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#231F20"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M9.09,9C9.325,8.332 9.789,7.768 10.4,7.409C11.011,7.05 11.729,6.919 12.427,7.039C13.125,7.158 13.759,7.522 14.215,8.064C14.671,8.606 14.921,9.292 14.92,10C14.92,12 11.92,13 11.92,13M12,17H12.01M22,12C22,17.523 17.523,22 12,22C6.477,22 2,17.523 2,12C2,6.477 6.477,2 12,2C17.523,2 22,6.477 22,12Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#231F20"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -1,16 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="restore_button_clear">Borrar Semilla</string>
|
||||
|
||||
<string name="restore_title">Ingresa la frase secreta de recuperación</string>
|
||||
<string name="restore_seed_instructions">Ingresa tu frase de semilla de 24 palabras para restaurar la billetera asociada.</string>
|
||||
<string name="restore_seed_hint">privacidad dignidad libertad …</string>
|
||||
<string name="restore_seed_button_next">Siguiente</string>
|
||||
|
||||
<string name="restore_seed_warning_suggestions">Esta palabra no está en el diccionario de frases semilla. Por favor, selecciona la correcta de las sugerencias.</string>
|
||||
<string name="restore_seed_warning_no_suggestions">Esta palabra no está en el diccionario de frases semilla.</string>
|
||||
|
||||
<string name="restore_birthday_header">Altura de cumpleaños de la billetera</string>
|
||||
<string name="restore_birthday_sub_header">(opcional)</string>
|
||||
<string name="restore_birthday_button_restore">Restaurar</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,16 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="restore_button_clear">Clear Seed</string>
|
||||
<string name="restore_title">Restore</string>
|
||||
<string name="restore_subtitle">Seed Recovery Phrase</string>
|
||||
<string name="restore_message">Please type in your 24-word secret recovery phrase in the correct order.</string>
|
||||
<string name="restore_button">Next</string>
|
||||
|
||||
<string name="restore_title">Enter secret recovery phrase</string>
|
||||
<string name="restore_seed_instructions">Enter your 24-word seed phrase to restore the associated wallet.</string>
|
||||
<string name="restore_seed_hint">privacy dignity freedom …</string>
|
||||
<string name="restore_seed_button_next">Next</string>
|
||||
<string name="restore_dialog_title">Need to know more?</string>
|
||||
<string name="restore_dialog_message_1_bold_part">The Secret Recovery Phrase</string>
|
||||
<string name="restore_dialog_message_1">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.</string>
|
||||
<string name="restore_dialog_message_2_bold_part">The Wallet Birthday Height</string>
|
||||
<string name="restore_dialog_message_2">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.
|
||||
</string>
|
||||
<string name="restore_dialog_button">Got it!</string>
|
||||
|
||||
<string name="restore_seed_warning_suggestions">This word is not in the seed phrase dictionary. Please select the correct one from the suggestions.</string>
|
||||
<string name="restore_seed_warning_no_suggestions">This word is not in the seed phrase dictionary.</string>
|
||||
<string name="restore_bd_height_btn">Estimate my block height</string>
|
||||
<string name="restore_bd_restore_btn">Restore</string>
|
||||
|
||||
<string name="restore_bd_subtitle">Wallet Birthday Height</string>
|
||||
<string name="restore_bd_message">Entering your Wallet Birthday Height helps speed up the restore process.</string>
|
||||
<string name="restore_bd_text_field_title">Block Height</string>
|
||||
<string name="restore_bd_text_field_hint">Enter number</string>
|
||||
<string name="restore_bd_text_field_note">Wallet Birthday Height is the point in time when your wallet was created.</string>
|
||||
|
||||
<string name="restore_birthday_header">Wallet birthday height</string>
|
||||
<string name="restore_birthday_sub_header">(optional)</string>
|
||||
<string name="restore_birthday_button_restore">Restore</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="restore_success_title">¡Mantén Zashi abierto!</string>
|
||||
<string name="restore_success_subtitle">Tu billetera ha sido restaurada con éxito y ahora se está sincronizando</string>
|
||||
<string name="restore_success_description">Para evitar interrupciones, mantén la pantalla encendida, conecta tu dispositivo a una fuente de energía y guárdalo en un lugar seguro.</string>
|
||||
<string name="restore_success_subtitle">Your wallet is being restored.</string>
|
||||
<string name="restore_success_description">Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption:</string>
|
||||
<string name="restoring_initial_dialog_checkbox">Mantener la pantalla encendida mientras se restaura.</string>
|
||||
<string name="restore_success_note_part_1">Nota:</string>
|
||||
<string name="restore_success_note_part_2">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.</string>
|
||||
<string name="restore_success_note_part_2">Your funds cannot be spent with Zashi until your wallet is fully restored.</string>
|
||||
<string name="restore_success_button">¡Entendido!</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="restore_success_title">Keep Zashi open!</string>
|
||||
<string name="restore_success_subtitle">Your wallet has been successfully restored and is now syncing</string>
|
||||
<string name="restore_success_description">To prevent interruption, keep your screen awake, plug your device into a power source, and keep it in a secure place.</string>
|
||||
<string name="restore_success_subtitle">Your wallet is being restored.</string>
|
||||
<string name="restore_success_description">Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption:</string>
|
||||
<string name="restoring_initial_dialog_checkbox">Keep screen on while restoring.</string>
|
||||
<string name="restore_success_note_part_1">Note:</string>
|
||||
<string name="restore_success_note_part_2">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.</string>
|
||||
<string name="restore_success_note_part_2">Your funds cannot be spent with Zashi until your wallet is fully restored.</string>
|
||||
<string name="restore_success_button">Got it!</string>
|
||||
<string name="restore_success_bullet_1">Keep the Zashi app open on an active phone screen.</string>
|
||||
<string name="restore_success_bullet_2">To prevent your phone screen from going dark, turn off power-saving mode and keep your phone plugged in.</string>
|
||||
</resources>
|
||||
|
|
|
@ -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<RestoreViewModel>().value.userWordList.current.value.size ==
|
||||
composeTestRule.activity.viewModels<RestoreSeedViewModel>().value.userWordList.current.value.size ==
|
||||
SeedPhrase.SEED_PHRASE_SIZE
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue