[#1367] Send 0 funds only within shielded trans

- Closes #1367
- Plus slightly refactored SendForm composable to two functions
- Changelog update
This commit is contained in:
Honza 2024-04-19 11:46:36 +02:00
parent 533f335038
commit b42a455ba9
5 changed files with 118 additions and 76 deletions

View File

@ -9,6 +9,9 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased]
### Fixed
- Sending zero funds is allowed only for shielded recipient address type
## [0.2.0 (609)] - 2024-04-18
### Added

View File

@ -128,7 +128,7 @@ class SendViewTestSetup(
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
},
setAmountState = {},
amountState = AmountState.new(context, "", monetarySeparators),
amountState = AmountState.new(context, monetarySeparators, "", false),
setMemoState = {},
memoState = MemoState.new(""),
walletRestoringState = WalletRestoringState.NONE,

View File

@ -8,6 +8,7 @@ import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
@ -21,6 +22,7 @@ import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.proposeSend
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
@ -95,7 +97,7 @@ internal fun WrapSend(
)
}
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
@VisibleForTesting
@Composable
internal fun WrapSend(
@ -140,14 +142,28 @@ internal fun WrapSend(
// Amount computation:
val (amountState, setAmountState) =
rememberSaveable(stateSaver = AmountState.Saver) {
// Default amount state
mutableStateOf(
AmountState.new(
context = context,
value = zecSend?.amount?.toZecString() ?: "",
monetarySeparators = monetarySeparators
monetarySeparators = monetarySeparators,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false
)
)
}
// New amount state based on the recipient address type (e.g. shielded supports zero funds sending and
// transparent not)
LaunchedEffect(key1 = recipientAddressState) {
setAmountState(
AmountState.new(
context = context,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
monetarySeparators = monetarySeparators,
value = amountState.value
)
)
}
// Memo computation:
val (memoState, setMemoState) =
@ -155,12 +171,12 @@ internal fun WrapSend(
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
}
// Clearing form if required form the previous navigation destination
// Clearing form from the previous navigation destination if required
if (sendArguments?.clearForm == true) {
setSendStage(SendStage.Form)
setZecSend(null)
setRecipientAddressState(RecipientAddressState.new("", null))
setAmountState(AmountState.new(context, "", monetarySeparators))
setAmountState(AmountState.new(context, monetarySeparators, "", false))
setMemoState(MemoState.new(""))
}

View File

@ -23,8 +23,9 @@ sealed class AmountState(
companion object {
fun new(
context: Context,
monetarySeparators: MonetarySeparators,
value: String,
monetarySeparators: MonetarySeparators
isTransparentRecipient: Boolean
): AmountState {
// Validate raw input string
val validated =
@ -41,11 +42,11 @@ sealed class AmountState(
// Convert the input to Zatoshi type-safe amount representation
val zatoshi = (Zatoshi.fromZecString(context, value, monetarySeparators))
// Note that the 0 funds sending is supported for sending a memo-only transaction
return if (zatoshi == null) {
Invalid(value)
} else {
Valid(value, zatoshi)
// Note that the zero funds sending is supported for sending a memo-only shielded transaction
return when {
(zatoshi == null) -> Invalid(value)
(zatoshi.value == 0L && isTransparentRecipient) -> Invalid(value)
else -> Valid(value, zatoshi)
}
}

View File

@ -264,8 +264,6 @@ private fun SendForm(
hasCameraFeature: Boolean,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
// TODO [#1171]: Remove default MonetarySeparators locale
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
val monetarySeparators = MonetarySeparators.current(Locale.US)
@ -305,16 +303,17 @@ private fun SendForm(
SendFormAmountTextField(
amountSate = amountState,
setAmountState = setAmountState,
monetarySeparators = monetarySeparators,
focusManager = focusManager,
walletSnapshot = walletSnapshot,
imeAction =
if (recipientAddressState.type == AddressType.Transparent) {
ImeAction.Done
} else {
ImeAction.Next
}
},
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
monetarySeparators = monetarySeparators,
setAmountState = setAmountState,
walletSnapshot = walletSnapshot,
)
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
@ -342,68 +341,83 @@ private fun SendForm(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
// Common conditions continuously checked for validity
val sendButtonEnabled =
recipientAddressState.type !is AddressType.Invalid &&
recipientAddressState.address.isNotEmpty() &&
amountState is AmountState.Valid &&
amountState.value.isNotBlank() &&
walletSnapshot.canSpend(amountState.zatoshi) &&
// A valid memo is necessary only for non-transparent recipient
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
SendButton(amountState, memoState, monetarySeparators, onCreateZecSend, recipientAddressState, walletSnapshot)
}
}
Column(
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryButton(
onClick = {
// SDK side validations
val zecSendValidation =
ZecSendExt.new(
context = context,
destinationString = recipientAddressState.address,
zecString = amountState.value,
// Take memo for a valid non-transparent receiver only
memoString =
if (recipientAddressState.type == AddressType.Transparent) {
""
} else {
memoState.text
},
monetarySeparators = monetarySeparators
)
@Composable
@Suppress("LongParameterList")
fun SendButton(
amountState: AmountState,
memoState: MemoState,
monetarySeparators: MonetarySeparators,
onCreateZecSend: (ZecSend) -> Unit,
recipientAddressState: RecipientAddressState,
walletSnapshot: WalletSnapshot,
) {
val context = LocalContext.current
when (zecSendValidation) {
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
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}" }
}
// Common conditions continuously checked for validity
val sendButtonEnabled =
recipientAddressState.type !is AddressType.Invalid &&
recipientAddressState.address.isNotEmpty() &&
amountState is AmountState.Valid &&
amountState.value.isNotBlank() &&
walletSnapshot.canSpend(amountState.zatoshi) &&
// 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(
onClick = {
// SDK side validations
val zecSendValidation =
ZecSendExt.new(
context = context,
destinationString = recipientAddressState.address,
zecString = amountState.value,
// Take memo for a valid non-transparent receiver only
memoString =
if (recipientAddressState.type == AddressType.Transparent) {
""
} else {
memoState.text
},
monetarySeparators = monetarySeparators
)
when (zecSendValidation) {
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
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,
modifier =
Modifier
.testTag(SendTag.SEND_FORM_BUTTON)
.fillMaxWidth()
)
}
},
text = stringResource(id = R.string.send_create),
enabled = sendButtonEnabled,
modifier =
Modifier
.testTag(SendTag.SEND_FORM_BUTTON)
.fillMaxWidth()
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
BodySmall(
text =
stringResource(
id = R.string.send_fee,
// TODO [#1047]: Representing Zatoshi amount
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
),
textFontWeight = FontWeight.SemiBold
)
}
BodySmall(
text =
stringResource(
id = R.string.send_fee,
// TODO [#1047]: Representing Zatoshi amount
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
),
textFontWeight = FontWeight.SemiBold
)
}
}
@ -496,10 +510,11 @@ fun SendFormAddressTextField(
fun SendFormAmountTextField(
amountSate: AmountState,
focusManager: FocusManager,
imeAction: ImeAction,
isTransparentRecipient: Boolean,
monetarySeparators: MonetarySeparators,
setAmountState: (AmountState) -> Unit,
walletSnapshot: WalletSnapshot,
imeAction: ImeAction,
) {
val context = LocalContext.current
@ -540,7 +555,14 @@ fun SendFormAmountTextField(
FormTextField(
value = amountSate.value,
onValueChange = { newValue ->
setAmountState(AmountState.new(context, newValue, monetarySeparators))
setAmountState(
AmountState.new(
context = context,
value = newValue,
monetarySeparators = monetarySeparators,
isTransparentRecipient = isTransparentRecipient
)
)
},
modifier = Modifier.fillMaxWidth(),
error = amountError,