[#1367] Send 0 funds only within shielded transaction
- Closes #1367 - Plus slightly refactored SendForm composable to two functions - Changelog update
This commit is contained in:
parent
b00c807df1
commit
fb5d446bab
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(""))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue