Feature/address book (#1614)
* [#1564] Send screen redesign (#1601) * [#1564] Send screen redesign Closes #1564 Closes #1580 * [#1564] Test hotfix Closes #1564 Closes #1580 * [#1564] Test hotfix * [#1564] Bugfixes and code cleanup * [#1564] Focus handling * Address Book UI (#1606) * Address Book UI * Design hotfix * Code cleanup * Test hotfix * Confirmation screen redesign (#1602) * Confirmation screen redesign * Documentation update * Design hotfixes * History item redesign (#1603) * History item redesign * Empty Memo message removed * Hidden fee for a receiving transaction * Address Book, Add Contact & Update Contact logic (#1610) * Address Book Screen logic * Add New Contact screen logic * Update Contact screen logic * Code cleanup * Code cleanup
This commit is contained in:
parent
519b48b524
commit
1fedce1cff
|
@ -8,6 +8,9 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
|||
|
||||
### Changed
|
||||
- The Receive screen UI has been redesigned
|
||||
- Send screen redesigned
|
||||
- Confirmation screen redesigned
|
||||
- History item redesigned
|
||||
|
||||
## [1.2 (739)] - 2024-09-27
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
|
||||
### Changed
|
||||
- The Receive screen UI has been redesigned
|
||||
- Send screen redesigned
|
||||
- Confirmation screen redesigned
|
||||
- History item redesigned
|
||||
|
||||
## [1.2 (739)] - 2024-09-27
|
||||
|
||||
|
|
|
@ -31,6 +31,19 @@ style:
|
|||
excludes: [ '**/*.kts' ]
|
||||
ignoreAnnotated:
|
||||
- 'Preview'
|
||||
- 'PreviewScreens'
|
||||
|
||||
complexity:
|
||||
LongMethod:
|
||||
active: true
|
||||
ignoreAnnotated:
|
||||
- 'Preview'
|
||||
- 'PreviewScreens'
|
||||
LongParameterList:
|
||||
active: true
|
||||
ignoreAnnotated:
|
||||
- 'Preview'
|
||||
- 'PreviewScreens'
|
||||
|
||||
Compose:
|
||||
ModifierMissing:
|
||||
|
|
|
@ -218,6 +218,7 @@ fun TextWithIcon(
|
|||
textAlign: TextAlign = TextAlign.Start,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
color: Color = ZcashTheme.colors.textPrimary,
|
||||
fontWeight: FontWeight? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
|
@ -248,6 +249,7 @@ fun TextWithIcon(
|
|||
overflow = overflow,
|
||||
textAlign = textAlign,
|
||||
style = style,
|
||||
fontWeight = fontWeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,4 +137,6 @@ data class TextFieldState(
|
|||
val error: StringResource? = null,
|
||||
val isEnabled: Boolean = true,
|
||||
val onValueChange: (String) -> Unit,
|
||||
)
|
||||
) {
|
||||
val isError = error != null
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldColors.textColor(
|
||||
enabled: Boolean,
|
||||
isError: Boolean,
|
||||
interactionSource: InteractionSource
|
||||
): State<Color> {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
val targetValue =
|
||||
when {
|
||||
!enabled -> disabledTextColor
|
||||
isError -> errorTextColor
|
||||
focused -> focusedTextColor
|
||||
else -> unfocusedTextColor
|
||||
}
|
||||
return rememberUpdatedState(targetValue)
|
||||
}
|
||||
|
||||
internal val TextFieldColors.selectionColors: TextSelectionColors
|
||||
@Composable get() = textSelectionColors
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldColors.cursorColor(isError: Boolean): State<Color> {
|
||||
return rememberUpdatedState(if (isError) errorCursorColor else cursorColor)
|
||||
}
|
|
@ -38,7 +38,6 @@ fun ZashiBottomBar(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun BottomBarPreview() =
|
||||
|
|
|
@ -4,8 +4,12 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
@ -26,101 +30,193 @@ import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
|||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.orDark
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListItem(
|
||||
state: ButtonState,
|
||||
@DrawableRes icon: Int,
|
||||
trailing: @Composable () -> Unit = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_chevron_right orDark R.drawable.ic_chevron_right_dark),
|
||||
contentDescription = state.text.getValue(),
|
||||
)
|
||||
}
|
||||
) {
|
||||
ZashiSettingsListItem(
|
||||
text = state.text.getValue(),
|
||||
icon = icon,
|
||||
trailing = trailing,
|
||||
onClick = state.onClick
|
||||
)
|
||||
}
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListItem(
|
||||
text: String,
|
||||
@DrawableRes icon: Int,
|
||||
trailing: @Composable () -> Unit = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_chevron_right orDark R.drawable.ic_chevron_right_dark),
|
||||
contentDescription = text,
|
||||
)
|
||||
},
|
||||
subtitle: String? = null,
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
ZashiSettingsListItem(
|
||||
leading = {
|
||||
state =
|
||||
ZashiSettingsListItemState(
|
||||
text = stringRes(text),
|
||||
subtitle = subtitle?.let { stringRes(it) },
|
||||
isEnabled = isEnabled,
|
||||
onClick = onClick
|
||||
),
|
||||
icon = icon,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListItem(
|
||||
state: ZashiSettingsListItemState,
|
||||
@DrawableRes icon: Int
|
||||
) {
|
||||
ZashiSettingsListItem(
|
||||
leading = { modifier ->
|
||||
ZashiSettingsListLeadingItem(
|
||||
modifier = modifier,
|
||||
icon = icon,
|
||||
contentDescription = state.text.getValue()
|
||||
)
|
||||
},
|
||||
content = { modifier ->
|
||||
ZashiSettingsListContentItem(
|
||||
modifier = modifier,
|
||||
text = state.text.getValue(),
|
||||
subtitle = state.subtitle?.getValue()
|
||||
)
|
||||
},
|
||||
trailing = { modifier ->
|
||||
ZashiSettingsListTrailingItem(
|
||||
modifier = modifier,
|
||||
isEnabled = state.isEnabled,
|
||||
contentDescription = state.text.getValue()
|
||||
)
|
||||
},
|
||||
onClick = state.onClick.takeIf { state.isEnabled }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListLeadingItem(
|
||||
icon: Int,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(40.dp),
|
||||
painter = painterResource(icon),
|
||||
contentDescription = text
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListTrailingItem(
|
||||
isEnabled: Boolean,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (isEnabled) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_chevron_right orDark R.drawable.ic_chevron_right_dark),
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListContentItem(
|
||||
text: String,
|
||||
subtitle: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = ZashiTypography.textMd,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
},
|
||||
trailing = trailing,
|
||||
onClick = onClick
|
||||
subtitle?.let {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = it,
|
||||
style = ZashiTypography.textXs,
|
||||
color = ZashiColors.Text.textTertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListItem(
|
||||
leading: @Composable () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
trailing: @Composable () -> Unit,
|
||||
onClick: () -> Unit
|
||||
leading: @Composable (Modifier) -> Unit,
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
trailing: @Composable (Modifier) -> Unit,
|
||||
contentPadding: PaddingValues = PaddingValues(vertical = 12.dp),
|
||||
onClick: (() -> Unit)?
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
.clip(RoundedCornerShape(12.dp)) then
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick,
|
||||
role = Role.Button,
|
||||
)
|
||||
.padding(vertical = 12.dp),
|
||||
} else {
|
||||
Modifier
|
||||
} then Modifier.padding(contentPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
leading()
|
||||
leading(Modifier)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
content()
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
trailing()
|
||||
content(Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
trailing(Modifier)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
data class ZashiSettingsListItemState(
|
||||
val text: StringResource,
|
||||
val subtitle: StringResource? = null,
|
||||
val isEnabled: Boolean = true,
|
||||
val onClick: () -> Unit = {},
|
||||
)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun ZashiSettingsListItemPreview() =
|
||||
private fun EnabledPreview() =
|
||||
ZcashTheme {
|
||||
BlankSurface {
|
||||
ZashiSettingsListItem(
|
||||
text = "Test",
|
||||
subtitle = "Subtitle",
|
||||
icon = R.drawable.ic_radio_button_checked,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun DisabledPreview() =
|
||||
ZcashTheme {
|
||||
BlankSurface {
|
||||
ZashiSettingsListItem(
|
||||
text = "Test",
|
||||
subtitle = "Subtitle",
|
||||
icon = R.drawable.ic_radio_button_checked,
|
||||
isEnabled = false,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
@ -38,11 +39,71 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
|||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun ZashiTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = Modifier,
|
||||
error: String? = null,
|
||||
isEnabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
prefix: @Composable (() -> Unit)? = null,
|
||||
suffix: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = ZashiTextFieldDefaults.shape,
|
||||
colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
|
||||
) {
|
||||
ZashiTextField(
|
||||
state =
|
||||
TextFieldState(
|
||||
value = stringRes(value),
|
||||
error = error?.let { stringRes(it) },
|
||||
isEnabled = isEnabled,
|
||||
onValueChange = onValueChange,
|
||||
),
|
||||
modifier = modifier,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
innerModifier = innerModifier
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun ZashiTextField(
|
||||
state: TextFieldState,
|
||||
modifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = Modifier,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
|
@ -63,16 +124,8 @@ fun ZashiTextField(
|
|||
colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
|
||||
) {
|
||||
TextFieldInternal(
|
||||
value = state.value.getValue(),
|
||||
onValueChange = state.onValueChange,
|
||||
modifier =
|
||||
modifier then
|
||||
Modifier.border(
|
||||
width = 1.dp,
|
||||
color = colors.borderColor,
|
||||
shape = ZashiTextFieldDefaults.shape
|
||||
),
|
||||
enabled = state.isEnabled,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle,
|
||||
label = label,
|
||||
|
@ -82,7 +135,6 @@ fun ZashiTextField(
|
|||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
isError = state.error != null,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
|
@ -91,17 +143,16 @@ fun ZashiTextField(
|
|||
minLines = minLines,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors.toTextFieldColors(),
|
||||
colors = colors,
|
||||
innerModifier = innerModifier
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TextFieldInternal(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
enabled: Boolean,
|
||||
state: TextFieldState,
|
||||
readOnly: Boolean,
|
||||
textStyle: TextStyle,
|
||||
label: @Composable (() -> Unit)?,
|
||||
|
@ -111,7 +162,6 @@ private fun TextFieldInternal(
|
|||
prefix: @Composable (() -> Unit)?,
|
||||
suffix: @Composable (() -> Unit)?,
|
||||
supportingText: @Composable (() -> Unit)?,
|
||||
isError: Boolean,
|
||||
visualTransformation: VisualTransformation,
|
||||
keyboardOptions: KeyboardOptions,
|
||||
keyboardActions: KeyboardActions,
|
||||
|
@ -120,27 +170,41 @@ private fun TextFieldInternal(
|
|||
minLines: Int,
|
||||
interactionSource: MutableInteractionSource,
|
||||
shape: Shape,
|
||||
colors: TextFieldColors,
|
||||
colors: ZashiTextFieldColors,
|
||||
modifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = Modifier,
|
||||
) {
|
||||
val borderColor by colors.borderColor(state)
|
||||
val androidColors = colors.toTextFieldColors()
|
||||
// If color is not provided via the text style, use content color as a default
|
||||
val textColor =
|
||||
textStyle.color.takeOrElse {
|
||||
colors.textColor(enabled, isError, interactionSource).value
|
||||
androidColors.textColor(state.isEnabled, state.isError, interactionSource).value
|
||||
}
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.selectionColors) {
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides androidColors.selectionColors) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
value = state.value.getValue(),
|
||||
modifier =
|
||||
modifier
|
||||
.defaultMinSize(minWidth = TextFieldDefaults.MinWidth),
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
innerModifier.fillMaxWidth() then
|
||||
if (borderColor == Color.Unspecified) {
|
||||
Modifier
|
||||
} else {
|
||||
Modifier.border(
|
||||
width = 1.dp,
|
||||
color = borderColor,
|
||||
shape = ZashiTextFieldDefaults.shape
|
||||
)
|
||||
} then Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth),
|
||||
onValueChange = state.onValueChange,
|
||||
enabled = state.isEnabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
cursorBrush = SolidColor(colors.cursorColor(isError).value),
|
||||
cursorBrush = SolidColor(androidColors.cursorColor(state.isError).value),
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
|
@ -151,7 +215,7 @@ private fun TextFieldInternal(
|
|||
decorationBox = @Composable { innerTextField ->
|
||||
// places leading icon, text field with label and placeholder, trailing icon
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = value,
|
||||
value = state.value.getValue(),
|
||||
visualTransformation = visualTransformation,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
|
@ -163,41 +227,31 @@ private fun TextFieldInternal(
|
|||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = enabled,
|
||||
isError = isError,
|
||||
enabled = state.isEnabled,
|
||||
isError = state.isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors,
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
colors = androidColors,
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
start = if (leadingIcon != null) 8.dp else 12.dp,
|
||||
end = 12.dp,
|
||||
top = if (trailingIcon != null || leadingIcon != null) 12.dp else 8.dp,
|
||||
bottom = if (trailingIcon != null || leadingIcon != null) 12.dp else 8.dp,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (state.error != null && state.error.getValue().isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = state.error.getValue(),
|
||||
style = ZashiTypography.textSm,
|
||||
color = colors.hintColor(state).value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextFieldColors.textColor(
|
||||
enabled: Boolean,
|
||||
isError: Boolean,
|
||||
interactionSource: InteractionSource
|
||||
): State<Color> {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
val targetValue =
|
||||
when {
|
||||
!enabled -> disabledTextColor
|
||||
isError -> errorTextColor
|
||||
focused -> focusedTextColor
|
||||
else -> unfocusedTextColor
|
||||
}
|
||||
return rememberUpdatedState(targetValue)
|
||||
}
|
||||
|
||||
private val TextFieldColors.selectionColors: TextSelectionColors
|
||||
@Composable get() = textSelectionColors
|
||||
|
||||
@Composable
|
||||
private fun TextFieldColors.cursorColor(isError: Boolean): State<Color> {
|
||||
return rememberUpdatedState(if (isError) errorCursorColor else cursorColor)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
@ -206,26 +260,58 @@ data class ZashiTextFieldColors(
|
|||
val hintColor: Color,
|
||||
val borderColor: Color,
|
||||
val containerColor: Color,
|
||||
)
|
||||
val placeholderColor: Color,
|
||||
val disabledTextColor: Color,
|
||||
val disabledHintColor: Color,
|
||||
val disabledBorderColor: Color,
|
||||
val disabledContainerColor: Color,
|
||||
val disabledPlaceholderColor: Color,
|
||||
val errorTextColor: Color,
|
||||
val errorHintColor: Color,
|
||||
val errorBorderColor: Color,
|
||||
val errorContainerColor: Color,
|
||||
val errorPlaceholderColor: Color,
|
||||
) {
|
||||
@Composable
|
||||
internal fun borderColor(state: TextFieldState): State<Color> {
|
||||
val targetValue =
|
||||
when {
|
||||
!state.isEnabled -> disabledBorderColor
|
||||
state.isError -> errorBorderColor
|
||||
else -> borderColor
|
||||
}
|
||||
return rememberUpdatedState(targetValue)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZashiTextFieldColors.toTextFieldColors() =
|
||||
@Composable
|
||||
internal fun hintColor(state: TextFieldState): State<Color> {
|
||||
val targetValue =
|
||||
when {
|
||||
!state.isEnabled -> disabledHintColor
|
||||
state.isError -> errorHintColor
|
||||
else -> hintColor
|
||||
}
|
||||
return rememberUpdatedState(targetValue)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun toTextFieldColors() =
|
||||
TextFieldDefaults.colors(
|
||||
focusedTextColor = textColor,
|
||||
unfocusedTextColor = textColor,
|
||||
disabledTextColor = textColor,
|
||||
errorTextColor = Color.Unspecified,
|
||||
disabledTextColor = disabledTextColor,
|
||||
errorTextColor = errorTextColor,
|
||||
focusedContainerColor = containerColor,
|
||||
unfocusedContainerColor = containerColor,
|
||||
disabledContainerColor = containerColor,
|
||||
errorContainerColor = Color.Unspecified,
|
||||
disabledContainerColor = disabledContainerColor,
|
||||
errorContainerColor = errorContainerColor,
|
||||
cursorColor = Color.Unspecified,
|
||||
errorCursorColor = Color.Unspecified,
|
||||
selectionColors = null,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Unspecified,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
focusedLeadingIconColor = Color.Unspecified,
|
||||
unfocusedLeadingIconColor = Color.Unspecified,
|
||||
disabledLeadingIconColor = Color.Unspecified,
|
||||
|
@ -238,14 +324,14 @@ private fun ZashiTextFieldColors.toTextFieldColors() =
|
|||
unfocusedLabelColor = Color.Unspecified,
|
||||
disabledLabelColor = Color.Unspecified,
|
||||
errorLabelColor = Color.Unspecified,
|
||||
focusedPlaceholderColor = hintColor,
|
||||
unfocusedPlaceholderColor = hintColor,
|
||||
disabledPlaceholderColor = hintColor,
|
||||
errorPlaceholderColor = Color.Unspecified,
|
||||
focusedPlaceholderColor = placeholderColor,
|
||||
unfocusedPlaceholderColor = placeholderColor,
|
||||
disabledPlaceholderColor = disabledPlaceholderColor,
|
||||
errorPlaceholderColor = errorPlaceholderColor,
|
||||
focusedSupportingTextColor = hintColor,
|
||||
unfocusedSupportingTextColor = hintColor,
|
||||
disabledSupportingTextColor = hintColor,
|
||||
errorSupportingTextColor = Color.Unspecified,
|
||||
disabledSupportingTextColor = disabledHintColor,
|
||||
errorSupportingTextColor = errorHintColor,
|
||||
focusedPrefixColor = Color.Unspecified,
|
||||
unfocusedPrefixColor = Color.Unspecified,
|
||||
disabledPrefixColor = Color.Unspecified,
|
||||
|
@ -255,29 +341,52 @@ private fun ZashiTextFieldColors.toTextFieldColors() =
|
|||
disabledSuffixColor = Color.Unspecified,
|
||||
errorSuffixColor = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
|
||||
object ZashiTextFieldDefaults {
|
||||
val shape: Shape
|
||||
get() = RoundedCornerShape(8.dp)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun defaultColors(
|
||||
textColor: Color = ZashiColors.Inputs.Default.text,
|
||||
textColor: Color = ZashiColors.Inputs.Filled.text,
|
||||
hintColor: Color = ZashiColors.Inputs.Default.hint,
|
||||
borderColor: Color = ZashiColors.Inputs.Default.stroke,
|
||||
containerColor: Color = ZashiColors.Inputs.Default.bg
|
||||
borderColor: Color = Color.Unspecified,
|
||||
containerColor: Color = ZashiColors.Inputs.Default.bg,
|
||||
placeholderColor: Color = ZashiColors.Inputs.Default.text,
|
||||
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
|
||||
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
|
||||
disabledBorderColor: Color = ZashiColors.Inputs.Disabled.stroke,
|
||||
disabledContainerColor: Color = ZashiColors.Inputs.Disabled.bg,
|
||||
disabledPlaceholderColor: Color = ZashiColors.Inputs.Disabled.text,
|
||||
errorTextColor: Color = ZashiColors.Inputs.ErrorFilled.text,
|
||||
errorHintColor: Color = ZashiColors.Inputs.ErrorDefault.hint,
|
||||
errorBorderColor: Color = ZashiColors.Inputs.ErrorDefault.stroke,
|
||||
errorContainerColor: Color = ZashiColors.Inputs.ErrorDefault.bg,
|
||||
errorPlaceholderColor: Color = ZashiColors.Inputs.ErrorDefault.text,
|
||||
) = ZashiTextFieldColors(
|
||||
textColor = textColor,
|
||||
hintColor = hintColor,
|
||||
borderColor = borderColor,
|
||||
containerColor = containerColor
|
||||
containerColor = containerColor,
|
||||
placeholderColor = placeholderColor,
|
||||
disabledTextColor = disabledTextColor,
|
||||
disabledHintColor = disabledHintColor,
|
||||
disabledBorderColor = disabledBorderColor,
|
||||
disabledContainerColor = disabledContainerColor,
|
||||
disabledPlaceholderColor = disabledPlaceholderColor,
|
||||
errorTextColor = errorTextColor,
|
||||
errorHintColor = errorHintColor,
|
||||
errorBorderColor = errorBorderColor,
|
||||
errorContainerColor = errorContainerColor,
|
||||
errorPlaceholderColor = errorPlaceholderColor,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun ZashiTextFieldPreview() =
|
||||
private fun DefaultPreview() =
|
||||
ZcashTheme {
|
||||
ZashiTextField(
|
||||
state =
|
||||
|
@ -286,3 +395,16 @@ private fun ZashiTextFieldPreview() =
|
|||
) {}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun ErrorPreview() =
|
||||
ZcashTheme {
|
||||
ZashiTextField(
|
||||
state =
|
||||
TextFieldState(
|
||||
value = stringRes("Text"),
|
||||
error = stringRes("Error"),
|
||||
) {}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,6 @@ import android.content.res.Configuration
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import kotlin.annotation.AnnotationRetention.SOURCE
|
||||
|
||||
// TODO [#1580]: Suppress compilation warning on PreviewScreens
|
||||
// https://github.com/Electric-Coin-Company/zashi-android/issues/1580
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Preview(name = "1: Light preview", showBackground = true)
|
||||
@Preview(name = "2: Dark preview", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Retention(SOURCE)
|
||||
|
|
|
@ -32,6 +32,10 @@ android {
|
|||
setOf(
|
||||
"src/main/res/ui/about",
|
||||
"src/main/res/ui/account",
|
||||
"src/main/res/ui/address_book",
|
||||
"src/main/res/ui/contact",
|
||||
"src/main/res/ui/add_contact",
|
||||
"src/main/res/ui/update_contact",
|
||||
"src/main/res/ui/advanced_settings",
|
||||
"src/main/res/ui/authentication",
|
||||
"src/main/res/ui/balances",
|
||||
|
|
|
@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.screen.send
|
|||
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
|
@ -49,11 +50,8 @@ internal fun ComposeContentTestRule.setValidAmount() {
|
|||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setAmount(amount: String) {
|
||||
onNodeWithText(
|
||||
getStringResourceWithArgs(
|
||||
R.string.send_amount_hint,
|
||||
ZcashCurrency.fromResources(getAppContext()).name
|
||||
)
|
||||
onNode(
|
||||
hasTestTag(SendTag.SEND_AMOUNT_FIELD)
|
||||
).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(amount)
|
||||
|
|
|
@ -26,6 +26,7 @@ class SettingsViewTestSetup(
|
|||
private val onBackgroundSyncChangedCount = AtomicInteger(0)
|
||||
private val onKeepScreenOnChangedCount = AtomicInteger(0)
|
||||
private val onAnalyticsChangedCount = AtomicInteger(0)
|
||||
private val onAddressBookCount = AtomicInteger(0)
|
||||
|
||||
private val settingsTroubleshootingState =
|
||||
if (isTroubleshootingEnabled) {
|
||||
|
@ -91,6 +92,11 @@ class SettingsViewTestSetup(
|
|||
return onAnalyticsChangedCount.get()
|
||||
}
|
||||
|
||||
fun getAddressBookCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onAddressBookCount.get()
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
|
@ -112,6 +118,9 @@ class SettingsViewTestSetup(
|
|||
onAboutUsClick = {
|
||||
onAboutCount.incrementAndGet()
|
||||
},
|
||||
onAddressBookClick = {
|
||||
onAddressBookCount.incrementAndGet()
|
||||
}
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package co.electriccoin.zcash.di
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
|
@ -12,4 +14,5 @@ val repositoryModule =
|
|||
module {
|
||||
singleOf(::WalletRepositoryImpl) bind WalletRepository::class
|
||||
singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class
|
||||
singleOf(::AddressBookRepositoryImpl) bind AddressBookRepository::class
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package co.electriccoin.zcash.di
|
||||
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
|
||||
|
@ -11,6 +14,10 @@ import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
|
|||
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
@ -29,4 +36,11 @@ val useCaseModule =
|
|||
singleOf(::ObserveConfigurationUseCase)
|
||||
singleOf(::RescanBlockchainUseCase)
|
||||
singleOf(::GetTransparentAddressUseCase)
|
||||
singleOf(::ObserveAddressBookContactsUseCase)
|
||||
singleOf(::ValidateContactAddressUseCase)
|
||||
singleOf(::ValidateContactNameUseCase)
|
||||
singleOf(::SaveContactUseCase)
|
||||
singleOf(::UpdateContactUseCase)
|
||||
singleOf(::DeleteContactUseCase)
|
||||
singleOf(::GetContactUseCase)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,11 @@ import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
|
|||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.account.viewmodel.TransactionHistoryViewModel
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettingsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
|
||||
|
@ -39,4 +42,7 @@ val viewModelModule =
|
|||
viewModelOf(::WhatsNewViewModel)
|
||||
viewModelOf(::UpdateViewModel)
|
||||
viewModelOf(::ChooseServerViewModel)
|
||||
viewModelOf(::AddressBookViewModel)
|
||||
viewModelOf(::AddContactViewModel)
|
||||
viewModelOf(::UpdateContactViewModel)
|
||||
}
|
||||
|
|
|
@ -12,11 +12,14 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptionsBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.NavigationArgs.UPDATE_CONTACT_ID
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_INITIAL_STAGE
|
||||
|
@ -25,6 +28,8 @@ import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_PROPOSAL
|
|||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_RECIPIENT_ADDRESS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADDRESS_BOOK
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADD_NEW_CONTACT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.CHOOSE_SERVER
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.DELETE_WALLET
|
||||
|
@ -38,6 +43,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.SEND_CONFIRMATION
|
|||
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.UPDATE_CONTACT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||
|
@ -48,10 +54,13 @@ 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.about.WrapAbout
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.WrapAddressBook
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.WrapAdvancedSettings
|
||||
import co.electriccoin.zcash.ui.screen.authentication.AuthenticationUseCase
|
||||
import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication
|
||||
import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer
|
||||
import co.electriccoin.zcash.ui.screen.contact.WrapAddContact
|
||||
import co.electriccoin.zcash.ui.screen.contact.WrapUpdateContact
|
||||
import co.electriccoin.zcash.ui.screen.deletewallet.WrapDeleteWallet
|
||||
import co.electriccoin.zcash.ui.screen.disconnected.WrapDisconnected
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.optin.AndroidExchangeRateOptIn
|
||||
|
@ -263,6 +272,19 @@ internal fun MainActivity.Navigation() {
|
|||
goSettings = { navController.navigateJustOnce(SETTINGS) }
|
||||
)
|
||||
}
|
||||
composable(ADDRESS_BOOK) {
|
||||
WrapAddressBook()
|
||||
}
|
||||
composable(ADD_NEW_CONTACT) {
|
||||
WrapAddContact()
|
||||
}
|
||||
composable(
|
||||
route = "$UPDATE_CONTACT/{$UPDATE_CONTACT_ID}",
|
||||
arguments = listOf(navArgument(UPDATE_CONTACT_ID) { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
val contactId = backStackEntry.arguments?.getString(UPDATE_CONTACT_ID).orEmpty()
|
||||
WrapUpdateContact(contactId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -449,4 +471,11 @@ object NavigationTargets {
|
|||
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
|
||||
const val SUPPORT = "support"
|
||||
const val WHATS_NEW = "whats_new"
|
||||
const val ADDRESS_BOOK = "address_book"
|
||||
const val ADD_NEW_CONTACT = "add_new_contact"
|
||||
const val UPDATE_CONTACT = "update_contact"
|
||||
}
|
||||
|
||||
object NavigationArgs {
|
||||
const val UPDATE_CONTACT_ID = "contactId"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class AddressBookContact(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
)
|
|
@ -0,0 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common.model
|
||||
|
||||
@JvmInline
|
||||
value class ValidContactName(val value: String)
|
|
@ -0,0 +1,65 @@
|
|||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
interface AddressBookRepository {
|
||||
val contacts: StateFlow<List<AddressBookContact>>
|
||||
|
||||
suspend fun saveContact(
|
||||
name: String,
|
||||
address: String
|
||||
)
|
||||
|
||||
suspend fun updateContact(
|
||||
contact: AddressBookContact,
|
||||
name: String,
|
||||
address: String
|
||||
)
|
||||
|
||||
suspend fun deleteContact(contact: AddressBookContact)
|
||||
|
||||
suspend fun getContact(id: String): AddressBookContact?
|
||||
}
|
||||
|
||||
class AddressBookRepositoryImpl : AddressBookRepository {
|
||||
override val contacts = MutableStateFlow(emptyList<AddressBookContact>())
|
||||
|
||||
override suspend fun saveContact(
|
||||
name: String,
|
||||
address: String
|
||||
) {
|
||||
contacts.update { it + AddressBookContact(name = name.trim(), address = address.trim()) }
|
||||
}
|
||||
|
||||
override suspend fun updateContact(
|
||||
contact: AddressBookContact,
|
||||
name: String,
|
||||
address: String
|
||||
) {
|
||||
contacts.update {
|
||||
it.toMutableList()
|
||||
.apply {
|
||||
set(
|
||||
it.indexOf(contact),
|
||||
AddressBookContact(name = name.trim(), address = address.trim())
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteContact(contact: AddressBookContact) {
|
||||
contacts.update {
|
||||
contacts.value.toMutableList()
|
||||
.apply {
|
||||
remove(contact)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getContact(id: String): AddressBookContact? = contacts.value.find { it.id == id }
|
||||
}
|
|
@ -61,6 +61,8 @@ interface WalletRepository {
|
|||
suspend fun getSelectedServer(): LightWalletEndpoint
|
||||
|
||||
suspend fun getAllServers(): List<LightWalletEndpoint>
|
||||
|
||||
suspend fun getSynchronizer(): Synchronizer
|
||||
}
|
||||
|
||||
class WalletRepositoryImpl(
|
||||
|
@ -238,4 +240,6 @@ class WalletRepositoryImpl(
|
|||
defaultServers + selectedServer
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSynchronizer(): Synchronizer = synchronizer.filterNotNull().first()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
|
||||
class DeleteContactUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
suspend operator fun invoke(contact: AddressBookContact) {
|
||||
addressBookRepository.deleteContact(contact)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
|
||||
class GetContactUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
suspend operator fun invoke(id: String) = addressBookRepository.getContact(id)
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class GetSynchronizerUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
) {
|
||||
suspend operator fun invoke() = walletRepository.synchronizer.filterNotNull().first()
|
||||
suspend operator fun invoke() = walletRepository.getSynchronizer()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
|
||||
class ObserveAddressBookContactsUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
operator fun invoke() = addressBookRepository.contacts
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
|
||||
class SaveContactUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
name: String,
|
||||
address: String
|
||||
) {
|
||||
addressBookRepository.saveContact(name = name, address = address)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
|
||||
class UpdateContactUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
contact: AddressBookContact,
|
||||
name: String,
|
||||
address: String
|
||||
) {
|
||||
addressBookRepository.updateContact(contact = contact, name = name, address = address)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
|
||||
class ValidateContactAddressUseCase(
|
||||
private val addressBookRepository: AddressBookRepository,
|
||||
private val walletRepository: WalletRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
address: String,
|
||||
exclude: AddressBookContact? = null
|
||||
): Result {
|
||||
val result = walletRepository.getSynchronizer().validateAddress(address)
|
||||
return when {
|
||||
result.isNotValid -> Result.Invalid
|
||||
addressBookRepository.contacts.value
|
||||
.filter {
|
||||
if (exclude == null) true else it != exclude
|
||||
}
|
||||
.any { it.address == address.trim() } -> Result.NotUnique
|
||||
|
||||
else -> Result.Valid
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object Valid : Result
|
||||
|
||||
data object Invalid : Result
|
||||
|
||||
data object NotUnique : Result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
|
||||
class ValidateContactNameUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
operator fun invoke(
|
||||
name: String,
|
||||
exclude: AddressBookContact? = null
|
||||
) = when {
|
||||
name.length > CONTACT_NAME_MAX_LENGTH -> Result.TooLong
|
||||
addressBookRepository.contacts.value
|
||||
.filter {
|
||||
if (exclude == null) true else it != exclude
|
||||
}
|
||||
.any { it.name == name.trim() } -> Result.NotUnique
|
||||
|
||||
else -> Result.Valid
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object Valid : Result
|
||||
|
||||
data object TooLong : Result
|
||||
|
||||
data object NotUnique : Result
|
||||
}
|
||||
}
|
||||
|
||||
private const val CONTACT_NAME_MAX_LENGTH = 32
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.BorderStroke
|
|||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
@ -18,12 +19,13 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.DividerDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
@ -34,8 +36,11 @@ import androidx.compose.ui.platform.testTag
|
|||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -45,7 +50,6 @@ import cash.z.ecc.android.sdk.model.TransactionOverview
|
|||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.TransactionState
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.sdk.extension.DEFAULT_FEE
|
||||
import cash.z.ecc.sdk.extension.toZecStringAbbreviated
|
||||
import cash.z.ecc.sdk.extension.toZecStringFull
|
||||
|
@ -61,7 +65,11 @@ import co.electriccoin.zcash.ui.design.component.CircularMidProgressIndicator
|
|||
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.TextWithIcon
|
||||
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.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.account.HistoryTag
|
||||
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionUiFixture
|
||||
|
@ -257,7 +265,7 @@ private fun ComposableHistoryListItemPreview() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
@Preview("History List Item Expanded")
|
||||
@PreviewScreens
|
||||
private fun ComposableHistoryListItemExpandedPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface {
|
||||
|
@ -321,43 +329,49 @@ private fun HistoryItem(
|
|||
TransactionExtendedState.SENT -> {
|
||||
typeText = stringResource(id = R.string.account_history_item_sent)
|
||||
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_send_icon)
|
||||
textColor = MaterialTheme.colorScheme.onBackground
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular
|
||||
textColor = ZashiColors.Text.textPrimary
|
||||
textStyle = ZashiTypography.textSm
|
||||
}
|
||||
|
||||
TransactionExtendedState.SENDING -> {
|
||||
typeText = stringResource(id = R.string.account_history_item_sending)
|
||||
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_send_icon)
|
||||
textColor = ZcashTheme.colors.textDescription
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRunning
|
||||
textColor = ZashiColors.Text.textPrimary
|
||||
textStyle = ZashiTypography.textSm
|
||||
}
|
||||
|
||||
TransactionExtendedState.SEND_FAILED -> {
|
||||
typeText = stringResource(id = R.string.account_history_item_send_failed)
|
||||
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_send_icon)
|
||||
textColor = ZcashTheme.colors.historyRedColor
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleFailed
|
||||
textColor = ZashiColors.Text.textError
|
||||
textStyle =
|
||||
ZashiTypography.textSm.copy(
|
||||
textDecoration = TextDecoration.LineThrough
|
||||
)
|
||||
}
|
||||
|
||||
TransactionExtendedState.RECEIVED -> {
|
||||
typeText = stringResource(id = R.string.account_history_item_received)
|
||||
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_receive_icon)
|
||||
textColor = MaterialTheme.colorScheme.onBackground
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular
|
||||
textColor = ZashiColors.Text.textPrimary
|
||||
textStyle = ZashiTypography.textSm
|
||||
}
|
||||
|
||||
TransactionExtendedState.RECEIVING -> {
|
||||
typeText = stringResource(id = R.string.account_history_item_receiving)
|
||||
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_receive_icon)
|
||||
textColor = ZcashTheme.colors.textDescription
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRunning
|
||||
textColor = ZashiColors.Text.textPrimary
|
||||
textStyle = ZashiTypography.textSm
|
||||
}
|
||||
|
||||
TransactionExtendedState.RECEIVE_FAILED -> {
|
||||
typeText = stringResource(id = R.string.account_history_item_receive_failed)
|
||||
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_receive_icon)
|
||||
textColor = ZcashTheme.colors.historyRedColor
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleFailed
|
||||
textColor = ZashiColors.Text.textError
|
||||
textStyle =
|
||||
ZashiTypography.textSm.copy(
|
||||
textDecoration = TextDecoration.LineThrough
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -374,15 +388,22 @@ private fun HistoryItem(
|
|||
TrxItemState.EXPANDED
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onAction(
|
||||
TrxItemAction.ExpandableStateChange(
|
||||
transaction.overview.rawId,
|
||||
TrxItemState.COLLAPSED
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(all = ZcashTheme.dimens.spacingLarge)
|
||||
.padding(24.dp)
|
||||
.animateContentSize()
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
imageVector = typeIcon,
|
||||
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Text.textPrimary),
|
||||
contentDescription = typeText,
|
||||
modifier = Modifier.padding(top = ZcashTheme.dimens.spacingTiny)
|
||||
)
|
||||
|
@ -399,12 +420,10 @@ private fun HistoryItem(
|
|||
onAction = onAction
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXtiny))
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
// To add an extra spacing at the end
|
||||
Column(
|
||||
modifier = Modifier.padding(end = ZcashTheme.dimens.spacingUpLarge)
|
||||
) {
|
||||
Column {
|
||||
val isInExpectedState =
|
||||
transaction.expandableState == TrxItemState.EXPANDED_ADDRESS ||
|
||||
transaction.expandableState == TrxItemState.EXPANDED_ALL
|
||||
|
@ -415,13 +434,13 @@ private fun HistoryItem(
|
|||
) {
|
||||
HistoryItemExpandedAddressPart(onAction, transaction.recipient)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
HistoryItemDatePart(transaction)
|
||||
|
||||
if (transaction.expandableState.isInAnyExtendedState()) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
HistoryItemExpandedPart(onAction, transaction)
|
||||
}
|
||||
|
@ -431,7 +450,7 @@ private fun HistoryItem(
|
|||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
private fun HistoryItemCollapsedMainPart(
|
||||
transaction: TransactionUi,
|
||||
typeText: String,
|
||||
|
@ -454,6 +473,7 @@ private fun HistoryItemCollapsedMainPart(
|
|||
text = typeText,
|
||||
style = textStyle,
|
||||
color = textColor,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.testTag(HistoryTag.TRANSACTION_ITEM_TITLE)
|
||||
)
|
||||
|
||||
|
@ -468,15 +488,18 @@ private fun HistoryItemCollapsedMainPart(
|
|||
val valueTextStyle: TextStyle
|
||||
val valueTextColor: Color
|
||||
if (transaction.overview.getExtendedState().isFailed()) {
|
||||
valueTextStyle = ZcashTheme.extendedTypography.transactionItemStyles.contentLineThrough
|
||||
valueTextColor = ZcashTheme.colors.historyRedColor
|
||||
valueTextStyle =
|
||||
ZashiTypography.textSm.copy(
|
||||
textDecoration = TextDecoration.LineThrough
|
||||
)
|
||||
valueTextColor = ZashiColors.Text.textError
|
||||
} else {
|
||||
valueTextStyle = ZcashTheme.extendedTypography.transactionItemStyles.valueFirstPart
|
||||
valueTextStyle = ZashiTypography.textSm
|
||||
valueTextColor =
|
||||
if (transaction.overview.isSentTransaction) {
|
||||
ZcashTheme.colors.historyRedColor
|
||||
ZashiColors.Text.textError
|
||||
} else {
|
||||
ZcashTheme.colors.textPrimary
|
||||
ZashiColors.Text.textPrimary
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -500,7 +523,7 @@ private fun HistoryItemCollapsedMainPart(
|
|||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = valueTextStyle,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.transactionItemStyles.valueSecondPart
|
||||
leastSignificantPart = ZashiTypography.textXxs
|
||||
),
|
||||
textColor = valueTextColor,
|
||||
)
|
||||
|
@ -545,8 +568,8 @@ private fun HistoryItemCollapsedAddressPart(
|
|||
|
||||
Text(
|
||||
text = transaction.recipient.addressValue,
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.addressCollapsed,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
|
@ -580,26 +603,29 @@ private fun HistoryItemExpandedAddressPart(
|
|||
) {
|
||||
Text(
|
||||
text = recipient.addressValue,
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(EXPANDED_ADDRESS_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
TextWithIcon(
|
||||
text = stringResource(id = R.string.account_history_item_tap_to_copy),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Btns.Tertiary.btnTertiaryFg,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
iconVector = ImageVector.vectorResource(R.drawable.ic_trx_copy),
|
||||
iconTintColor = ZcashTheme.colors.secondaryColor,
|
||||
iconTintColor = ZashiColors.Text.textTertiary,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||
.clickable { onAction(TrxItemAction.AddressClick(recipient)) }
|
||||
.padding(all = ZcashTheme.dimens.spacingTiny)
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(radius = 2.dp, color = ZashiColors.Text.textTertiary),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onAction(TrxItemAction.AddressClick(recipient)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -619,8 +645,8 @@ private fun HistoryItemDatePart(
|
|||
if (formattedDate != null) {
|
||||
Text(
|
||||
text = formattedDate,
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
|
@ -643,8 +669,8 @@ private fun HistoryItemExpandedPart(
|
|||
id = R.plurals.account_history_item_message,
|
||||
count = transaction.messages!!.size
|
||||
),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.contentMedium,
|
||||
color = ZcashTheme.colors.textPrimary
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
@ -662,51 +688,17 @@ private fun HistoryItemExpandedPart(
|
|||
)
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
} else if (transaction.recipientAddressType == null ||
|
||||
transaction.recipientAddressType == AddressType.Shielded
|
||||
}
|
||||
|
||||
HistoryItemTransactionIdPart(transaction = transaction, onAction = onAction)
|
||||
|
||||
if (transaction.overview.getExtendedState() !in
|
||||
listOf(TransactionExtendedState.RECEIVING, TransactionExtendedState.RECEIVED)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.account_history_item_no_message),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.contentItalic,
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
modifier = Modifier.fillMaxWidth(EXPANDED_TRANSACTION_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
|
||||
HistoryItemTransactionIdPart(
|
||||
transaction = transaction,
|
||||
onAction = onAction
|
||||
)
|
||||
|
||||
Spacer(modifier = (Modifier.height(ZcashTheme.dimens.spacingDefault)))
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HistoryItemTransactionFeePart(fee = transaction.overview.feePaid)
|
||||
|
||||
Spacer(modifier = (Modifier.height(ZcashTheme.dimens.spacingLarge)))
|
||||
|
||||
TextWithIcon(
|
||||
text = stringResource(id = R.string.account_history_item_collapse_transaction),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.contentUnderline,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
iconVector = ImageVector.vectorResource(id = R.drawable.ic_trx_collapse),
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||
.clickable {
|
||||
if (transaction.expandableState >= TrxItemState.EXPANDED) {
|
||||
onAction(
|
||||
TrxItemAction.ExpandableStateChange(
|
||||
transaction.overview.rawId,
|
||||
TrxItemState.COLLAPSED
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(all = ZcashTheme.dimens.spacingTiny)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<String>?.containsValidMemo(): Boolean {
|
||||
|
@ -735,35 +727,38 @@ private fun HistoryItemTransactionIdPart(
|
|||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.account_history_item_transaction_id),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = txIdString,
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(EXPANDED_TRANSACTION_WIDTH_RATIO)
|
||||
.testTag(HistoryTag.TRANSACTION_ID)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
TextWithIcon(
|
||||
text = stringResource(id = R.string.account_history_item_tap_to_copy),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Btns.Tertiary.btnTertiaryFg,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
iconVector = ImageVector.vectorResource(R.drawable.ic_trx_copy),
|
||||
iconTintColor = ZcashTheme.colors.secondaryColor,
|
||||
iconTintColor = ZashiColors.Text.textTertiary,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||
.clickable { onAction(TrxItemAction.TransactionIdClick(txIdString)) }
|
||||
.padding(all = ZcashTheme.dimens.spacingTiny)
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(radius = 2.dp, color = ZashiColors.Text.textTertiary),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onAction(TrxItemAction.TransactionIdClick(txIdString)) }
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
|
@ -782,21 +777,21 @@ private fun HistoryItemTransactionIdPart(
|
|||
)
|
||||
)
|
||||
}
|
||||
.padding(all = ZcashTheme.dimens.spacingTiny)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.account_history_item_transaction_id),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
text = txIdString,
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.End,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
|
@ -813,14 +808,14 @@ private fun HistoryItemTransactionFeePart(
|
|||
fee: Zatoshi?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(modifier = modifier) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.account_history_item_transaction_fee),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (fee == null) {
|
||||
Text(
|
||||
|
@ -829,8 +824,8 @@ private fun HistoryItemTransactionFeePart(
|
|||
id = R.string.account_history_item_transaction_fee_typical,
|
||||
DEFAULT_FEE
|
||||
),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.feeFirstPart,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
)
|
||||
} else {
|
||||
StyledBalance(
|
||||
|
@ -839,10 +834,10 @@ private fun HistoryItemTransactionFeePart(
|
|||
isHideBalances = false,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.transactionItemStyles.feeFirstPart,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.transactionItemStyles.feeSecondPart
|
||||
mostSignificantPart = ZashiTypography.textSm,
|
||||
leastSignificantPart = ZashiTypography.textXxs
|
||||
),
|
||||
textColor = ZcashTheme.colors.textDescription
|
||||
textColor = ZashiColors.Text.textTertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -858,11 +853,14 @@ private fun HistoryItemMessagePart(
|
|||
val textStyle: TextStyle
|
||||
val textColor: Color
|
||||
if (state.isFailed()) {
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.contentLineThrough
|
||||
textColor = ZcashTheme.colors.historyRedColor
|
||||
textStyle =
|
||||
ZashiTypography.textSm.copy(
|
||||
textDecoration = TextDecoration.LineThrough
|
||||
)
|
||||
textColor = ZashiColors.Text.textError
|
||||
} else {
|
||||
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.content
|
||||
textColor = ZcashTheme.colors.textPrimary
|
||||
textStyle = ZashiTypography.textSm
|
||||
textColor = ZashiColors.Text.textPrimary
|
||||
}
|
||||
|
||||
Column(modifier = modifier.then(Modifier.fillMaxWidth())) {
|
||||
|
@ -870,12 +868,12 @@ private fun HistoryItemMessagePart(
|
|||
val bubbleStroke: BorderStroke
|
||||
val arrowAlignment: BubbleArrowAlignment
|
||||
if (state.isSendType()) {
|
||||
bubbleBackgroundColor = Color.Transparent
|
||||
bubbleStroke = BorderStroke(1.dp, ZcashTheme.colors.textFieldFrame)
|
||||
bubbleBackgroundColor = ZashiColors.Utility.Gray.utilityGray200 orDark Color.Transparent
|
||||
bubbleStroke = BorderStroke(1.dp, ZashiColors.Text.textPrimary)
|
||||
arrowAlignment = BubbleArrowAlignment.BottomLeft
|
||||
} else {
|
||||
bubbleBackgroundColor = ZcashTheme.colors.historyMessageBubbleColor
|
||||
bubbleStroke = BorderStroke(1.dp, ZcashTheme.colors.historyMessageBubbleStrokeColor)
|
||||
bubbleBackgroundColor = ZashiColors.Utility.Gray.utilityGray200 orDark Color.Transparent
|
||||
bubbleStroke = BorderStroke(1.dp, ZashiColors.Text.textPrimary)
|
||||
arrowAlignment = BubbleArrowAlignment.BottomRight
|
||||
}
|
||||
|
||||
|
@ -893,19 +891,23 @@ private fun HistoryItemMessagePart(
|
|||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
TextWithIcon(
|
||||
text = stringResource(id = R.string.account_history_item_tap_to_copy),
|
||||
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
|
||||
color = ZcashTheme.colors.textDescription,
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Btns.Tertiary.btnTertiaryFg,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
iconVector = ImageVector.vectorResource(R.drawable.ic_trx_copy),
|
||||
iconTintColor = ZcashTheme.colors.secondaryColor,
|
||||
iconTintColor = ZashiColors.Text.textTertiary,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||
.clickable { onAction(TrxItemAction.MessageClick(message)) }
|
||||
.padding(all = ZcashTheme.dimens.spacingTiny)
|
||||
.clickable(
|
||||
onClick = { onAction(TrxItemAction.MessageClick(message)) },
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(radius = 2.dp, color = ZashiColors.Text.textTertiary),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package co.electriccoin.zcash.ui.screen.addressbook
|
||||
|
||||
object AddressBookTag {
|
||||
const val TOP_APP_BAR = "top_app_bar"
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.addressbook
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.view.AddressBookView
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun WrapAddressBook() {
|
||||
val navController = LocalNavController.current
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val viewModel = koinViewModel<AddressBookViewModel>()
|
||||
val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.navigationCommand.collect {
|
||||
navController.navigate(it)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.backNavigationCommand.collect {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
state.onBack()
|
||||
}
|
||||
|
||||
AddressBookView(
|
||||
state = state,
|
||||
topAppBarSubTitleState = walletState,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package co.electriccoin.zcash.ui.screen.addressbook.model
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
|
||||
data class AddressBookState(
|
||||
val contacts: List<AddressBookContactState>,
|
||||
val isLoading: Boolean,
|
||||
val version: StringResource,
|
||||
val onBack: () -> Unit,
|
||||
val addButton: ButtonState
|
||||
)
|
||||
|
||||
data class AddressBookContactState(
|
||||
val initials: StringResource,
|
||||
val isShielded: Boolean,
|
||||
val name: StringResource,
|
||||
val address: StringResource,
|
||||
val onClick: () -> Unit,
|
||||
)
|
|
@ -0,0 +1,343 @@
|
|||
package co.electriccoin.zcash.ui.screen.addressbook.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiBottomBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiHorizontalDivider
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListTrailingItem
|
||||
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.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookTag
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookContactState
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookState
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun AddressBookView(
|
||||
state: AddressBookState,
|
||||
topAppBarSubTitleState: TopAppBarSubTitleState
|
||||
) {
|
||||
BlankBgScaffold(
|
||||
topBar = {
|
||||
AddressBookTopAppBar(onBack = state.onBack, subTitleState = topAppBarSubTitleState)
|
||||
}
|
||||
) { paddingValues ->
|
||||
when {
|
||||
state.contacts.isEmpty() && state.isLoading -> {
|
||||
CircularScreenProgressIndicator()
|
||||
}
|
||||
|
||||
state.contacts.isEmpty() && !state.isLoading -> {
|
||||
Empty(
|
||||
state = state,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding(),
|
||||
start = 4.dp,
|
||||
end = 4.dp
|
||||
)
|
||||
) {
|
||||
itemsIndexed(state.contacts) { index, item ->
|
||||
ContactItem(state = item)
|
||||
if (index != state.contacts.lastIndex) {
|
||||
ZashiHorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZashiBottomBar {
|
||||
AddContactButton(
|
||||
state.addButton,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactItem(state: AddressBookContactState) {
|
||||
ZashiSettingsListItem(
|
||||
leading = { modifier ->
|
||||
ContactItemLeading(modifier = modifier, state = state)
|
||||
},
|
||||
content = { modifier ->
|
||||
ContactItemContent(modifier = modifier, state = state)
|
||||
},
|
||||
trailing = { modifier ->
|
||||
ZashiSettingsListTrailingItem(
|
||||
modifier = modifier,
|
||||
isEnabled = true,
|
||||
contentDescription = state.name.getValue()
|
||||
)
|
||||
},
|
||||
onClick = state.onClick,
|
||||
contentPadding = PaddingValues(top = 12.dp, bottom = if (state.isShielded) 8.dp else 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactItemLeading(
|
||||
state: AddressBookContactState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier.size(height = 50.dp, width = 54.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(ZashiColors.Avatars.avatarBg, CircleShape)
|
||||
.size(40.dp)
|
||||
.padding(top = 10.dp)
|
||||
.align(Alignment.Center),
|
||||
text = state.initials.getValue(),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Avatars.avatarTextFg,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (state.isShielded) {
|
||||
Image(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_address_book_shielded),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactItemContent(
|
||||
state: AddressBookContactState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = state.name.getValue(),
|
||||
style = ZashiTypography.textMd,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = state.address.getValue(),
|
||||
style = ZashiTypography.textXs,
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Empty(
|
||||
state: AddressBookState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(painter = painterResource(id = R.drawable.ic_address_book_empty), contentDescription = "")
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.address_book_empty),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = ZashiColors.Text.textPrimary,
|
||||
style = ZashiTypography.header6
|
||||
)
|
||||
}
|
||||
|
||||
AddContactButton(
|
||||
state.addButton,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddContactButton(
|
||||
state: ButtonState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ZashiButton(
|
||||
modifier = modifier,
|
||||
state = state
|
||||
) { scope ->
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_address_book_plus),
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Btns.Primary.btnPrimaryFg),
|
||||
contentDescription = ""
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
scope.Text()
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
scope.Loading()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressBookTopAppBar(
|
||||
onBack: () -> Unit,
|
||||
subTitleState: TopAppBarSubTitleState,
|
||||
) {
|
||||
ZashiSmallTopAppBar(
|
||||
title = stringResource(id = R.string.address_book_title),
|
||||
subtitle =
|
||||
when (subTitleState) {
|
||||
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
|
||||
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
|
||||
TopAppBarSubTitleState.None -> null
|
||||
},
|
||||
modifier = Modifier.testTag(AddressBookTag.TOP_APP_BAR),
|
||||
showTitleLogo = true,
|
||||
navigationAction = {
|
||||
ZashiTopAppBarBackNavigation(onBack = onBack)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun DataPreview() {
|
||||
ZcashTheme {
|
||||
AddressBookView(
|
||||
state =
|
||||
AddressBookState(
|
||||
isLoading = false,
|
||||
version = stringRes("Version 1.2"),
|
||||
onBack = {},
|
||||
contacts =
|
||||
(1..10).map {
|
||||
AddressBookContactState(
|
||||
name = stringRes("Name Surname"),
|
||||
address = stringRes("3iY5ZSkRnevzSMu4hosasdasdasdasd12312312dasd9hw2"),
|
||||
initials = stringRes("NS"),
|
||||
isShielded = it % 2 == 0,
|
||||
onClick = {}
|
||||
)
|
||||
},
|
||||
addButton =
|
||||
ButtonState(
|
||||
text = stringRes("Add New Contact"),
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun LoadingPreview() {
|
||||
ZcashTheme {
|
||||
AddressBookView(
|
||||
state =
|
||||
AddressBookState(
|
||||
isLoading = true,
|
||||
version = stringRes("Version 1.2"),
|
||||
onBack = {},
|
||||
contacts = emptyList(),
|
||||
addButton =
|
||||
ButtonState(
|
||||
text = stringRes("Add New Contact"),
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun EmptyPreview() {
|
||||
ZcashTheme {
|
||||
AddressBookView(
|
||||
state =
|
||||
AddressBookState(
|
||||
isLoading = false,
|
||||
version = stringRes("Version 1.2"),
|
||||
onBack = {},
|
||||
contacts = emptyList(),
|
||||
addButton =
|
||||
ButtonState(
|
||||
text = stringRes("Add New Contact"),
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package co.electriccoin.zcash.ui.screen.addressbook.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADD_NEW_CONTACT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.UPDATE_CONTACT
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookContactState
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddressBookViewModel(
|
||||
observeAddressBookContacts: ObserveAddressBookContactsUseCase,
|
||||
getVersionInfo: GetVersionInfoProvider,
|
||||
) : ViewModel() {
|
||||
private val versionInfo = getVersionInfo()
|
||||
|
||||
val state =
|
||||
observeAddressBookContacts()
|
||||
.map { contacts -> createState(contacts = contacts, isLoading = false) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = createState(contacts = emptyList(), isLoading = true)
|
||||
)
|
||||
|
||||
val navigationCommand = MutableSharedFlow<String>()
|
||||
|
||||
val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||
|
||||
private fun createState(
|
||||
contacts: List<AddressBookContact>,
|
||||
isLoading: Boolean
|
||||
) = AddressBookState(
|
||||
version = stringRes(R.string.address_book_version, versionInfo.versionName),
|
||||
isLoading = isLoading,
|
||||
contacts =
|
||||
contacts.map { contact ->
|
||||
AddressBookContactState(
|
||||
initials = getContactInitials(contact),
|
||||
isShielded = false,
|
||||
name = stringRes(contact.name),
|
||||
address = stringRes(contact.address),
|
||||
onClick = { onUpdateContactClick(contact) }
|
||||
)
|
||||
},
|
||||
onBack = ::onBack,
|
||||
addButton =
|
||||
ButtonState(
|
||||
onClick = ::onAddContactClick,
|
||||
text = stringRes(R.string.address_book_add)
|
||||
)
|
||||
)
|
||||
|
||||
private fun getContactInitials(contact: AddressBookContact) =
|
||||
stringRes(
|
||||
contact.name
|
||||
.split(" ")
|
||||
.mapNotNull { part ->
|
||||
part.takeIf { it.isNotEmpty() }?.first()?.toString()
|
||||
}
|
||||
.take(2)
|
||||
.joinToString(separator = "")
|
||||
)
|
||||
|
||||
private fun onBack() =
|
||||
viewModelScope.launch {
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onUpdateContactClick(contact: AddressBookContact) =
|
||||
viewModelScope.launch {
|
||||
navigationCommand.emit("$UPDATE_CONTACT/${contact.id}")
|
||||
}
|
||||
|
||||
private fun onAddContactClick() =
|
||||
viewModelScope.launch {
|
||||
navigationCommand.emit(ADD_NEW_CONTACT)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.screen.advancedsettings
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
|
||||
data class AdvancedSettingsState(
|
||||
val onBack: () -> Unit,
|
||||
|
@ -9,5 +9,5 @@ data class AdvancedSettingsState(
|
|||
val onChooseServerClick: () -> Unit,
|
||||
val onCurrencyConversionClick: () -> Unit,
|
||||
val onDeleteZashiClick: () -> Unit,
|
||||
val coinbaseButton: ButtonState?,
|
||||
val coinbaseButton: ZashiSettingsListItemState?,
|
||||
)
|
||||
|
|
|
@ -25,11 +25,11 @@ import androidx.compose.ui.unit.sp
|
|||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiHorizontalDivider
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
|
@ -172,7 +172,7 @@ private fun AdvancedSettingsPreview() =
|
|||
onCurrencyConversionClick = {},
|
||||
onDeleteZashiClick = {},
|
||||
coinbaseButton =
|
||||
ButtonState(
|
||||
ZashiSettingsListItemState(
|
||||
text = stringRes("Coinbase"),
|
||||
onClick = {}
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ import co.electriccoin.zcash.ui.R
|
|||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
@ -33,7 +33,7 @@ class AdvancedSettingsViewModel(
|
|||
onCurrencyConversionClick = ::onCurrencyConversionClick,
|
||||
onDeleteZashiClick = {},
|
||||
coinbaseButton =
|
||||
ButtonState(
|
||||
ZashiSettingsListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from the UI
|
||||
// in the Testnet build
|
||||
text = stringRes(R.string.advanced_settings_coinbase, getZcashCurrency.getLocalizedName()),
|
||||
|
|
|
@ -436,7 +436,8 @@ private fun CustomServerRadioButton(
|
|||
colors =
|
||||
ZashiTextFieldDefaults.defaultColors(
|
||||
containerColor = ZashiColors.Surfaces.bgPrimary,
|
||||
textColor = ZashiColors.Text.textPrimary
|
||||
textColor = ZashiColors.Text.textPrimary,
|
||||
borderColor = ZashiColors.Inputs.Default.stroke,
|
||||
) orDark
|
||||
ZashiTextFieldDefaults.defaultColors(
|
||||
containerColor = ZashiColors.Surfaces.bgSecondary,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.contact
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.view.ContactView
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun WrapAddContact() {
|
||||
val navController = LocalNavController.current
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val viewModel = koinViewModel<AddContactViewModel>()
|
||||
val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.navigationCommand.collect {
|
||||
navController.navigate(it)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.backNavigationCommand.collect {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
|
||||
state?.let {
|
||||
ContactView(
|
||||
state = it,
|
||||
topAppBarSubTitleState = walletState,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.contact
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.view.ContactView
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
internal fun WrapUpdateContact(contactId: String) {
|
||||
val navController = LocalNavController.current
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val viewModel = koinViewModel<UpdateContactViewModel> { parametersOf(contactId) }
|
||||
val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.navigationCommand.collect {
|
||||
navController.navigate(it)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.backNavigationCommand.collect {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
|
||||
state?.let {
|
||||
ContactView(
|
||||
state = it,
|
||||
topAppBarSubTitleState = walletState,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package co.electriccoin.zcash.ui.screen.contact
|
||||
|
||||
object ContactTag {
|
||||
const val TOP_APP_BAR = "top_app_bar"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package co.electriccoin.zcash.ui.screen.contact.model
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.TextFieldState
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
|
||||
data class ContactState(
|
||||
val title: StringResource,
|
||||
val isLoading: Boolean,
|
||||
val walletAddress: TextFieldState,
|
||||
val contactName: TextFieldState,
|
||||
val negativeButton: ButtonState?,
|
||||
val positiveButton: ButtonState,
|
||||
val onBack: () -> Unit,
|
||||
)
|
|
@ -0,0 +1,200 @@
|
|||
package co.electriccoin.zcash.ui.screen.contact.view
|
||||
|
||||
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.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
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.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTextField
|
||||
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.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.contact.ContactTag
|
||||
import co.electriccoin.zcash.ui.screen.contact.model.ContactState
|
||||
|
||||
@Composable
|
||||
fun ContactView(
|
||||
state: ContactState,
|
||||
topAppBarSubTitleState: TopAppBarSubTitleState
|
||||
) {
|
||||
BlankBgScaffold(
|
||||
topBar = {
|
||||
ContactTopAppBar(onBack = state.onBack, subTitleState = topAppBarSubTitleState, state = state)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (state.isLoading) {
|
||||
CircularScreenProgressIndicator()
|
||||
} else {
|
||||
ContactViewInternal(
|
||||
state = state,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding() + 24.dp,
|
||||
bottom = paddingValues.calculateBottomPadding() + 24.dp,
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactViewInternal(
|
||||
state: ContactState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.contact_address_label),
|
||||
style = ZashiTypography.textSm,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = ZashiColors.Inputs.Filled.label
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
ZashiTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.walletAddress,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.contact_address_hint),
|
||||
style = ZashiTypography.textMd,
|
||||
color = ZashiColors.Inputs.Default.text
|
||||
)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.contact_name_label),
|
||||
style = ZashiTypography.textSm,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = ZashiColors.Inputs.Filled.label
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
ZashiTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.contactName,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.contact_name_hint),
|
||||
style = ZashiTypography.textMd,
|
||||
color = ZashiColors.Inputs.Default.text
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
ZashiButton(
|
||||
state = state.positiveButton,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
state.negativeButton?.let {
|
||||
ZashiButton(
|
||||
state = it,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ZashiButtonDefaults.destructive1Colors()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactTopAppBar(
|
||||
onBack: () -> Unit,
|
||||
subTitleState: TopAppBarSubTitleState,
|
||||
state: ContactState
|
||||
) {
|
||||
ZashiSmallTopAppBar(
|
||||
title = state.title.getValue(),
|
||||
subtitle =
|
||||
when (subTitleState) {
|
||||
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
|
||||
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
|
||||
TopAppBarSubTitleState.None -> null
|
||||
},
|
||||
modifier = Modifier.testTag(ContactTag.TOP_APP_BAR),
|
||||
showTitleLogo = true,
|
||||
navigationAction = {
|
||||
ZashiTopAppBarBackNavigation(onBack = onBack)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun DataPreview() {
|
||||
ZcashTheme {
|
||||
ContactView(
|
||||
state =
|
||||
ContactState(
|
||||
isLoading = false,
|
||||
onBack = {},
|
||||
title = stringRes("Title"),
|
||||
walletAddress = TextFieldState(stringRes("Address")) {},
|
||||
contactName = TextFieldState(stringRes("Name")) {},
|
||||
positiveButton =
|
||||
ButtonState(
|
||||
text = stringRes("Positive"),
|
||||
),
|
||||
negativeButton =
|
||||
ButtonState(
|
||||
text = stringRes("Negative"),
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun LoadingPreview() {
|
||||
ZcashTheme {
|
||||
ContactView(
|
||||
state =
|
||||
ContactState(
|
||||
isLoading = true,
|
||||
onBack = {},
|
||||
title = stringRes("Title"),
|
||||
walletAddress = TextFieldState(stringRes("Address")) {},
|
||||
contactName = TextFieldState(stringRes("Name")) {},
|
||||
positiveButton =
|
||||
ButtonState(
|
||||
text = stringRes("Add New Contact"),
|
||||
),
|
||||
negativeButton =
|
||||
ButtonState(
|
||||
text = stringRes("Add New Contact"),
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package co.electriccoin.zcash.ui.screen.contact.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.TextFieldState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.contact.model.ContactState
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddContactViewModel(
|
||||
private val validateContactAddress: ValidateContactAddressUseCase,
|
||||
private val validateContactName: ValidateContactNameUseCase,
|
||||
private val saveContact: SaveContactUseCase
|
||||
) : ViewModel() {
|
||||
private val contactAddress = MutableStateFlow("")
|
||||
private val contactName = MutableStateFlow("")
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val contactAddressState =
|
||||
contactAddress.mapLatest { address ->
|
||||
TextFieldState(
|
||||
value = stringRes(address),
|
||||
error =
|
||||
if (address.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
when (validateContactAddress(address)) {
|
||||
ValidateContactAddressUseCase.Result.Invalid -> stringRes("")
|
||||
ValidateContactAddressUseCase.Result.NotUnique ->
|
||||
stringRes(R.string.contact_address_error_not_unique)
|
||||
|
||||
ValidateContactAddressUseCase.Result.Valid -> null
|
||||
}
|
||||
},
|
||||
onValueChange = { newValue ->
|
||||
contactAddress.update { newValue }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val contactNameState =
|
||||
contactName.mapLatest { name ->
|
||||
TextFieldState(
|
||||
value = stringRes(name),
|
||||
error =
|
||||
if (name.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
when (validateContactName(name)) {
|
||||
ValidateContactNameUseCase.Result.TooLong ->
|
||||
stringRes(R.string.contact_name_error_too_long)
|
||||
ValidateContactNameUseCase.Result.NotUnique ->
|
||||
stringRes(R.string.contact_name_error_not_unique)
|
||||
ValidateContactNameUseCase.Result.Valid ->
|
||||
null
|
||||
}
|
||||
},
|
||||
onValueChange = { newValue ->
|
||||
contactName.update { newValue }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val isSavingContact = MutableStateFlow(false)
|
||||
|
||||
private val saveButtonState =
|
||||
combine(contactAddressState, contactNameState, isSavingContact) { address, name, isSavingContact ->
|
||||
ButtonState(
|
||||
text = stringRes(R.string.add_new_contact_primary_btn),
|
||||
isEnabled =
|
||||
address.error == null &&
|
||||
name.error == null &&
|
||||
contactAddress.value.isNotEmpty() &&
|
||||
contactName.value.isNotEmpty(),
|
||||
onClick = ::onSaveButtonClick,
|
||||
isLoading = isSavingContact
|
||||
)
|
||||
}
|
||||
|
||||
val state =
|
||||
combine(contactAddressState, contactNameState, saveButtonState) { address, name, saveButton ->
|
||||
ContactState(
|
||||
title = stringRes(R.string.new_contact_title),
|
||||
isLoading = false,
|
||||
walletAddress = address,
|
||||
contactName = name,
|
||||
negativeButton = null,
|
||||
positiveButton = saveButton,
|
||||
onBack = ::onBack,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
val navigationCommand = MutableSharedFlow<String>()
|
||||
|
||||
val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||
|
||||
private fun onBack() =
|
||||
viewModelScope.launch {
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onSaveButtonClick() =
|
||||
viewModelScope.launch {
|
||||
isSavingContact.update { true }
|
||||
saveContact(name = contactName.value, address = contactAddress.value)
|
||||
backNavigationCommand.emit(Unit)
|
||||
isSavingContact.update { false }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package co.electriccoin.zcash.ui.screen.contact.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.TextFieldState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.contact.model.ContactState
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UpdateContactViewModel(
|
||||
private val contactId: String,
|
||||
private val validateContactAddress: ValidateContactAddressUseCase,
|
||||
private val validateContactName: ValidateContactNameUseCase,
|
||||
private val updateContact: UpdateContactUseCase,
|
||||
private val deleteContact: DeleteContactUseCase,
|
||||
private val getContact: GetContactUseCase
|
||||
) : ViewModel() {
|
||||
private var contact: AddressBookContact? = null
|
||||
private val contactAddress = MutableStateFlow("")
|
||||
private val contactName = MutableStateFlow("")
|
||||
|
||||
private val isUpdatingContact = MutableStateFlow(false)
|
||||
private val isDeletingContact = MutableStateFlow(false)
|
||||
private val isLoadingContact = MutableStateFlow(true)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val contactAddressState =
|
||||
contactAddress.mapLatest { address ->
|
||||
TextFieldState(
|
||||
value = stringRes(address),
|
||||
error =
|
||||
if (address.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
when (validateContactAddress(address = address, exclude = contact)) {
|
||||
ValidateContactAddressUseCase.Result.Invalid -> stringRes("")
|
||||
ValidateContactAddressUseCase.Result.NotUnique ->
|
||||
stringRes(R.string.contact_address_error_not_unique)
|
||||
|
||||
ValidateContactAddressUseCase.Result.Valid -> null
|
||||
}
|
||||
},
|
||||
onValueChange = { newValue ->
|
||||
contactAddress.update { newValue }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val contactNameState =
|
||||
contactName.mapLatest { name ->
|
||||
TextFieldState(
|
||||
value = stringRes(name),
|
||||
error =
|
||||
if (name.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
when (validateContactName(name = name, exclude = contact)) {
|
||||
ValidateContactNameUseCase.Result.TooLong ->
|
||||
stringRes(R.string.contact_name_error_too_long)
|
||||
ValidateContactNameUseCase.Result.NotUnique ->
|
||||
stringRes(R.string.contact_name_error_not_unique)
|
||||
ValidateContactNameUseCase.Result.Valid -> null
|
||||
}
|
||||
},
|
||||
onValueChange = { newValue ->
|
||||
contactName.update { newValue }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val updateButtonState =
|
||||
combine(contactAddressState, contactNameState, isUpdatingContact) { address, name, isUpdatingContact ->
|
||||
ButtonState(
|
||||
text = stringRes(R.string.update_contact_primary_btn),
|
||||
isEnabled =
|
||||
address.error == null &&
|
||||
name.error == null &&
|
||||
contactAddress.value.isNotEmpty() &&
|
||||
contactName.value.isNotEmpty() &&
|
||||
(contactName.value.trim() != contact?.name || contactAddress.value.trim() != contact?.address),
|
||||
onClick = ::onUpdateButtonClick,
|
||||
isLoading = isUpdatingContact
|
||||
)
|
||||
}
|
||||
|
||||
private val deleteButtonState =
|
||||
isDeletingContact.map { isDeletingContact ->
|
||||
ButtonState(
|
||||
text = stringRes(R.string.update_contact_secondary_btn),
|
||||
onClick = ::onDeleteButtonClick,
|
||||
isLoading = isDeletingContact
|
||||
)
|
||||
}
|
||||
|
||||
val state =
|
||||
combine(
|
||||
contactAddressState,
|
||||
contactNameState,
|
||||
updateButtonState,
|
||||
deleteButtonState,
|
||||
isLoadingContact
|
||||
) { address, name, saveButton, deleteButton, isLoadingContact ->
|
||||
ContactState(
|
||||
title = stringRes(R.string.update_contact_title),
|
||||
isLoading = isLoadingContact,
|
||||
walletAddress = address,
|
||||
contactName = name,
|
||||
negativeButton = deleteButton,
|
||||
positiveButton = saveButton,
|
||||
onBack = ::onBack,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
val navigationCommand = MutableSharedFlow<String>()
|
||||
|
||||
val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
getContact(contactId).let { contact ->
|
||||
contactAddress.update { contact?.address.orEmpty() }
|
||||
contactName.update { contact?.name.orEmpty() }
|
||||
this@UpdateContactViewModel.contact = contact
|
||||
}
|
||||
isLoadingContact.update { false }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBack() =
|
||||
viewModelScope.launch {
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onUpdateButtonClick() =
|
||||
viewModelScope.launch {
|
||||
contact?.let {
|
||||
isUpdatingContact.update { true }
|
||||
updateContact(contact = it, name = contactName.value, address = contactAddress.value)
|
||||
backNavigationCommand.emit(Unit)
|
||||
isUpdatingContact.update { false }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDeleteButtonClick() =
|
||||
viewModelScope.launch {
|
||||
contact?.let {
|
||||
isDeletingContact.update { true }
|
||||
deleteContact(it)
|
||||
backNavigationCommand.emit(Unit)
|
||||
isDeletingContact.update { false }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,5 @@ package co.electriccoin.zcash.ui.screen.send
|
|||
*/
|
||||
object SendTag {
|
||||
const val SEND_FORM_BUTTON = "send_form_button"
|
||||
const val SEND_FAILED_BUTTON = "send_failed_button"
|
||||
const val SEND_SUCCESS_BUTTON = "send_success_button"
|
||||
const val SEND_AMOUNT_FIELD = "SEND_AMOUNT_FIELD"
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ package co.electriccoin.zcash.ui.screen.send.view
|
|||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
@ -12,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
|
@ -21,15 +22,16 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
@ -39,21 +41,20 @@ import androidx.compose.ui.platform.testTag
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.ZecSendExt
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.sdk.extension.DEFAULT_FEE
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
|
@ -70,16 +71,14 @@ import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
|||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
import co.electriccoin.zcash.ui.design.component.BubbleArrowAlignment
|
||||
import co.electriccoin.zcash.ui.design.component.BubbleMessage
|
||||
import co.electriccoin.zcash.ui.design.component.FormTextField
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
import co.electriccoin.zcash.ui.design.component.Small
|
||||
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.TopAppBarHideBalancesNavigation
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTextField
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults
|
||||
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.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
||||
|
@ -87,6 +86,7 @@ import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
|||
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
|
@ -161,7 +161,6 @@ private fun SendFormTransparentAddressPreview() {
|
|||
|
||||
// TODO [#1260]: Cover Send screens UI with tests
|
||||
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun Send(
|
||||
|
@ -322,7 +321,7 @@ private fun SendMainContent(
|
|||
// TODO [#1257]: Send.Form TextFields not persisted on a configuration change when the underlying ViewPager is on the
|
||||
// Balances page
|
||||
// TODO [#1257]: https://github.com/Electric-Coin-Company/zashi-android/issues/1257
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
private fun SendForm(
|
||||
balanceState: BalanceState,
|
||||
|
@ -360,7 +359,7 @@ private fun SendForm(
|
|||
onReferenceClick = goBalances
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// TODO [#1256]: Consider Send.Form TextFields scrolling
|
||||
// TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256
|
||||
|
@ -416,7 +415,7 @@ private fun SendForm(
|
|||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(54.dp))
|
||||
|
||||
SendButton(
|
||||
amountState = amountState,
|
||||
|
@ -425,11 +424,13 @@ private fun SendForm(
|
|||
recipientAddressState = recipientAddressState,
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(78.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun SendButton(
|
||||
amountState: AmountState,
|
||||
memoState: MemoState,
|
||||
|
@ -437,6 +438,7 @@ fun SendButton(
|
|||
recipientAddressState: RecipientAddressState,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
// Common conditions continuously checked for validity
|
||||
|
@ -449,12 +451,9 @@ fun SendButton(
|
|||
// A valid memo is necessary only for non-transparent recipient
|
||||
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
PrimaryButton(
|
||||
ZashiButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
// SDK side validations
|
||||
val zecSendValidation =
|
||||
ZecSendExt.new(
|
||||
|
@ -471,13 +470,35 @@ fun SendButton(
|
|||
)
|
||||
|
||||
when (zecSendValidation) {
|
||||
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
|
||||
is ZecSendExt.ZecSendValidation.Valid ->
|
||||
onCreateZecSend(
|
||||
zecSendValidation.zecSend.copy(
|
||||
destination =
|
||||
when (recipientAddressState.type) {
|
||||
is AddressType.Invalid ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Shielded ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Tex ->
|
||||
WalletAddress.Tex.new(recipientAddressState.address)
|
||||
AddressType.Transparent ->
|
||||
WalletAddress.Transparent.new(recipientAddressState.address)
|
||||
AddressType.Unified ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
null -> WalletAddress.Unified.new(recipientAddressState.address)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
is ZecSendExt.ZecSendValidation.Invalid -> {
|
||||
// We do not expect this validation to fail, so logging is enough here
|
||||
// An error popup could be reasonable here as well
|
||||
Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
text = stringResource(id = R.string.send_create),
|
||||
enabled = sendButtonEnabled,
|
||||
|
@ -486,22 +507,10 @@ fun SendButton(
|
|||
.testTag(SendTag.SEND_FORM_BUTTON)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
BodySmall(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.send_fee,
|
||||
DEFAULT_FEE
|
||||
),
|
||||
textFontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SendFormAddressTextField(
|
||||
hasCameraFeature: Boolean,
|
||||
|
@ -510,8 +519,8 @@ fun SendFormAddressTextField(
|
|||
setRecipientAddress: (String) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
|
@ -521,7 +530,11 @@ fun SendFormAddressTextField(
|
|||
// Scroll TextField above ime keyboard
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
) {
|
||||
Small(text = stringResource(id = R.string.send_address_label))
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_address_label),
|
||||
color = ZashiColors.Inputs.Default.label,
|
||||
style = ZashiTypography.textMd
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
|
@ -536,34 +549,42 @@ fun SendFormAddressTextField(
|
|||
null
|
||||
}
|
||||
|
||||
FormTextField(
|
||||
ZashiTextField(
|
||||
value = recipientAddressValue,
|
||||
onValueChange = {
|
||||
setRecipientAddress(it)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
},
|
||||
error = recipientAddressError,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_address_hint),
|
||||
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||
color = ZcashTheme.colors.textFieldHint
|
||||
style = ZashiTypography.textMd,
|
||||
color = ZashiColors.Inputs.Default.text
|
||||
)
|
||||
},
|
||||
trailingIcon =
|
||||
if (hasCameraFeature) {
|
||||
{
|
||||
IconButton(
|
||||
Image(
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
onClick = onQrScannerOpen,
|
||||
content = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.qr_code_icon),
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(radius = 4.dp),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
painter = painterResource(R.drawable.qr_code_icon),
|
||||
contentDescription = stringResource(R.string.send_scan_content_description),
|
||||
tint = ZcashTheme.colors.secondaryColor,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
@ -577,16 +598,15 @@ fun SendFormAddressTextField(
|
|||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Down)
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
}
|
||||
),
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SendFormAmountTextField(
|
||||
amountState: AmountState,
|
||||
|
@ -632,13 +652,16 @@ fun SendFormAmountTextField(
|
|||
// Scroll TextField above ime keyboard
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
) {
|
||||
Small(text = stringResource(id = R.string.send_amount_label))
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_amount_label),
|
||||
color = ZashiColors.Inputs.Default.label,
|
||||
style = ZashiTypography.textMd
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
Row {
|
||||
FormTextField(
|
||||
textStyle = ZcashTheme.extendedTypography.textFieldValue.copy(fontSize = 14.sp),
|
||||
ZashiTextField(
|
||||
value = amountState.value,
|
||||
onValueChange = { newValue ->
|
||||
setAmountState(
|
||||
|
@ -653,6 +676,7 @@ fun SendFormAmountTextField(
|
|||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
innerModifier = Modifier.testTag(SendTag.SEND_AMOUNT_FIELD),
|
||||
error = amountError,
|
||||
placeholder = {
|
||||
Text(
|
||||
|
@ -661,8 +685,8 @@ fun SendFormAmountTextField(
|
|||
id = R.string.send_amount_hint,
|
||||
zcashCurrency
|
||||
),
|
||||
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||
color = ZcashTheme.colors.textFieldHint
|
||||
style = ZashiTypography.textMd,
|
||||
color = ZashiColors.Inputs.Default.text
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
|
@ -676,49 +700,30 @@ fun SendFormAmountTextField(
|
|||
focusManager.clearFocus(true)
|
||||
},
|
||||
onNext = {
|
||||
if (exchangeRateState is ExchangeRateState.Data) {
|
||||
focusManager.moveFocus(FocusDirection.Right)
|
||||
} else {
|
||||
focusManager.moveFocus(FocusDirection.Down)
|
||||
}
|
||||
}
|
||||
),
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
leadingIcon = {
|
||||
Image(
|
||||
modifier = Modifier.requiredSize(7.dp, 13.dp),
|
||||
painter = painterResource(R.drawable.ic_send_zashi),
|
||||
contentDescription = "",
|
||||
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
|
||||
colorFilter = ColorFilter.tint(color = ZashiColors.Inputs.Default.text),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (exchangeRateState is ExchangeRateState.Data) {
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingMin))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Image(
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
painter = painterResource(id = R.drawable.ic_send_convert),
|
||||
contentDescription = "",
|
||||
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingMin))
|
||||
FormTextField(
|
||||
enabled = !exchangeRateState.isStale,
|
||||
textStyle = ZcashTheme.extendedTypography.textFieldValue.copy(fontSize = 14.sp),
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
ZashiTextField(
|
||||
isEnabled = !exchangeRateState.isStale,
|
||||
value = amountState.fiatValue,
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
cursorColor = ZcashTheme.colors.textPrimary,
|
||||
disabledTextColor = ZcashTheme.colors.textDisabled,
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
errorContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent
|
||||
),
|
||||
onValueChange = { newValue ->
|
||||
setAmountState(
|
||||
AmountState.newFromFiat(
|
||||
|
@ -738,8 +743,8 @@ fun SendFormAmountTextField(
|
|||
stringResource(
|
||||
id = R.string.send_usd_amount_hint
|
||||
),
|
||||
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||
color = ZcashTheme.colors.textFieldHint
|
||||
style = ZashiTypography.textMd,
|
||||
color = ZashiColors.Inputs.Default.text
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
|
@ -756,17 +761,15 @@ fun SendFormAmountTextField(
|
|||
focusManager.moveFocus(FocusDirection.Down)
|
||||
}
|
||||
),
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
leadingIcon = {
|
||||
Image(
|
||||
modifier = Modifier.requiredSize(7.dp, 13.dp),
|
||||
painter = painterResource(R.drawable.ic_usd),
|
||||
painter = painterResource(R.drawable.ic_send_usd),
|
||||
contentDescription = "",
|
||||
colorFilter =
|
||||
if (!exchangeRateState.isStale) {
|
||||
ColorFilter.tint(color = ZcashTheme.colors.secondaryColor)
|
||||
ColorFilter.tint(color = ZashiColors.Inputs.Default.text)
|
||||
} else {
|
||||
ColorFilter.tint(color = ZcashTheme.colors.textDisabled)
|
||||
ColorFilter.tint(color = ZashiColors.Inputs.Disabled.text)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -776,8 +779,8 @@ fun SendFormAmountTextField(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
@Composable
|
||||
fun SendFormMemoTextField(
|
||||
isMemoFieldAvailable: Boolean,
|
||||
|
@ -794,54 +797,27 @@ fun SendFormMemoTextField(
|
|||
// Scroll TextField above ime keyboard
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.send_paper_plane),
|
||||
contentDescription = null,
|
||||
tint =
|
||||
if (isMemoFieldAvailable) {
|
||||
ZcashTheme.colors.textPrimary
|
||||
} else {
|
||||
ZcashTheme.colors.textDisabled
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
Small(
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_memo_label),
|
||||
color =
|
||||
if (isMemoFieldAvailable) {
|
||||
ZcashTheme.colors.textPrimary
|
||||
} else {
|
||||
ZcashTheme.colors.textDisabled
|
||||
}
|
||||
color = ZashiColors.Inputs.Default.label,
|
||||
style = ZashiTypography.textMd
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
BubbleMessage(
|
||||
arrowAlignment = BubbleArrowAlignment.BottomLeft,
|
||||
backgroundColor =
|
||||
if (isMemoFieldAvailable) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
ZcashTheme.colors.textDisabled
|
||||
}
|
||||
) {
|
||||
FormTextField(
|
||||
enabled = isMemoFieldAvailable,
|
||||
ZashiTextField(
|
||||
minLines = if (isMemoFieldAvailable) 2 else 1,
|
||||
isEnabled = isMemoFieldAvailable,
|
||||
value =
|
||||
if (isMemoFieldAvailable) {
|
||||
memoState.text
|
||||
} else {
|
||||
""
|
||||
},
|
||||
error = if (memoState is MemoState.Correct) null else "",
|
||||
onValueChange = {
|
||||
setMemoState(MemoState.new(it))
|
||||
},
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
|
@ -849,34 +825,63 @@ fun SendFormMemoTextField(
|
|||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
placeholder = {
|
||||
if (isMemoFieldAvailable) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_memo_hint),
|
||||
style = ZcashTheme.extendedTypography.textFieldHint,
|
||||
color = ZcashTheme.colors.textFieldHint
|
||||
style = ZashiTypography.textMd,
|
||||
color = ZashiColors.Inputs.Default.text
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.send_transparent_memo),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Utility.Gray.utilityGray700
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingIcon =
|
||||
if (isMemoFieldAvailable) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_confirmation_message_info),
|
||||
contentDescription = "",
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Utility.Gray.utilityGray500)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors =
|
||||
if (isMemoFieldAvailable) {
|
||||
ZashiTextFieldDefaults.defaultColors()
|
||||
} else {
|
||||
ZashiTextFieldDefaults.defaultColors(
|
||||
disabledTextColor = ZashiColors.Inputs.Disabled.text,
|
||||
disabledHintColor = ZashiColors.Inputs.Disabled.hint,
|
||||
disabledBorderColor = Color.Unspecified,
|
||||
disabledContainerColor = ZashiColors.Inputs.Disabled.bg,
|
||||
disabledPlaceholderColor = ZashiColors.Inputs.Disabled.text,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minHeight = ZcashTheme.dimens.textFieldMemoPanelDefaultHeight,
|
||||
withBorder = false
|
||||
)
|
||||
}
|
||||
|
||||
if (isMemoFieldAvailable) {
|
||||
Body(
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.send_memo_bytes_counter,
|
||||
Memo.MAX_MEMO_LENGTH_BYTES - memoState.byteSize,
|
||||
Memo.MAX_MEMO_LENGTH_BYTES
|
||||
),
|
||||
textFontWeight = FontWeight.Bold,
|
||||
color =
|
||||
if (memoState is MemoState.Correct) {
|
||||
ZcashTheme.colors.textFieldHint
|
||||
ZashiColors.Inputs.Default.hint
|
||||
} else {
|
||||
ZcashTheme.colors.textFieldWarning
|
||||
ZashiColors.Inputs.Filled.required
|
||||
},
|
||||
textAlign = TextAlign.End,
|
||||
style = ZashiTypography.textSm,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
@ -26,14 +26,18 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.sdk.extension.toZecStringFull
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
|
@ -48,17 +52,24 @@ import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
|||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.BubbleArrowAlignment
|
||||
import co.electriccoin.zcash.ui.design.component.BubbleMessage
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
import co.electriccoin.zcash.ui.design.component.SecondaryButton
|
||||
import co.electriccoin.zcash.ui.design.component.Small
|
||||
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.Tiny
|
||||
import co.electriccoin.zcash.ui.design.component.TextFieldState
|
||||
import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTextField
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZecAmountTriple
|
||||
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.stringRes
|
||||
import co.electriccoin.zcash.ui.fixture.ObserveFiatCurrencyResultFixture
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeLabel
|
||||
import co.electriccoin.zcash.ui.screen.sendconfirmation.SendConfirmationTag
|
||||
|
@ -300,18 +311,21 @@ private fun SendConfirmationTopAppBar(
|
|||
SendConfirmationStage.Confirmation,
|
||||
SendConfirmationStage.Sending,
|
||||
is SendConfirmationStage.Failure,
|
||||
is SendConfirmationStage.FailureGrpc, -> {
|
||||
SmallTopAppBar(
|
||||
subTitle = subTitle,
|
||||
titleText = stringResource(id = R.string.send_stage_confirmation_title),
|
||||
is SendConfirmationStage.FailureGrpc,
|
||||
-> {
|
||||
ZashiSmallTopAppBar(
|
||||
title = stringResource(id = R.string.send_stage_confirmation_title),
|
||||
subtitle = subTitle,
|
||||
)
|
||||
}
|
||||
|
||||
SendConfirmationStage.MultipleTrxFailure -> {
|
||||
SmallTopAppBar(
|
||||
subTitle = subTitle,
|
||||
titleText = stringResource(id = R.string.send_confirmation_multiple_error_title),
|
||||
)
|
||||
}
|
||||
|
||||
SendConfirmationStage.MultipleTrxFailureReported -> {
|
||||
SmallTopAppBar(
|
||||
subTitle = subTitle,
|
||||
|
@ -366,6 +380,7 @@ private fun SendConfirmationMainContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
is SendConfirmationStage.MultipleTrxFailure, SendConfirmationStage.MultipleTrxFailureReported -> {
|
||||
MultipleSubmissionFailure(
|
||||
onContactSupport = {
|
||||
|
@ -397,7 +412,11 @@ private fun SendConfirmationContent(
|
|||
) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
Small(stringResource(R.string.send_confirmation_amount))
|
||||
Text(
|
||||
stringResource(R.string.send_confirmation_amount),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
|
||||
BalanceWidgetBigLineOnly(
|
||||
parts = zecSend.amount.toZecStringFull().asZecAmountTriple(),
|
||||
|
@ -409,27 +428,41 @@ private fun SendConfirmationContent(
|
|||
zatoshi = zecSend.amount,
|
||||
state = exchangeRate,
|
||||
isHideBalances = false,
|
||||
style = ZashiTypography.textMd.copy(fontWeight = FontWeight.SemiBold),
|
||||
textColor = ZashiColors.Text.textPrimary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Small(stringResource(R.string.send_confirmation_address))
|
||||
Text(
|
||||
stringResource(R.string.send_confirmation_address),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Tiny(zecSend.destination.address)
|
||||
Text(
|
||||
zecSend.destination.address,
|
||||
style = ZashiTypography.textXs,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Small(stringResource(R.string.send_confirmation_fee))
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
Row {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.send_confirmation_amount_item),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary
|
||||
)
|
||||
|
||||
StyledBalance(
|
||||
// The not-null assertion operator is necessary here even if we check its nullability before
|
||||
// due to: "Smart cast to 'Proposal' is impossible, because 'zecSend.proposal' is a public API
|
||||
// property declared in different module. See more details on the Kotlin forum.
|
||||
balanceParts = zecSend.proposal!!.totalFeeRequired().toZecStringFull().asZecAmountTriple(),
|
||||
balanceParts = zecSend.amount.toZecStringFull().asZecAmountTriple(),
|
||||
// We don't hide any balance in confirmation screen
|
||||
isHideBalances = false,
|
||||
textStyle =
|
||||
|
@ -438,28 +471,89 @@ private fun SendConfirmationContent(
|
|||
leastSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
if (zecSend.memo.value.isNotEmpty()) {
|
||||
Small(stringResource(R.string.send_confirmation_memo))
|
||||
Row {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.send_confirmation_fee),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
|
||||
|
||||
BubbleMessage(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
arrowAlignment = BubbleArrowAlignment.BottomLeft,
|
||||
backgroundColor = Color.Transparent
|
||||
) {
|
||||
Tiny(
|
||||
text = zecSend.memo.value,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = ZcashTheme.dimens.spacingMid)
|
||||
StyledBalance(
|
||||
// The not-null assertion operator is necessary here even if we check its nullability before
|
||||
// due to: "Smart cast to 'Proposal' is impossible, because 'zecSend.proposal' is a public API
|
||||
// property declared in different module. See more details on the Kotlin forum.
|
||||
balanceParts =
|
||||
zecSend.proposal?.totalFeeRequired()?.toZecStringFull()?.asZecAmountTriple()
|
||||
?: ZecAmountTriple("main", "prefix"),
|
||||
// We don't hide any balance in confirmation screen
|
||||
isHideBalances = false,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
val isMemoFieldAvailable =
|
||||
zecSend.destination !is WalletAddress.Transparent &&
|
||||
zecSend.destination !is WalletAddress.Tex
|
||||
|
||||
if (zecSend.memo.value.isNotEmpty() || !isMemoFieldAvailable) {
|
||||
Text(
|
||||
stringResource(R.string.send_confirmation_memo),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textTertiary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
ZashiTextField(
|
||||
state = TextFieldState(value = stringRes(zecSend.memo.value), isEnabled = false) {},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
colors =
|
||||
ZashiTextFieldDefaults.defaultColors(
|
||||
disabledTextColor = ZashiColors.Inputs.Filled.text,
|
||||
disabledHintColor = ZashiColors.Inputs.Disabled.hint,
|
||||
disabledBorderColor = Color.Unspecified,
|
||||
disabledContainerColor = ZashiColors.Inputs.Disabled.bg,
|
||||
disabledPlaceholderColor = ZashiColors.Inputs.Disabled.text,
|
||||
),
|
||||
placeholder =
|
||||
if (isMemoFieldAvailable) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
Text(
|
||||
text = stringResource(R.string.send_transparent_memo),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Utility.Gray.utilityGray700
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingIcon =
|
||||
if (isMemoFieldAvailable) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_confirmation_message_info),
|
||||
contentDescription = "",
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Utility.Gray.utilityGray500)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
|
||||
}
|
||||
|
||||
|
@ -476,12 +570,10 @@ private fun SendConfirmationContent(
|
|||
onConfirmation = onConfirmation
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||
Spacer(modifier = Modifier.height(52.dp))
|
||||
}
|
||||
}
|
||||
|
||||
const val BUTTON_WIDTH_RATIO = 0.5f
|
||||
|
||||
@Composable
|
||||
fun SendConfirmationActionButtons(
|
||||
onConfirmation: () -> Unit,
|
||||
|
@ -489,33 +581,37 @@ fun SendConfirmationActionButtons(
|
|||
isSending: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
PrimaryButton(
|
||||
text = stringResource(id = R.string.send_confirmation_send_button),
|
||||
ZashiButton(
|
||||
state =
|
||||
ButtonState(
|
||||
text = stringRes(R.string.send_confirmation_send_button),
|
||||
onClick = onConfirmation,
|
||||
enabled = !isSending,
|
||||
showProgressBar = isSending,
|
||||
minHeight = ZcashTheme.dimens.buttonHeightSmall,
|
||||
buttonColors = ZcashTheme.colors.tertiaryButtonColors,
|
||||
isEnabled = !isSending,
|
||||
isLoading = isSending,
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(SendConfirmationTag.SEND_CONFIRMATION_SEND_BUTTON)
|
||||
.weight(BUTTON_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingLarge))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.send_confirmation_back_button),
|
||||
ZashiButton(
|
||||
state =
|
||||
ButtonState(
|
||||
text = stringRes(R.string.send_confirmation_back_button),
|
||||
onClick = onBack,
|
||||
enabled = !isSending,
|
||||
minHeight = ZcashTheme.dimens.buttonHeightSmall,
|
||||
isEnabled = !isSending,
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.testTag(SendConfirmationTag.SEND_CONFIRMATION_BACK_BUTTON)
|
||||
.weight(BUTTON_WIDTH_RATIO)
|
||||
.fillMaxWidth()
|
||||
.testTag(SendConfirmationTag.SEND_CONFIRMATION_BACK_BUTTON),
|
||||
colors = ZashiButtonDefaults.tertiaryColors()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ data class SettingsState(
|
|||
val isLoading: Boolean,
|
||||
val version: StringResource,
|
||||
val settingsTroubleshootingState: SettingsTroubleshootingState?,
|
||||
val onAddressBookClick: () -> Unit,
|
||||
val onBack: () -> Unit,
|
||||
val onAdvancedSettingsClick: () -> Unit,
|
||||
val onAboutUsClick: () -> Unit,
|
||||
|
|
|
@ -76,6 +76,12 @@ fun Settings(
|
|||
end = 4.dp
|
||||
),
|
||||
) {
|
||||
ZashiSettingsListItem(
|
||||
text = stringResource(id = R.string.settings_address_book),
|
||||
icon = R.drawable.ic_settings_address_book,
|
||||
onClick = state.onAddressBookClick
|
||||
)
|
||||
ZashiHorizontalDivider()
|
||||
ZashiSettingsListItem(
|
||||
text = stringResource(id = R.string.settings_advanced_settings),
|
||||
icon = R.drawable.ic_advanced_settings orDark R.drawable.ic_advanced_settings_dark,
|
||||
|
@ -230,6 +236,7 @@ private fun PreviewSettings() {
|
|||
onAdvancedSettingsClick = {},
|
||||
onAboutUsClick = {},
|
||||
onSendUsFeedbackClick = {},
|
||||
onAddressBookClick = {}
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
@ -251,6 +258,7 @@ private fun PreviewSettingsLoading() {
|
|||
onAdvancedSettingsClick = {},
|
||||
onAboutUsClick = {},
|
||||
onSendUsFeedbackClick = {},
|
||||
onAddressBookClick = {}
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
|||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADDRESS_BOOK
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
||||
import co.electriccoin.zcash.ui.R
|
||||
|
@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.flow
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class SettingsViewModel(
|
||||
observeConfiguration: ObserveConfigurationUseCase,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
|
@ -100,6 +102,7 @@ class SettingsViewModel(
|
|||
onAdvancedSettingsClick = ::onAdvancedSettingsClick,
|
||||
onAboutUsClick = ::onAboutUsClick,
|
||||
onSendUsFeedbackClick = ::onSendUsFeedbackClick,
|
||||
onAddressBookClick = ::onAddressBookClick
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
|
||||
|
||||
|
@ -152,6 +155,12 @@ class SettingsViewModel(
|
|||
navigationCommand.emit(SUPPORT)
|
||||
}
|
||||
|
||||
private fun onAddressBookClick() {
|
||||
viewModelScope.launch {
|
||||
navigationCommand.emit(ADDRESS_BOOK)
|
||||
}
|
||||
}
|
||||
|
||||
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
|
||||
flow<Boolean?> {
|
||||
emitAll(default.observe(standardPreferenceProvider()))
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="11dp"
|
||||
android:height="10dp"
|
||||
android:viewportWidth="11"
|
||||
android:viewportHeight="10">
|
||||
android:width="17dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="17"
|
||||
android:viewportHeight="16">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h10.25v9.75h-10.25z"/>
|
||||
android:pathData="M0.249,0h16v16h-16z"/>
|
||||
<path
|
||||
android:pathData="M0,9.75V1.72H8.46V9.75H0ZM7.58,8.9V2.57H0.88V8.91H7.58V8.9Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M9.81,7.2C9.57,7.2 9.37,7.01 9.37,6.78V0.85H3.1C2.86,0.85 2.66,0.66 2.66,0.43C2.66,0.2 2.86,0.01 3.1,0.01H10.25V6.79C10.25,7.02 10.05,7.21 9.81,7.21V7.2Z"
|
||||
android:fillColor="#000000"/>
|
||||
android:pathData="M3.583,10C2.961,10 2.651,10 2.406,9.899C2.079,9.763 1.82,9.504 1.684,9.177C1.583,8.932 1.583,8.621 1.583,8V3.467C1.583,2.72 1.583,2.347 1.728,2.061C1.856,1.81 2.06,1.606 2.311,1.479C2.596,1.333 2.969,1.333 3.716,1.333H8.249C8.871,1.333 9.181,1.333 9.426,1.435C9.753,1.57 10.013,1.83 10.148,2.156C10.249,2.401 10.249,2.712 10.249,3.333M8.383,14.667H12.783C13.529,14.667 13.903,14.667 14.188,14.521C14.439,14.394 14.643,14.189 14.771,13.939C14.916,13.653 14.916,13.28 14.916,12.533V8.133C14.916,7.387 14.916,7.013 14.771,6.728C14.643,6.477 14.439,6.273 14.188,6.145C13.903,6 13.529,6 12.783,6H8.383C7.636,6 7.263,6 6.977,6.145C6.727,6.273 6.523,6.477 6.395,6.728C6.249,7.013 6.249,7.387 6.249,8.133V12.533C6.249,13.28 6.249,13.653 6.395,13.939C6.523,14.189 6.727,14.394 6.977,14.521C7.263,14.667 7.636,14.667 8.383,14.667Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.33"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#4D4941"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
|
@ -16,7 +16,6 @@ n <plurals name="account_history_item_message">
|
|||
<item quantity="one">Message</item>
|
||||
<item quantity="other">Messages</item>
|
||||
</plurals>
|
||||
<string name="account_history_item_no_message">No message included in transaction</string>
|
||||
<string name="account_history_item_collapse_transaction">Collapse transaction</string>
|
||||
<string name="account_history_item_transaction_id">Transaction ID</string>
|
||||
<string name="account_history_item_transaction_fee">Transaction Fee</string>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="add_new_contact_title">Add New Contact</string>
|
||||
<string name="add_new_contact_primary_btn">Save</string>
|
||||
</resources>
|
|
@ -0,0 +1,32 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88dp"
|
||||
android:height="88dp"
|
||||
android:viewportWidth="88"
|
||||
android:viewportHeight="88">
|
||||
<path
|
||||
android:pathData="M0.5,36C0.5,16.118 16.618,0 36.5,0C56.382,0 72.5,16.118 72.5,36C72.5,55.882 56.382,72 36.5,72C16.618,72 0.5,55.882 0.5,36Z"
|
||||
android:fillColor="#EBEBE6"/>
|
||||
<path
|
||||
android:pathData="M34.499,38L48.499,24M34.669,38.438L38.174,47.448C38.482,48.242 38.637,48.639 38.859,48.755C39.052,48.855 39.282,48.855 39.474,48.755C39.697,48.639 39.852,48.243 40.161,47.449L48.949,24.932C49.228,24.216 49.368,23.858 49.291,23.629C49.225,23.43 49.069,23.274 48.87,23.208C48.641,23.132 48.283,23.271 47.567,23.551L25.05,32.338C24.257,32.648 23.86,32.803 23.744,33.025C23.644,33.218 23.644,33.448 23.745,33.64C23.861,33.863 24.257,34.017 25.051,34.326L34.062,37.83C34.223,37.893 34.304,37.924 34.371,37.972C34.432,38.015 34.484,38.068 34.527,38.128C34.576,38.196 34.607,38.276 34.669,38.438Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2.66667"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#4D4941"
|
||||
android:strokeLineCap="round"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M51,66C51,57.163 58.163,50 67,50C75.837,50 83,57.163 83,66C83,74.837 75.837,82 67,82C58.163,82 51,74.837 51,66Z"/>
|
||||
<path
|
||||
android:pathData="M67,50L67,50A16,16 0,0 1,83 66L83,66A16,16 0,0 1,67 82L67,82A16,16 0,0 1,51 66L51,66A16,16 0,0 1,67 50z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M51,66C51,57.176 58.176,50 67,50C75.824,50 83,57.176 83,66C83,74.824 75.824,82 67,82C58.176,82 51,74.824 51,66ZM72.707,58.575V61.01L65.935,70.195H72.707V73.425H68.342V76.101H65.658V73.425H61.293V70.99L68.058,61.804H61.293V58.575H65.658V55.892H68.342V58.575H72.707Z"
|
||||
android:fillColor="#FCBB1A"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M67,49C57.611,49 50,56.611 50,66C50,75.389 57.611,83 67,83C76.389,83 84,75.389 84,66C84,56.611 76.389,49 67,49Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"/>
|
||||
</vector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M10,4.167V15.833M4.167,10H15.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,18 @@
|
|||
<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="M12,1C5.925,1 1,5.925 1,12C1,18.075 5.925,23 12,23C18.075,23 23,18.075 23,12C23,5.925 18.075,1 12,1Z"
|
||||
android:fillColor="#231F20"/>
|
||||
<path
|
||||
android:pathData="M12,1C5.925,1 1,5.925 1,12C1,18.075 5.925,23 12,23C18.075,23 23,18.075 23,12C23,5.925 18.075,1 12,1Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M11.857,6.544C11.952,6.531 12.048,6.531 12.143,6.544C12.252,6.56 12.354,6.598 12.435,6.629L12.457,6.637L15.203,7.667C15.512,7.782 15.783,7.883 15.992,8.068C16.174,8.229 16.315,8.432 16.402,8.659C16.501,8.92 16.501,9.209 16.5,9.539L16.5,12C16.5,13.413 15.731,14.592 14.901,15.457C14.064,16.329 13.107,16.944 12.601,17.239L12.58,17.251C12.488,17.306 12.368,17.376 12.21,17.41C12.08,17.438 11.92,17.438 11.79,17.41C11.632,17.376 11.512,17.306 11.42,17.251L11.399,17.239C10.893,16.944 9.936,16.329 9.1,15.457C8.269,14.592 7.5,13.413 7.5,12L7.5,9.539C7.499,9.209 7.499,8.92 7.598,8.659C7.685,8.432 7.826,8.229 8.008,8.068C8.217,7.883 8.488,7.782 8.797,7.667L11.544,6.637L11.565,6.629C11.646,6.598 11.748,6.56 11.857,6.544ZM14.104,10.854C14.299,10.658 14.299,10.342 14.104,10.146C13.908,9.951 13.592,9.951 13.396,10.146L11.5,12.043L10.854,11.396C10.658,11.201 10.342,11.201 10.146,11.396C9.951,11.592 9.951,11.908 10.146,12.104L11.146,13.104C11.342,13.299 11.658,13.299 11.854,13.104L14.104,10.854Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -0,0 +1,6 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="address_book_title">Address Book</string>
|
||||
<string name="address_book_add">Add New Contact</string>
|
||||
<string name="address_book_empty">Your address book is empty</string>
|
||||
<string name="address_book_version">Version %s</string>
|
||||
</resources>
|
|
@ -0,0 +1,13 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="new_contact_title">Add New Contact</string>
|
||||
<string name="saved_contact_title">Saved Contact</string>
|
||||
|
||||
<string name="contact_name_label">Contact Name</string>
|
||||
<string name="contact_name_hint">Enter contact name…</string>
|
||||
<string name="contact_address_label">Wallet Address</string>
|
||||
<string name="contact_address_hint">Enter Wallet Address…</string>
|
||||
|
||||
<string name="contact_name_error_not_unique">This contact name is already in use. Please choose a different name.</string>
|
||||
<string name="contact_name_error_too_long">This contact name exceeds the 32-character limit. Please shorten the name.</string>
|
||||
<string name="contact_address_error_not_unique">This wallet address is already in your Address Book.</string>
|
||||
</resources>
|
|
@ -1,14 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="11dp"
|
||||
android:height="9dp"
|
||||
android:viewportWidth="11"
|
||||
android:viewportHeight="9">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0.5,0.601h10v8h-10z"/>
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3.289,8.601L0.5,6.047V5.181H10.5V6.162H2.064L3.926,7.868L3.289,8.601ZM7.71,0.601L10.499,3.155V4.053L0.499,4.022V3.04H8.935L7.073,1.334L7.71,0.601Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
android:pathData="M20,17H4M4,17L8,13M4,17L8,21M4,7H20M20,7L16,3M20,7L16,11"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#231F20"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M5,13.333C5,15.174 6.492,16.667 8.333,16.667H11.667C13.508,16.667 15,15.174 15,13.333C15,11.492 13.508,10 11.667,10H8.333C6.492,10 5,8.508 5,6.667C5,4.826 6.492,3.333 8.333,3.333H11.667C13.508,3.333 15,4.826 15,6.667M10,1.667V18.333"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#87816F"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -1,9 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="7dp"
|
||||
android:height="13dp"
|
||||
android:viewportWidth="7"
|
||||
android:viewportHeight="13">
|
||||
android:width="20dp"
|
||||
android:height="21dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="21">
|
||||
<path
|
||||
android:pathData="M7.009,3.942V2.534H4.48V0.98H2.926V2.534H0.397V4.405H4.321L0.397,9.728V11.138H2.926V12.683H4.48V11.138H7.009V9.267H3.085L7.009,3.942Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
android:pathData="M15.521,5.563V3.108H11.115V0.4H8.406V3.108H4V6.369H10.837L4,15.644V18.1H8.406V20.792H11.115V18.1H15.521V14.84H8.683L15.521,5.563Z"
|
||||
android:fillColor="#87816F"/>
|
||||
</vector>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="7dp"
|
||||
android:height="12dp"
|
||||
android:viewportWidth="7"
|
||||
android:viewportHeight="12">
|
||||
<path
|
||||
android:pathData="M3.067,2.719V0.381H3.865V2.719H3.067ZM3.067,11.343V8.921H3.865V11.343H3.067ZM3.431,9.649C2.918,9.649 2.456,9.593 2.045,9.481C1.635,9.369 1.285,9.215 0.995,9.019C0.706,8.814 0.482,8.571 0.323,8.291C0.174,8.011 0.099,7.694 0.099,7.339C0.099,7.302 0.099,7.264 0.099,7.227C0.099,7.19 0.104,7.162 0.113,7.143H1.989C1.989,7.162 1.989,7.18 1.989,7.199C1.989,7.218 1.989,7.236 1.989,7.255C1.999,7.488 2.073,7.68 2.213,7.829C2.353,7.969 2.535,8.072 2.759,8.137C2.993,8.202 3.235,8.235 3.487,8.235C3.711,8.235 3.926,8.216 4.131,8.179C4.346,8.132 4.523,8.053 4.663,7.941C4.813,7.829 4.887,7.684 4.887,7.507C4.887,7.283 4.794,7.11 4.607,6.989C4.43,6.868 4.192,6.77 3.893,6.695C3.604,6.62 3.287,6.536 2.941,6.443C2.624,6.368 2.307,6.284 1.989,6.191C1.672,6.088 1.383,5.958 1.121,5.799C0.869,5.64 0.664,5.435 0.505,5.183C0.347,4.922 0.267,4.595 0.267,4.203C0.267,3.82 0.351,3.489 0.519,3.209C0.687,2.92 0.916,2.682 1.205,2.495C1.504,2.308 1.849,2.173 2.241,2.089C2.643,1.996 3.072,1.949 3.529,1.949C3.959,1.949 4.36,1.996 4.733,2.089C5.107,2.173 5.433,2.304 5.713,2.481C5.993,2.649 6.213,2.864 6.371,3.125C6.53,3.377 6.609,3.662 6.609,3.979C6.609,4.044 6.609,4.105 6.609,4.161C6.609,4.217 6.605,4.254 6.595,4.273H4.733V4.161C4.733,3.993 4.682,3.853 4.579,3.741C4.477,3.62 4.327,3.526 4.131,3.461C3.945,3.396 3.716,3.363 3.445,3.363C3.259,3.363 3.086,3.377 2.927,3.405C2.778,3.433 2.647,3.475 2.535,3.531C2.423,3.587 2.335,3.657 2.269,3.741C2.213,3.816 2.185,3.909 2.185,4.021C2.185,4.18 2.251,4.31 2.381,4.413C2.521,4.506 2.703,4.586 2.927,4.651C3.151,4.716 3.399,4.786 3.669,4.861C4.005,4.954 4.355,5.048 4.719,5.141C5.093,5.225 5.438,5.342 5.755,5.491C6.073,5.64 6.329,5.855 6.525,6.135C6.721,6.406 6.819,6.774 6.819,7.241C6.819,7.689 6.731,8.067 6.553,8.375C6.385,8.683 6.147,8.93 5.839,9.117C5.531,9.304 5.172,9.439 4.761,9.523C4.351,9.607 3.907,9.649 3.431,9.649Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
|
@ -1,9 +1,21 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="26dp"
|
||||
android:height="26dp"
|
||||
android:viewportWidth="26"
|
||||
android:viewportHeight="26">
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:pathData="M16.727,11.88H22.906C24.644,11.88 25.5,11.01 25.5,9.218V3.148C25.5,1.355 24.644,0.5 22.906,0.5H16.727C15.003,0.5 14.134,1.355 14.134,3.148V9.218C14.134,11.011 15.003,11.88 16.727,11.88ZM3.094,11.88H9.286C11.01,11.88 11.88,11.01 11.88,9.218V3.148C11.88,1.355 11.01,0.5 9.286,0.5H3.094C1.369,0.5 0.5,1.355 0.5,3.148V9.218C0.5,11.011 1.369,11.88 3.094,11.88ZM3.121,9.965C2.645,9.965 2.415,9.72 2.415,9.218V3.148C2.415,2.659 2.645,2.415 3.121,2.415H9.245C9.72,2.415 9.965,2.659 9.965,3.148V9.218C9.965,9.721 9.72,9.965 9.245,9.965H3.121ZM16.755,9.965C16.279,9.965 16.049,9.72 16.049,9.218V3.148C16.049,2.659 16.279,2.415 16.755,2.415H22.893C23.354,2.415 23.585,2.659 23.585,3.148V9.218C23.585,9.721 23.354,9.965 22.893,9.965H16.755ZM5.022,7.616H7.344C7.548,7.616 7.629,7.534 7.629,7.304V5.049C7.629,4.832 7.548,4.75 7.344,4.75H5.022C4.819,4.75 4.764,4.832 4.764,5.049V7.304C4.764,7.534 4.819,7.616 5.022,7.616ZM18.737,7.616H21.046C21.25,7.616 21.331,7.534 21.331,7.304V5.049C21.331,4.832 21.25,4.75 21.046,4.75H18.737C18.534,4.75 18.466,4.832 18.466,5.049V7.304C18.466,7.534 18.534,7.616 18.737,7.616ZM3.094,25.5H9.286C11.01,25.5 11.88,24.645 11.88,22.852V16.768C11.88,14.99 11.01,14.12 9.286,14.12H3.094C1.369,14.12 0.5,14.99 0.5,16.768V22.852C0.5,24.645 1.369,25.5 3.094,25.5ZM15.043,17.637H17.365C17.569,17.637 17.65,17.556 17.65,17.325V15.071C17.65,14.853 17.569,14.772 17.365,14.772H15.043C14.84,14.772 14.785,14.853 14.785,15.071V17.325C14.785,17.556 14.84,17.637 15.043,17.637ZM22.254,17.637H24.576C24.78,17.637 24.861,17.556 24.861,17.325V15.071C24.861,14.853 24.78,14.772 24.576,14.772H22.254C22.051,14.772 21.983,14.853 21.983,15.071V17.325C21.983,17.556 22.05,17.637 22.254,17.637ZM3.121,23.585C2.645,23.585 2.415,23.341 2.415,22.852V16.782C2.415,16.28 2.645,16.035 3.121,16.035H9.245C9.72,16.035 9.965,16.28 9.965,16.782V22.852C9.965,23.341 9.72,23.585 9.245,23.585H3.121ZM5.022,21.25H7.344C7.548,21.25 7.629,21.168 7.629,20.924V18.683C7.629,18.465 7.548,18.384 7.344,18.384H5.022C4.819,18.384 4.764,18.465 4.764,18.683V20.924C4.764,21.168 4.819,21.25 5.022,21.25ZM18.683,21.25H21.005C21.209,21.25 21.29,21.168 21.29,20.924V18.683C21.29,18.465 21.209,18.384 21.005,18.384H18.683C18.479,18.384 18.425,18.465 18.425,18.683V20.924C18.425,21.168 18.479,21.25 18.683,21.25ZM15.043,24.848H17.365C17.569,24.848 17.65,24.766 17.65,24.535V22.281C17.65,22.064 17.569,21.983 17.365,21.983H15.043C14.84,21.983 14.785,22.064 14.785,22.281V24.535C14.785,24.766 14.84,24.848 15.043,24.848ZM22.254,24.848H24.576C24.78,24.848 24.861,24.766 24.861,24.535V22.281C24.861,22.064 24.78,21.983 24.576,21.983H22.254C22.051,21.983 21.983,22.064 21.983,22.281V24.535C21.983,24.766 22.05,24.848 22.254,24.848Z"
|
||||
android:fillColor="#000000"/>
|
||||
android:pathData="M0.5,8C0.5,3.858 3.858,0.5 8,0.5H28C32.142,0.5 35.5,3.858 35.5,8V28C35.5,32.142 32.142,35.5 28,35.5H8C3.858,35.5 0.5,32.142 0.5,28V8Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M0.5,8C0.5,3.858 3.858,0.5 8,0.5H28C32.142,0.5 35.5,3.858 35.5,8V28C35.5,32.142 32.142,35.5 28,35.5H8C3.858,35.5 0.5,32.142 0.5,28V8Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#D9D8CF"/>
|
||||
<path
|
||||
android:pathData="M13.833,18H18V22.167M10.508,18H10.5M14.675,22.167H14.667M18.008,25.5H18M25.508,18H25.5M10.5,22.167H11.75M20.917,18H22.583M10.5,25.5H14.667M18,9.667V14.667M22.667,25.5H24.167C24.633,25.5 24.867,25.5 25.045,25.409C25.202,25.329 25.329,25.202 25.409,25.045C25.5,24.867 25.5,24.633 25.5,24.167V22.667C25.5,22.2 25.5,21.967 25.409,21.788C25.329,21.632 25.202,21.504 25.045,21.424C24.867,21.333 24.633,21.333 24.167,21.333H22.667C22.2,21.333 21.967,21.333 21.788,21.424C21.632,21.504 21.504,21.632 21.424,21.788C21.333,21.967 21.333,22.2 21.333,22.667V24.167C21.333,24.633 21.333,24.867 21.424,25.045C21.504,25.202 21.632,25.329 21.788,25.409C21.967,25.5 22.2,25.5 22.667,25.5ZM22.667,14.667H24.167C24.633,14.667 24.867,14.667 25.045,14.576C25.202,14.496 25.329,14.368 25.409,14.212C25.5,14.033 25.5,13.8 25.5,13.333V11.833C25.5,11.367 25.5,11.133 25.409,10.955C25.329,10.798 25.202,10.671 25.045,10.591C24.867,10.5 24.633,10.5 24.167,10.5H22.667C22.2,10.5 21.967,10.5 21.788,10.591C21.632,10.671 21.504,10.798 21.424,10.955C21.333,11.133 21.333,11.367 21.333,11.833V13.333C21.333,13.8 21.333,14.033 21.424,14.212C21.504,14.368 21.632,14.496 21.788,14.576C21.967,14.667 22.2,14.667 22.667,14.667ZM11.833,14.667H13.333C13.8,14.667 14.033,14.667 14.212,14.576C14.368,14.496 14.496,14.368 14.576,14.212C14.667,14.033 14.667,13.8 14.667,13.333V11.833C14.667,11.367 14.667,11.133 14.576,10.955C14.496,10.798 14.368,10.671 14.212,10.591C14.033,10.5 13.8,10.5 13.333,10.5H11.833C11.367,10.5 11.133,10.5 10.955,10.591C10.798,10.671 10.671,10.798 10.591,10.955C10.5,11.133 10.5,11.367 10.5,11.833V13.333C10.5,13.8 10.5,14.033 10.591,14.212C10.671,14.368 10.798,14.496 10.955,14.576C11.133,14.667 11.367,14.667 11.833,14.667Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#231F20"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="16">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h20v16h-20z"/>
|
||||
<path
|
||||
android:pathData="M19.92,0.07C19.81,-0.03 19.5,0 19.31,0.06C13.04,2.2 6.77,4.34 0.5,6.5C0.31,6.56 0.17,6.75 0,6.88C0.13,7.04 0.24,7.25 0.41,7.35C1.57,8.05 2.76,8.72 3.92,9.43C4.15,9.57 4.37,9.85 4.44,10.11C4.93,11.9 5.38,13.7 5.86,15.49C5.91,15.68 6.05,15.96 6.2,15.99C6.35,16.02 6.6,15.85 6.74,15.7C7.68,14.75 8.62,13.79 9.53,12.82C9.79,12.54 9.99,12.48 10.35,12.66C11.64,13.32 12.95,13.93 14.25,14.56C14.39,14.63 14.53,14.69 14.77,14.79C14.89,14.6 15.04,14.43 15.12,14.23C16.74,9.71 18.35,5.18 19.96,0.65C20.02,0.47 20.04,0.16 19.93,0.06L19.92,0.07ZM4.26,8.81C3.28,8.21 2.28,7.64 1.2,7C6.3,5.25 11.28,3.55 16.26,1.84L16.3,1.92C16.16,2.01 16.03,2.1 15.89,2.18C12.27,4.38 8.66,6.57 5.04,8.78C4.76,8.95 4.55,8.99 4.26,8.8V8.81ZM6.71,10.97C6.53,11.87 6.39,12.77 6.12,13.68C6.02,13.3 5.92,12.93 5.82,12.55C5.6,11.69 5.38,10.84 5.15,9.98C5.09,9.76 5.06,9.58 5.32,9.43C8.56,7.48 11.79,5.51 15.03,3.55C15.07,3.53 15.12,3.53 15.3,3.49C14.16,4.44 13.13,5.3 12.09,6.16C11.63,6.54 8.6,8.99 8.14,9.38C8.07,9.44 8,9.5 7.95,9.57C7.62,9.81 7.3,10.08 7.01,10.37C6.85,10.52 6.75,10.77 6.71,10.98V10.97ZM6.81,14.61C7.01,13.42 7.19,12.38 7.38,11.25C8,11.55 8.58,11.83 9.2,12.13C8.41,12.95 7.65,13.73 6.8,14.61H6.81ZM14.48,13.9C12.24,12.81 10.04,11.75 7.77,10.65C8.09,10.38 8.39,10.18 8.62,9.92C11.01,7.93 15.96,3.88 18.35,1.89C18.49,1.77 18.63,1.66 18.86,1.6C17.41,5.68 15.95,9.77 14.48,13.9Z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -1,19 +1,18 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="send_stage_send_title">Send</string>
|
||||
<string name="send_scan_content_description">Scan</string>
|
||||
<string name="send_address_label">To:</string>
|
||||
<string name="send_address_label">Send to</string>
|
||||
<string name="send_address_hint">Zcash Address</string>
|
||||
<string name="send_address_invalid">Invalid address</string>
|
||||
<string name="send_amount_label">Amount:</string>
|
||||
<string name="send_amount_hint"><xliff:g id="currency" example="ZEC">%1$s</xliff:g> Amount</string>
|
||||
<string name="send_usd_amount_hint">USD Amount</string>
|
||||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_amount_hint"><xliff:g id="currency" example="ZEC">%1$s</xliff:g></string>
|
||||
<string name="send_usd_amount_hint">USD</string>
|
||||
<string name="send_amount_insufficient_balance">Insufficient funds</string>
|
||||
<string name="send_amount_invalid">Invalid amount</string>
|
||||
<string name="send_memo_label">Message</string>
|
||||
<string name="send_memo_hint">Write private message here…</string>
|
||||
<string name="send_memo_hint">Write encrypted message here…</string>
|
||||
<string name="send_memo_bytes_counter">
|
||||
<xliff:g id="typed_bytes" example="12">%1$s</xliff:g>/
|
||||
<xliff:g id="max_bytes" example="500">%2$s</xliff:g>
|
||||
<xliff:g id="typed_bytes" example="12">%1$s</xliff:g>/<xliff:g id="max_bytes" example="512">%2$s</xliff:g>
|
||||
</string>
|
||||
<string name="send_create">Review</string>
|
||||
<string name="send_fee">(Typical Fee < <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
|
||||
|
@ -23,5 +22,6 @@
|
|||
<string name="send_dialog_error_btn">OK</string>
|
||||
|
||||
<string name="send_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g>…<xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
|
||||
<string name="send_transparent_memo">Transparent transactions can\'t have memos</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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.009M18.334,10C18.334,14.602 14.603,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.603,1.667 18.334,5.398 18.334,10Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#94907B"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -1,12 +1,13 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="send_stage_confirmation_title">Confirmation</string>
|
||||
<string name="send_confirmation_amount">Amount:</string>
|
||||
<string name="send_confirmation_address">To:</string>
|
||||
<string name="send_confirmation_amount">Total Amount</string>
|
||||
<string name="send_confirmation_address">Sending to</string>
|
||||
<string name="send_confirmation_memo">Message</string>
|
||||
<string name="send_confirmation_fee">Fee:</string>
|
||||
<string name="send_confirmation_fee">Fee</string>
|
||||
<string name="send_confirmation_amount_item">Amount</string>
|
||||
<string name="send_confirmation_send_button">Send</string>
|
||||
<string name="send_confirmation_back_button">Go Back</string>
|
||||
<string name="send_confirmation_back_button">Cancel</string>
|
||||
|
||||
<string name="send_confirmation_dialog_error_title">Transaction Failed</string>
|
||||
<string name="send_confirmation_dialog_error_text">An error occurred and the attempt to send funds failed. Try it again, please.</string>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
|
||||
android:fillColor="#454243"/>
|
||||
<path
|
||||
android:pathData="M12.5,26.667C14.446,24.602 17.089,23.333 20,23.333C22.911,23.333 25.553,24.602 27.5,26.667M23.75,16.25C23.75,18.321 22.071,20 20,20C17.929,20 16.25,18.321 16.25,16.25C16.25,14.179 17.929,12.5 20,12.5C22.071,12.5 23.75,14.179 23.75,16.25Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.66667"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E8E8E8"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
|
||||
android:fillColor="#EBEBE6"/>
|
||||
<path
|
||||
android:pathData="M12.5,26.667C14.446,24.602 17.089,23.333 20,23.333C22.911,23.333 25.553,24.602 27.5,26.667M23.75,16.25C23.75,18.321 22.071,20 20,20C17.929,20 16.25,18.321 16.25,16.25C16.25,14.179 17.929,12.5 20,12.5C22.071,12.5 23.75,14.179 23.75,16.25Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.66667"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#231F20"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -4,6 +4,7 @@
|
|||
<string name="settings_about_us">About Us</string>
|
||||
<string name="settings_feedback">Send Us Feedback</string>
|
||||
<string name="settings_version">Version %s</string>
|
||||
<string name="settings_address_book">Address Book</string>
|
||||
|
||||
<string name="settings_troubleshooting_menu_content_description">Additional settings</string>
|
||||
<string name="settings_troubleshooting_rescan">Rescan blockchain</string>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="update_contact_title">Saved Contact</string>
|
||||
<string name="update_contact_primary_btn">Save</string>
|
||||
<string name="update_contact_secondary_btn">Delete</string>
|
||||
</resources>
|
|
@ -33,7 +33,6 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
|
|||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
|
@ -48,6 +47,7 @@ import co.electriccoin.zcash.ui.screen.home.HomeTag
|
|||
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
||||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
||||
import co.electriccoin.zcash.ui.screen.securitywarning.view.SecurityScreenTag.ACKNOWLEDGE_CHECKBOX_TAG
|
||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -475,11 +475,8 @@ private fun sendZecScreenshots(
|
|||
// Screenshot: Empty form
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 1")
|
||||
|
||||
composeTestRule.onNodeWithText(
|
||||
resContext.getString(
|
||||
R.string.send_amount_hint,
|
||||
ZcashCurrency.fromResources(resContext).name
|
||||
)
|
||||
composeTestRule.onNode(
|
||||
hasTestTag(SendTag.SEND_AMOUNT_FIELD)
|
||||
).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
|
|
Loading…
Reference in New Issue