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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
|
||||||
|
@ -66,7 +68,9 @@ fun LabeledCheckBox(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
checked: Boolean = false,
|
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) }
|
val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
|
||||||
|
|
||||||
|
@ -114,8 +118,8 @@ fun LabeledCheckBox(
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = AnnotatedString(text),
|
text = AnnotatedString(text),
|
||||||
color = ZcashTheme.colors.textPrimary,
|
color = color,
|
||||||
style = ZcashTheme.extendedTypography.checkboxText
|
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,
|
text: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
style: TextStyle = ZashiButtonDefaults.style,
|
|
||||||
shape: Shape = ZashiButtonDefaults.shape,
|
|
||||||
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
|
||||||
@DrawableRes icon: Int? = null,
|
@DrawableRes icon: Int? = null,
|
||||||
@DrawableRes trailingIcon: Int? = null,
|
@DrawableRes trailingIcon: Int? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
|
style: TextStyle = ZashiButtonDefaults.style,
|
||||||
|
shape: Shape = ZashiButtonDefaults.shape,
|
||||||
|
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
||||||
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
|
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
|
||||||
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
|
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -21,7 +21,9 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.ui.design.R
|
import co.electriccoin.zcash.ui.design.R
|
||||||
|
@ -40,6 +42,9 @@ fun ZashiCheckbox(
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = ZashiTypography.textSm,
|
||||||
|
fontWeight: FontWeight = FontWeight.Medium,
|
||||||
|
color: Color = ZashiColors.Text.textPrimary,
|
||||||
) {
|
) {
|
||||||
ZashiCheckbox(
|
ZashiCheckbox(
|
||||||
state =
|
state =
|
||||||
|
@ -49,6 +54,9 @@ fun ZashiCheckbox(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
style = style,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
color = color,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +64,9 @@ fun ZashiCheckbox(
|
||||||
fun ZashiCheckbox(
|
fun ZashiCheckbox(
|
||||||
state: CheckboxState,
|
state: CheckboxState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = ZashiTypography.textSm,
|
||||||
|
fontWeight: FontWeight = FontWeight.Medium,
|
||||||
|
color: Color = ZashiColors.Text.textPrimary,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
|
@ -70,9 +81,9 @@ fun ZashiCheckbox(
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = state.text.getValue(),
|
text = state.text.getValue(),
|
||||||
style = ZashiTypography.textSm,
|
style = style,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = fontWeight,
|
||||||
color = ZashiColors.Text.textPrimary,
|
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.border
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
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.getValue
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||||
|
|
||||||
|
@ -48,7 +50,7 @@ fun ZashiTextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
innerModifier: Modifier = Modifier,
|
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
|
||||||
error: String? = null,
|
error: String? = null,
|
||||||
isEnabled: Boolean = true,
|
isEnabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
|
@ -106,7 +108,7 @@ fun ZashiTextField(
|
||||||
fun ZashiTextField(
|
fun ZashiTextField(
|
||||||
state: TextFieldState,
|
state: TextFieldState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
innerModifier: Modifier = Modifier,
|
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable (() -> Unit)? = null,
|
||||||
|
@ -124,6 +126,13 @@ fun ZashiTextField(
|
||||||
minLines: Int = 1,
|
minLines: Int = 1,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
shape: Shape = ZashiTextFieldDefaults.shape,
|
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()
|
colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
|
||||||
) {
|
) {
|
||||||
TextFieldInternal(
|
TextFieldInternal(
|
||||||
|
@ -147,10 +156,20 @@ fun ZashiTextField(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
|
contentPadding = contentPadding,
|
||||||
innerModifier = innerModifier
|
innerModifier = innerModifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ZashiTextFieldPlaceholder(res: StringResource) {
|
||||||
|
Text(
|
||||||
|
text = res.getValue(),
|
||||||
|
style = ZashiTypography.textMd,
|
||||||
|
color = ZashiColors.Inputs.Default.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList", "LongMethod")
|
@Suppress("LongParameterList", "LongMethod")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -174,10 +193,13 @@ private fun TextFieldInternal(
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
colors: ZashiTextFieldColors,
|
colors: ZashiTextFieldColors,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
innerModifier: 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()
|
val androidColors = colors.toTextFieldColors()
|
||||||
// If color is not provided via the text style, use content color as a default
|
// If color is not provided via the text style, use content color as a default
|
||||||
val textColor =
|
val textColor =
|
||||||
|
@ -193,16 +215,16 @@ private fun TextFieldInternal(
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = state.value.getValue(),
|
value = state.value.getValue(),
|
||||||
modifier =
|
modifier =
|
||||||
innerModifier.fillMaxWidth() then
|
innerModifier then
|
||||||
if (borderColor == Color.Unspecified) {
|
if (borderColor == Color.Unspecified) {
|
||||||
Modifier
|
Modifier
|
||||||
} else {
|
} else {
|
||||||
Modifier.border(
|
Modifier.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color = borderColor,
|
color = borderColor,
|
||||||
shape = ZashiTextFieldDefaults.shape
|
shape = shape
|
||||||
)
|
)
|
||||||
} then Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth),
|
},
|
||||||
onValueChange = state.onValueChange,
|
onValueChange = state.onValueChange,
|
||||||
enabled = state.isEnabled,
|
enabled = state.isEnabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
|
@ -243,13 +265,7 @@ private fun TextFieldInternal(
|
||||||
isError = state.isError,
|
isError = state.isError,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
colors = androidColors,
|
colors = androidColors,
|
||||||
contentPadding =
|
contentPadding = 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),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -303,7 +319,9 @@ data class ZashiTextFieldColors(
|
||||||
val textColor: Color,
|
val textColor: Color,
|
||||||
val hintColor: Color,
|
val hintColor: Color,
|
||||||
val borderColor: Color,
|
val borderColor: Color,
|
||||||
|
val focusedBorderColor: Color,
|
||||||
val containerColor: Color,
|
val containerColor: Color,
|
||||||
|
val focusedContainerColor: Color,
|
||||||
val placeholderColor: Color,
|
val placeholderColor: Color,
|
||||||
val disabledTextColor: Color,
|
val disabledTextColor: Color,
|
||||||
val disabledHintColor: Color,
|
val disabledHintColor: Color,
|
||||||
|
@ -317,11 +335,15 @@ data class ZashiTextFieldColors(
|
||||||
val errorPlaceholderColor: Color,
|
val errorPlaceholderColor: Color,
|
||||||
) {
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun borderColor(state: TextFieldState): State<Color> {
|
internal fun borderColor(
|
||||||
|
state: TextFieldState,
|
||||||
|
isFocused: Boolean
|
||||||
|
): State<Color> {
|
||||||
val targetValue =
|
val targetValue =
|
||||||
when {
|
when {
|
||||||
!state.isEnabled -> disabledBorderColor
|
!state.isEnabled -> disabledBorderColor
|
||||||
state.isError -> errorBorderColor
|
state.isError -> errorBorderColor
|
||||||
|
isFocused -> focusedBorderColor.takeOrElse { borderColor }
|
||||||
else -> borderColor
|
else -> borderColor
|
||||||
}
|
}
|
||||||
return rememberUpdatedState(targetValue)
|
return rememberUpdatedState(targetValue)
|
||||||
|
@ -345,7 +367,7 @@ data class ZashiTextFieldColors(
|
||||||
unfocusedTextColor = textColor,
|
unfocusedTextColor = textColor,
|
||||||
disabledTextColor = disabledTextColor,
|
disabledTextColor = disabledTextColor,
|
||||||
errorTextColor = errorTextColor,
|
errorTextColor = errorTextColor,
|
||||||
focusedContainerColor = containerColor,
|
focusedContainerColor = focusedContainerColor.takeOrElse { containerColor },
|
||||||
unfocusedContainerColor = containerColor,
|
unfocusedContainerColor = containerColor,
|
||||||
disabledContainerColor = disabledContainerColor,
|
disabledContainerColor = disabledContainerColor,
|
||||||
errorContainerColor = errorContainerColor,
|
errorContainerColor = errorContainerColor,
|
||||||
|
@ -391,13 +413,18 @@ object ZashiTextFieldDefaults {
|
||||||
val shape: Shape
|
val shape: Shape
|
||||||
get() = RoundedCornerShape(8.dp)
|
get() = RoundedCornerShape(8.dp)
|
||||||
|
|
||||||
|
val innerModifier: Modifier
|
||||||
|
get() = Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth).fillMaxWidth()
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
@Composable
|
@Composable
|
||||||
fun defaultColors(
|
fun defaultColors(
|
||||||
textColor: Color = ZashiColors.Inputs.Filled.text,
|
textColor: Color = ZashiColors.Inputs.Filled.text,
|
||||||
hintColor: Color = ZashiColors.Inputs.Default.hint,
|
hintColor: Color = ZashiColors.Inputs.Default.hint,
|
||||||
borderColor: Color = Color.Unspecified,
|
borderColor: Color = Color.Unspecified,
|
||||||
|
focusedBorderColor: Color = ZashiColors.Inputs.Focused.stroke,
|
||||||
containerColor: Color = ZashiColors.Inputs.Default.bg,
|
containerColor: Color = ZashiColors.Inputs.Default.bg,
|
||||||
|
focusedContainerColor: Color = ZashiColors.Inputs.Focused.bg,
|
||||||
placeholderColor: Color = ZashiColors.Inputs.Default.text,
|
placeholderColor: Color = ZashiColors.Inputs.Default.text,
|
||||||
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
|
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
|
||||||
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
|
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
|
||||||
|
@ -413,7 +440,9 @@ object ZashiTextFieldDefaults {
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
hintColor = hintColor,
|
hintColor = hintColor,
|
||||||
borderColor = borderColor,
|
borderColor = borderColor,
|
||||||
|
focusedBorderColor = focusedBorderColor,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
|
focusedContainerColor = focusedContainerColor,
|
||||||
placeholderColor = placeholderColor,
|
placeholderColor = placeholderColor,
|
||||||
disabledTextColor = disabledTextColor,
|
disabledTextColor = disabledTextColor,
|
||||||
disabledHintColor = disabledHintColor,
|
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
|
@PreviewScreens
|
||||||
@Composable
|
@Composable
|
||||||
private fun DefaultPreview() =
|
private fun DefaultPreview() =
|
||||||
|
|
|
@ -25,8 +25,8 @@ import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.component.CommonTag
|
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.model.RestoreStage
|
||||||
|
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag
|
||||||
import co.electriccoin.zcash.ui.test.getAppContext
|
import co.electriccoin.zcash.ui.test.getAppContext
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -55,7 +55,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertIsFocused()
|
it.assertIsFocused()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
SeedPhraseFixture.SEED_PHRASE + " " + SeedPhraseFixture.SEED_PHRASE
|
SeedPhraseFixture.SEED_PHRASE + " " + SeedPhraseFixture.SEED_PHRASE
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performKeyInput {
|
it.performKeyInput {
|
||||||
withKeyDown(Key.CtrlLeft) {
|
withKeyDown(Key.CtrlLeft) {
|
||||||
pressKey(Key.V)
|
pressKey(Key.V)
|
||||||
|
@ -94,11 +94,11 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
assertEquals(SeedPhrase.SEED_PHRASE_SIZE, testSetup.getUserInputWords().size)
|
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()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
// Insert uncompleted seed words
|
// Insert uncompleted seed words
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("test")
|
it.performTextInput("test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
// Insert complete seed words
|
// 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)
|
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.LocalScreenSecurity
|
||||||
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
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 co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -43,9 +44,9 @@ class RestoreViewSecuredScreenTest : UiTestPrerequisites() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
|
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
RestoreWallet(
|
RestoreSeedView(
|
||||||
ZcashNetwork.Mainnet,
|
ZcashNetwork.Mainnet,
|
||||||
RestoreState(),
|
RestoreSeedState(),
|
||||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
||||||
WordList(emptyList()),
|
WordList(emptyList()),
|
||||||
restoreHeight = null,
|
restoreHeight = null,
|
||||||
|
|
|
@ -25,9 +25,10 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.component.CommonTag
|
import co.electriccoin.zcash.ui.design.component.CommonTag
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
|
||||||
import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage
|
import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.RestoreState
|
import co.electriccoin.zcash.ui.screen.restore.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.screen.restore.state.WordList
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
|
@ -54,7 +55,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
fun seed_autocomplete_suggestions_appear() {
|
fun seed_autocomplete_suggestions_appear() {
|
||||||
newTestSetup()
|
newTestSetup()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("ab")
|
it.performTextInput("ab")
|
||||||
|
|
||||||
// Make sure text isn't cleared
|
// Make sure text isn't cleared
|
||||||
|
@ -62,13 +63,13 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(
|
||||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||||
).also {
|
).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(
|
||||||
matcher = hasText("able", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
matcher = hasText("able", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||||
).also {
|
).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
@ -79,17 +80,17 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
fun seed_choose_autocomplete() {
|
fun seed_choose_autocomplete() {
|
||||||
newTestSetup()
|
newTestSetup()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("ab")
|
it.performTextInput("ab")
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(
|
||||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||||
).also {
|
).also {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertTextEquals("abandon ", includeEditableText = true)
|
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,15 +108,15 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
fun seed_type_full_word() {
|
fun seed_type_full_word() {
|
||||||
newTestSetup()
|
newTestSetup()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("abandon")
|
it.performTextInput("abandon")
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertTextEquals("abandon ", includeEditableText = true)
|
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +210,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
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())
|
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +242,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
it.assertIsEnabled()
|
it.assertIsEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also {
|
||||||
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
|
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +267,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
initialWordsList = SeedPhraseFixture.new().split
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also {
|
||||||
it.performTextInput("1.2")
|
it.performTextInput("1.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,7 +354,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialStage: RestoreStage,
|
initialStage: RestoreStage,
|
||||||
initialWordsList: List<String>
|
initialWordsList: List<String>
|
||||||
) {
|
) {
|
||||||
private val state = RestoreState(initialStage)
|
private val state = RestoreSeedState(initialStage)
|
||||||
|
|
||||||
private val wordList = WordList(initialWordsList)
|
private val wordList = WordList(initialWordsList)
|
||||||
|
|
||||||
|
@ -391,7 +392,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
init {
|
init {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
RestoreWallet(
|
RestoreSeedView(
|
||||||
ZcashNetwork.Mainnet,
|
ZcashNetwork.Mainnet,
|
||||||
state,
|
state,
|
||||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
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.DeriveKeystoneAccountUnifiedAddressUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.FlipTransactionBookmarkUseCase
|
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.GetContactByAddressUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase
|
||||||
|
@ -121,7 +120,6 @@ val useCaseModule =
|
||||||
factoryOf(::IsCoinbaseAvailableUseCase)
|
factoryOf(::IsCoinbaseAvailableUseCase)
|
||||||
factoryOf(::GetZashiSpendingKeyUseCase)
|
factoryOf(::GetZashiSpendingKeyUseCase)
|
||||||
factoryOf(::ObservePersistableWalletUseCase)
|
factoryOf(::ObservePersistableWalletUseCase)
|
||||||
factoryOf(::GetBackupPersistableWalletUseCase)
|
|
||||||
factoryOf(::GetSupportUseCase)
|
factoryOf(::GetSupportUseCase)
|
||||||
factoryOf(::SendEmailUseCase)
|
factoryOf(::SendEmailUseCase)
|
||||||
factoryOf(::SendSupportEmailUseCase)
|
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.flexa.FlexaViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.home.HomeViewModel
|
import co.electriccoin.zcash.ui.screen.home.HomeViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
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.qrcode.viewmodel.QrCodeViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel
|
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.restoresuccess.viewmodel.RestoreSuccessViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransactionViewModel
|
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransactionViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||||
|
@ -57,9 +57,8 @@ val viewModelModule =
|
||||||
viewModelOf(::WalletViewModel)
|
viewModelOf(::WalletViewModel)
|
||||||
viewModelOf(::AuthenticationViewModel)
|
viewModelOf(::AuthenticationViewModel)
|
||||||
viewModelOf(::OldHomeViewModel)
|
viewModelOf(::OldHomeViewModel)
|
||||||
viewModelOf(::OnboardingViewModel)
|
|
||||||
viewModelOf(::StorageCheckViewModel)
|
viewModelOf(::StorageCheckViewModel)
|
||||||
viewModelOf(::RestoreViewModel)
|
viewModelOf(::RestoreSeedViewModel)
|
||||||
viewModelOf(::ScreenBrightnessViewModel)
|
viewModelOf(::ScreenBrightnessViewModel)
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
viewModelOf(::AdvancedSettingsViewModel)
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
|
@ -156,4 +155,5 @@ val viewModelModule =
|
||||||
viewModelOf(::TaxExportViewModel)
|
viewModelOf(::TaxExportViewModel)
|
||||||
viewModelOf(::BalanceViewModel)
|
viewModelOf(::BalanceViewModel)
|
||||||
viewModelOf(::HomeViewModel)
|
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.WrapAuthentication
|
||||||
import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants
|
import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants
|
||||||
import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart
|
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.onboarding.persistExistingWalletWithSeedPhrase
|
||||||
import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning
|
import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning
|
||||||
import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
||||||
|
@ -249,7 +249,7 @@ class MainActivity : FragmentActivity() {
|
||||||
CompositionLocalProvider(RemoteConfig provides configuration) {
|
CompositionLocalProvider(RemoteConfig provides configuration) {
|
||||||
when (secretState) {
|
when (secretState) {
|
||||||
SecretState.None -> {
|
SecretState.None -> {
|
||||||
WrapOnboarding()
|
RestoreNavigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
is SecretState.NeedsWarning -> {
|
is SecretState.NeedsWarning -> {
|
||||||
|
@ -263,7 +263,11 @@ class MainActivity : FragmentActivity() {
|
||||||
applicationContext,
|
applicationContext,
|
||||||
walletViewModel,
|
walletViewModel,
|
||||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||||
WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext))
|
WalletFixture.Alice.getBirthday(
|
||||||
|
ZcashNetwork.fromResources(
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
walletViewModel.persistNewWalletAndRestoringState(WalletRestoringState.INITIATING)
|
walletViewModel.persistNewWalletAndRestoringState(WalletRestoringState.INITIATING)
|
||||||
|
@ -284,7 +288,7 @@ class MainActivity : FragmentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
error("Unhandled secret state: $secretState")
|
// should not happen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,6 @@ interface WalletRepository {
|
||||||
val synchronizer: StateFlow<Synchronizer?>
|
val synchronizer: StateFlow<Synchronizer?>
|
||||||
val secretState: StateFlow<SecretState>
|
val secretState: StateFlow<SecretState>
|
||||||
val fastestServers: StateFlow<FastestServersState>
|
val fastestServers: StateFlow<FastestServersState>
|
||||||
val persistableWallet: Flow<PersistableWallet?>
|
|
||||||
val onboardingState: Flow<OnboardingState>
|
val onboardingState: Flow<OnboardingState>
|
||||||
|
|
||||||
val allAccounts: Flow<List<WalletAccount>?>
|
val allAccounts: Flow<List<WalletAccount>?>
|
||||||
|
@ -99,8 +98,6 @@ interface WalletRepository {
|
||||||
|
|
||||||
suspend fun getSynchronizer(): Synchronizer
|
suspend fun getSynchronizer(): Synchronizer
|
||||||
|
|
||||||
suspend fun getPersistableWallet(): PersistableWallet
|
|
||||||
|
|
||||||
fun persistExistingWalletWithSeedPhrase(
|
fun persistExistingWalletWithSeedPhrase(
|
||||||
network: ZcashNetwork,
|
network: ZcashNetwork,
|
||||||
seedPhrase: SeedPhrase,
|
seedPhrase: SeedPhrase,
|
||||||
|
@ -110,7 +107,7 @@ interface WalletRepository {
|
||||||
|
|
||||||
class WalletRepositoryImpl(
|
class WalletRepositoryImpl(
|
||||||
accountDataSource: AccountDataSource,
|
accountDataSource: AccountDataSource,
|
||||||
persistableWalletProvider: PersistableWalletProvider,
|
private val persistableWalletProvider: PersistableWalletProvider,
|
||||||
private val synchronizerProvider: SynchronizerProvider,
|
private val synchronizerProvider: SynchronizerProvider,
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val getDefaultServers: GetDefaultServersProvider,
|
private val getDefaultServers: GetDefaultServersProvider,
|
||||||
|
@ -143,22 +140,12 @@ class WalletRepositoryImpl(
|
||||||
override val allAccounts: StateFlow<List<WalletAccount>?> = accountDataSource.allAccounts
|
override val allAccounts: StateFlow<List<WalletAccount>?> = accountDataSource.allAccounts
|
||||||
|
|
||||||
override val secretState: StateFlow<SecretState> =
|
override val secretState: StateFlow<SecretState> =
|
||||||
combine(
|
onboardingState.map { onboardingState: OnboardingState ->
|
||||||
persistableWalletProvider.persistableWallet,
|
when (onboardingState) {
|
||||||
onboardingState
|
OnboardingState.NONE -> SecretState.None
|
||||||
) { persistableWallet: PersistableWallet?, onboardingState: OnboardingState ->
|
OnboardingState.NEEDS_WARN -> SecretState.NeedsWarning
|
||||||
when {
|
OnboardingState.NEEDS_BACKUP -> SecretState.NeedsBackup
|
||||||
onboardingState == OnboardingState.NONE -> SecretState.None
|
OnboardingState.READY -> SecretState.Ready
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
|
@ -204,11 +191,6 @@ class WalletRepositoryImpl(
|
||||||
initialValue = FastestServersState(servers = emptyList(), isLoading = true)
|
initialValue = FastestServersState(servers = emptyList(), isLoading = true)
|
||||||
)
|
)
|
||||||
|
|
||||||
override val persistableWallet: Flow<PersistableWallet?> =
|
|
||||||
secretState.map {
|
|
||||||
(it as? SecretState.Ready?)?.persistableWallet
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
||||||
combine(synchronizer, currentAccount) { synchronizer, currentAccount ->
|
combine(synchronizer, currentAccount) { synchronizer, currentAccount ->
|
||||||
|
@ -317,7 +299,7 @@ class WalletRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSelectedServer(): LightWalletEndpoint {
|
override suspend fun getSelectedServer(): LightWalletEndpoint {
|
||||||
return persistableWallet
|
return persistableWalletProvider.persistableWallet
|
||||||
.map {
|
.map {
|
||||||
it?.endpoint
|
it?.endpoint
|
||||||
}
|
}
|
||||||
|
@ -338,8 +320,6 @@ class WalletRepositoryImpl(
|
||||||
|
|
||||||
override suspend fun getSynchronizer(): Synchronizer = synchronizerProvider.getSynchronizer()
|
override suspend fun getSynchronizer(): Synchronizer = synchronizerProvider.getSynchronizer()
|
||||||
|
|
||||||
override suspend fun getPersistableWallet(): PersistableWallet = persistableWallet.filterNotNull().first()
|
|
||||||
|
|
||||||
override fun persistExistingWalletWithSeedPhrase(
|
override fun persistExistingWalletWithSeedPhrase(
|
||||||
network: ZcashNetwork,
|
network: ZcashNetwork,
|
||||||
seedPhrase: SeedPhrase,
|
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
|
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(
|
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
|
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.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class ObserveSelectedEndpointUseCase(
|
class ObserveSelectedEndpointUseCase(
|
||||||
private val walletRepository: WalletRepository
|
private val persistableWalletProvider: PersistableWalletProvider
|
||||||
) {
|
) {
|
||||||
operator fun invoke() =
|
operator fun invoke() =
|
||||||
walletRepository.persistableWallet
|
persistableWalletProvider.persistableWallet
|
||||||
.map {
|
.map {
|
||||||
it?.endpoint
|
it?.endpoint
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,9 +168,9 @@ sealed class SecretState {
|
||||||
|
|
||||||
object NeedsWarning : SecretState()
|
object NeedsWarning : SecretState()
|
||||||
|
|
||||||
class NeedsBackup(val persistableWallet: PersistableWallet) : SecretState()
|
object NeedsBackup : SecretState()
|
||||||
|
|
||||||
class Ready(val persistableWallet: PersistableWallet) : SecretState()
|
object Ready : SecretState()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,35 +4,47 @@ package co.electriccoin.zcash.ui.screen.onboarding
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
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.fixture.WalletFixture
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.sdk.type.fromResources
|
import cash.z.ecc.sdk.type.fromResources
|
||||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
|
||||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
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.LocalActivity
|
||||||
|
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||||
import co.electriccoin.zcash.ui.common.model.VersionInfo
|
import co.electriccoin.zcash.ui.common.model.VersionInfo
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
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.view.Onboarding
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
import co.electriccoin.zcash.ui.screen.restore.height.AndroidRestoreBDHeight
|
||||||
import co.electriccoin.zcash.ui.screen.restore.WrapRestore
|
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
|
@Composable
|
||||||
internal fun WrapOnboarding() {
|
fun MainActivity.RestoreNavigation() {
|
||||||
val activity = LocalActivity.current
|
val activity = LocalActivity.current
|
||||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
val navigationRouter = koinInject<NavigationRouter>()
|
||||||
val onboardingViewModel = koinActivityViewModel<OnboardingViewModel>()
|
val navController = LocalNavController.current
|
||||||
|
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||||
|
val navigator: Navigator = remember { NavigatorImpl(this@RestoreNavigation, navController, flexaViewModel) }
|
||||||
val versionInfo = VersionInfo.new(activity.applicationContext)
|
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 = {
|
val onCreateWallet = {
|
||||||
walletViewModel.persistOnboardingState(OnboardingState.NEEDS_WARN)
|
walletViewModel.persistOnboardingState(OnboardingState.NEEDS_WARN)
|
||||||
}
|
}
|
||||||
|
@ -49,7 +61,7 @@ internal fun WrapOnboarding() {
|
||||||
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext))
|
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext))
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
onboardingViewModel.setIsImporting(true)
|
navigationRouter.forward(RestoreSeed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,16 +74,33 @@ internal fun WrapOnboarding() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
navigationRouter.observePipeline().collect {
|
||||||
|
navigator.executeCommand(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = Onboarding,
|
||||||
|
enterTransition = { enterTransition() },
|
||||||
|
exitTransition = { exitTransition() },
|
||||||
|
popEnterTransition = { popEnterTransition() },
|
||||||
|
popExitTransition = { popExitTransition() }
|
||||||
|
) {
|
||||||
|
composable<Onboarding> {
|
||||||
Onboarding(
|
Onboarding(
|
||||||
isDebugMenuEnabled = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService,
|
isDebugMenuEnabled = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService,
|
||||||
onImportWallet = onImportWallet,
|
onImportWallet = onImportWallet,
|
||||||
onCreateWallet = onCreateWallet,
|
onCreateWallet = onCreateWallet,
|
||||||
onFixtureWallet = onFixtureWallet
|
onFixtureWallet = onFixtureWallet
|
||||||
)
|
)
|
||||||
|
}
|
||||||
activity.reportFullyDrawn()
|
composable<RestoreSeed> {
|
||||||
} else {
|
AndroidRestoreSeed()
|
||||||
WrapRestore()
|
}
|
||||||
|
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.
|
* These are only used for automated testing.
|
||||||
*/
|
*/
|
||||||
object RestoreTag {
|
object RestoreSeedTag {
|
||||||
const val SEED_WORD_TEXT_FIELD = "seed_text_field"
|
const val SEED_WORD_TEXT_FIELD = "seed_text_field"
|
||||||
const val BIRTHDAY_TEXT_FIELD = "birthday_text_field"
|
const val BIRTHDAY_TEXT_FIELD = "birthday_text_field"
|
||||||
const val CHIP_LAYOUT = "chip_group"
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.ParagraphStyle
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextIndent
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.tooling.preview.Devices
|
import androidx.compose.ui.tooling.preview.Devices
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.ui.R
|
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.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.ZashiButton
|
||||||
|
import co.electriccoin.zcash.ui.design.component.ZashiCheckbox
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
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.scaffoldPadding
|
||||||
|
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RestoreSuccess(state: RestoreSuccessViewState) {
|
fun RestoreSuccess(state: RestoreSuccessViewState) {
|
||||||
BlankBgScaffold { paddingValues ->
|
GradientBgScaffold(
|
||||||
|
startColor = ZashiColors.Utility.WarningYellow.utilityOrange100,
|
||||||
|
endColor = ZashiColors.Surfaces.bgPrimary,
|
||||||
|
) { paddingValues ->
|
||||||
RestoreSuccessContent(
|
RestoreSuccessContent(
|
||||||
state = state,
|
state = state,
|
||||||
modifier =
|
modifier =
|
||||||
|
@ -53,16 +65,8 @@ private fun RestoreSuccessContent(
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))
|
Spacer(Modifier.height(64.dp))
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.restore_success_title),
|
|
||||||
style = ZcashTheme.typography.secondary.headlineMedium
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge))
|
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.img_success_dialog),
|
painter = painterResource(id = R.drawable.img_success_dialog),
|
||||||
|
@ -70,32 +74,74 @@ private fun RestoreSuccessContent(
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
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(
|
||||||
text = stringResource(id = R.string.restore_success_subtitle),
|
text = stringResource(id = R.string.restore_success_subtitle),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = ZcashTheme.typography.secondary.headlineSmall,
|
style = ZashiTypography.textMd,
|
||||||
fontWeight = FontWeight.SemiBold
|
color = ZashiColors.Text.textPrimary,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.restore_success_description),
|
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(
|
Spacer(Modifier.height(4.dp))
|
||||||
modifier = Modifier.align(Alignment.Start),
|
|
||||||
checked = state.isKeepScreenOnChecked,
|
val bulletText1 = stringResource(R.string.restore_success_bullet_1)
|
||||||
onCheckedChange = { state.onCheckboxClick() },
|
Text(
|
||||||
text = stringResource(id = R.string.restoring_initial_dialog_checkbox)
|
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(
|
||||||
text =
|
text =
|
||||||
|
@ -106,12 +152,23 @@ private fun RestoreSuccessContent(
|
||||||
append(" ")
|
append(" ")
|
||||||
append(stringResource(id = R.string.restore_success_note_part_2))
|
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(
|
ZashiButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<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>
|
</resources>
|
||||||
|
|
|
@ -1,16 +1,29 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<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_dialog_title">Need to know more?</string>
|
||||||
<string name="restore_seed_instructions">Enter your 24-word seed phrase to restore the associated wallet.</string>
|
<string name="restore_dialog_message_1_bold_part">The Secret Recovery Phrase</string>
|
||||||
<string name="restore_seed_hint">privacy dignity freedom …</string>
|
<string name="restore_dialog_message_1">is a unique set of 24 words, appearing in a
|
||||||
<string name="restore_seed_button_next">Next</string>
|
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_bd_height_btn">Estimate my block height</string>
|
||||||
<string name="restore_seed_warning_no_suggestions">This word is not in the seed phrase dictionary.</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>
|
</resources>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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_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_subtitle">Your wallet is being restored.</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_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="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_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>
|
<string name="restore_success_button">¡Entendido!</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="restore_success_title">Keep Zashi open!</string>
|
<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_subtitle">Your wallet is being restored.</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_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="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_1">Note:</string>
|
||||||
<string name="restore_success_note_part_2">During the initial sync your funds cannot be sent or
|
<string name="restore_success_note_part_2">Your funds cannot be spent with Zashi until your wallet is fully restored.</string>
|
||||||
spent. Depending on the age of your wallet, it may take a few hours to fully sync.</string>
|
|
||||||
<string name="restore_success_button">Got it!</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>
|
</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.authentication.view.AnimationConstants.WELCOME_ANIM_TEST_TAG
|
||||||
import co.electriccoin.zcash.ui.screen.balances.BalanceTag
|
import co.electriccoin.zcash.ui.screen.balances.BalanceTag
|
||||||
import co.electriccoin.zcash.ui.screen.home.HomeTags
|
import co.electriccoin.zcash.ui.screen.home.HomeTags
|
||||||
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag
|
||||||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
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.securitywarning.view.SecurityScreenTag.ACKNOWLEDGE_CHECKBOX_TAG
|
||||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
import co.electriccoin.zcash.ui.screen.send.SendTag
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -210,7 +210,7 @@ class ScreenshotTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
val seedPhraseSplitLength = SeedPhraseFixture.new().split.size
|
val seedPhraseSplitLength = SeedPhraseFixture.new().split.size
|
||||||
SeedPhraseFixture.new().split.forEachIndexed { index, string ->
|
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)
|
it.performTextInput(string)
|
||||||
|
|
||||||
// Take a screenshot half-way through filling in the seed phrase
|
// Take a screenshot half-way through filling in the seed phrase
|
||||||
|
@ -221,7 +221,7 @@ class ScreenshotTest : UiTestPrerequisites() {
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.waitUntil {
|
composeTestRule.waitUntil {
|
||||||
composeTestRule.activity.viewModels<RestoreViewModel>().value.userWordList.current.value.size ==
|
composeTestRule.activity.viewModels<RestoreSeedViewModel>().value.userWordList.current.value.size ==
|
||||||
SeedPhrase.SEED_PHRASE_SIZE
|
SeedPhrase.SEED_PHRASE_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue