[#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:
Honza Rychnovský 2024-04-23 08:39:40 +02:00 committed by GitHub
parent b00c807df1
commit fb5d446bab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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] ## [Unreleased]
### Fixed
- Sending zero funds is allowed only for shielded recipient address type
## [0.2.0 (609)] - 2024-04-18 ## [0.2.0 (609)] - 2024-04-18
### Added ### Added

View File

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

View File

@ -8,6 +8,7 @@ import androidx.activity.compose.BackHandler
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable 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.ZecSend
import cash.z.ecc.android.sdk.model.proposeSend import cash.z.ecc.android.sdk.model.proposeSend
import cash.z.ecc.android.sdk.model.toZecString 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.spackle.Twig
import co.electriccoin.zcash.ui.common.compose.BalanceState import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletRestoringState
@ -95,7 +97,7 @@ internal fun WrapSend(
) )
} }
@Suppress("LongParameterList", "LongMethod") @Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
@VisibleForTesting @VisibleForTesting
@Composable @Composable
internal fun WrapSend( internal fun WrapSend(
@ -140,11 +142,25 @@ internal fun WrapSend(
// Amount computation: // Amount computation:
val (amountState, setAmountState) = val (amountState, setAmountState) =
rememberSaveable(stateSaver = AmountState.Saver) { rememberSaveable(stateSaver = AmountState.Saver) {
// Default amount state
mutableStateOf( mutableStateOf(
AmountState.new( AmountState.new(
context = context, context = context,
value = zecSend?.amount?.toZecString() ?: "", 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
) )
) )
} }
@ -155,12 +171,12 @@ internal fun WrapSend(
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: "")) 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) { if (sendArguments?.clearForm == true) {
setSendStage(SendStage.Form) setSendStage(SendStage.Form)
setZecSend(null) setZecSend(null)
setRecipientAddressState(RecipientAddressState.new("", null)) setRecipientAddressState(RecipientAddressState.new("", null))
setAmountState(AmountState.new(context, "", monetarySeparators)) setAmountState(AmountState.new(context, monetarySeparators, "", false))
setMemoState(MemoState.new("")) setMemoState(MemoState.new(""))
} }

View File

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

View File

@ -264,8 +264,6 @@ private fun SendForm(
hasCameraFeature: Boolean, hasCameraFeature: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current
// TODO [#1171]: Remove default MonetarySeparators locale // TODO [#1171]: Remove default MonetarySeparators locale
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171 // TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
val monetarySeparators = MonetarySeparators.current(Locale.US) val monetarySeparators = MonetarySeparators.current(Locale.US)
@ -305,16 +303,17 @@ private fun SendForm(
SendFormAmountTextField( SendFormAmountTextField(
amountSate = amountState, amountSate = amountState,
setAmountState = setAmountState,
monetarySeparators = monetarySeparators,
focusManager = focusManager, focusManager = focusManager,
walletSnapshot = walletSnapshot,
imeAction = imeAction =
if (recipientAddressState.type == AddressType.Transparent) { if (recipientAddressState.type == AddressType.Transparent) {
ImeAction.Done ImeAction.Done
} else { } else {
ImeAction.Next ImeAction.Next
} },
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
monetarySeparators = monetarySeparators,
setAmountState = setAmountState,
walletSnapshot = walletSnapshot,
) )
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault)) Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
@ -342,6 +341,22 @@ private fun SendForm(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
SendButton(amountState, memoState, monetarySeparators, onCreateZecSend, recipientAddressState, walletSnapshot)
}
}
@Composable
@Suppress("LongParameterList")
fun SendButton(
amountState: AmountState,
memoState: MemoState,
monetarySeparators: MonetarySeparators,
onCreateZecSend: (ZecSend) -> Unit,
recipientAddressState: RecipientAddressState,
walletSnapshot: WalletSnapshot,
) {
val context = LocalContext.current
// Common conditions continuously checked for validity // Common conditions continuously checked for validity
val sendButtonEnabled = val sendButtonEnabled =
recipientAddressState.type !is AddressType.Invalid && recipientAddressState.type !is AddressType.Invalid &&
@ -404,7 +419,6 @@ private fun SendForm(
textFontWeight = FontWeight.SemiBold textFontWeight = FontWeight.SemiBold
) )
} }
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -496,10 +510,11 @@ fun SendFormAddressTextField(
fun SendFormAmountTextField( fun SendFormAmountTextField(
amountSate: AmountState, amountSate: AmountState,
focusManager: FocusManager, focusManager: FocusManager,
imeAction: ImeAction,
isTransparentRecipient: Boolean,
monetarySeparators: MonetarySeparators, monetarySeparators: MonetarySeparators,
setAmountState: (AmountState) -> Unit, setAmountState: (AmountState) -> Unit,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
imeAction: ImeAction,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -540,7 +555,14 @@ fun SendFormAmountTextField(
FormTextField( FormTextField(
value = amountSate.value, value = amountSate.value,
onValueChange = { newValue -> onValueChange = { newValue ->
setAmountState(AmountState.new(context, newValue, monetarySeparators)) setAmountState(
AmountState.new(
context = context,
value = newValue,
monetarySeparators = monetarySeparators,
isTransparentRecipient = isTransparentRecipient
)
)
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
error = amountError, error = amountError,