Dust notes

This commit is contained in:
Milan Cerovsky 2025-05-12 12:22:27 +02:00
parent 0d30109a25
commit fe43bcad1c
11 changed files with 269 additions and 242 deletions

View File

@ -2,6 +2,7 @@ package cash.z.ecc.sdk.extension
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlin.math.floor
private const val DECIMALS_MAX_LONG = 8
private const val DECIMALS_MIN_LONG = 3
@ -34,6 +35,8 @@ fun Zatoshi.toZecStringAbbreviated(suffix: String): ZecAmountPair {
}
}
fun Zatoshi.floor(): Zatoshi = Zatoshi(floorRoundBy(value.toDouble(), 2500.0).toLong())
data class ZecAmountPair(
val main: String,
val suffix: String
@ -43,3 +46,8 @@ val Zatoshi.Companion.typicalFee: Zatoshi
get() = Zatoshi(TYPICAL_FEE)
private const val TYPICAL_FEE = 100000L
private fun floorRoundBy(number: Double, multiple: Double): Double {
require(multiple != 0.0) { "Multiple cannot be zero" }
return floor(number / multiple) * multiple
}

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.common.usecase
import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.sdk.extension.floor
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
@ -16,15 +17,16 @@ class CreateProposalUseCase(
private val navigationRouter: NavigationRouter
) {
@Suppress("TooGenericExceptionCaught")
suspend operator fun invoke(zecSend: ZecSend) {
suspend operator fun invoke(zecSend: ZecSend, floor: Boolean) {
val normalized = if (floor) zecSend.copy(amount = zecSend.amount.floor()) else zecSend
try {
when (accountDataSource.getSelectedAccount()) {
is KeystoneAccount -> {
keystoneProposalRepository.createProposal(zecSend)
keystoneProposalRepository.createProposal(normalized)
keystoneProposalRepository.createPCZTFromProposal()
}
is ZashiAccount ->
zashiProposalRepository.createProposal(zecSend)
zashiProposalRepository.createProposal(normalized)
}
navigationRouter.forward(ReviewTransaction)
} catch (e: Exception) {

View File

@ -2,6 +2,8 @@ package co.electriccoin.zcash.ui.screen.request.model
import android.content.Context
import cash.z.ecc.android.sdk.ext.convertUsdToZec
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.toZec
import cash.z.ecc.android.sdk.ext.toZecString
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.Locale
@ -9,6 +11,8 @@ import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.fromZecString
import cash.z.ecc.android.sdk.model.toFiatString
import cash.z.ecc.sdk.extension.floor
import cash.z.ecc.sdk.extension.toZecStringFull
import co.electriccoin.zcash.ui.screen.request.ext.convertToDouble
data class Request(
@ -17,63 +21,41 @@ data class Request(
val qrCodeState: QrCodeState,
)
sealed class AmountState(
open val amount: String,
open val currency: RequestCurrency
data class AmountState(
val amount: String,
val currency: RequestCurrency,
val isValid: Boolean?
) {
fun isValid(): Boolean = this is Valid
abstract fun copyState(
newValue: String = amount,
newCurrency: RequestCurrency = currency
): AmountState
fun toZecString(conversion: FiatCurrencyConversion): String =
runCatching {
fun toZecString(
conversion: FiatCurrencyConversion,
): String {
return runCatching {
amount.convertToDouble().convertUsdToZec(conversion.priceOfZec).toZecString()
}.getOrElse { "" }
fun toFiatString(
context: Context,
conversion: FiatCurrencyConversion
): String =
kotlin
.runCatching {
Zatoshi.fromZecString(context, amount, Locale.getDefault())?.toFiatString(
currencyConversion = conversion,
locale = Locale.getDefault()
) ?: ""
}.getOrElse { "" }
data class Valid(
override val amount: String,
override val currency: RequestCurrency
) : AmountState(amount, currency) {
override fun copyState(
newValue: String,
newCurrency: RequestCurrency
) = copy(amount = newValue, currency = newCurrency)
}
data class Default(
override val amount: String,
override val currency: RequestCurrency
) : AmountState(amount, currency) {
override fun copyState(
newValue: String,
newCurrency: RequestCurrency
) = copy(amount = newValue, currency = newCurrency)
fun toZecStringFloored(
conversion: FiatCurrencyConversion,
): String {
return runCatching {
amount.convertToDouble().convertUsdToZec(conversion.priceOfZec)
.convertZecToZatoshi()
.floor()
.toZecStringFull()
}.getOrElse { "" }
}
data class InValid(
override val amount: String,
override val currency: RequestCurrency
) : AmountState(amount, currency) {
override fun copyState(
newValue: String,
newCurrency: RequestCurrency
) = copy(amount = newValue, currency = newCurrency)
}
fun toFiatString(context: Context, conversion: FiatCurrencyConversion) =
runCatching {
Zatoshi.fromZecString(
context = context,
zecString = amount,
locale = Locale.getDefault()
)?.toFiatString(
currencyConversion = conversion,
locale = Locale.getDefault()
) ?: ""
}.getOrElse { "" }
}
sealed class MemoState(
@ -96,10 +78,7 @@ sealed class MemoState(
) : MemoState(text, byteSize, zecAmount)
companion object {
fun new(
memo: String,
amount: String
): MemoState {
fun new(memo: String, amount: String): MemoState {
val bytesCount = Memo.countLength(memo)
return if (bytesCount > Memo.MAX_MEMO_LENGTH_BYTES) {
InValid(memo, bytesCount, amount)

View File

@ -1,7 +1,3 @@
package co.electriccoin.zcash.ui.screen.request.model
sealed class RequestCurrency {
data object Zec : RequestCurrency()
data object Fiat : RequestCurrency()
}
enum class RequestCurrency { ZEC, FIAT }

View File

@ -63,16 +63,16 @@ internal fun RequestAmountView(
when (state.exchangeRateState) {
is ExchangeRateState.Data -> {
if (state.request.amountState.currency == RequestCurrency.Zec) {
if (state.request.amountState.currency == RequestCurrency.ZEC) {
RequestAmountWithMainZecView(
state = state,
onFiatPreferenceSwitch = { state.onSwitch(RequestCurrency.Fiat) },
onFiatPreferenceSwitch = { state.onSwitch(RequestCurrency.FIAT) },
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular)
)
} else {
RequestAmountWithMainFiatView(
state = state,
onFiatPreferenceSwitch = { state.onSwitch(RequestCurrency.Zec) },
onFiatPreferenceSwitch = { state.onSwitch(RequestCurrency.ZEC) },
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular)
)
}
@ -417,7 +417,7 @@ private fun InvalidAmountView(
.fillMaxWidth()
.requiredHeight(48.dp)
) {
if (amountState is AmountState.InValid) {
if (amountState.isValid == false) {
Image(
painter = painterResource(id = R.drawable.ic_alert_outline),
contentDescription = null

View File

@ -65,7 +65,7 @@ private fun RequestPreview() =
RequestState.Amount(
request =
Request(
amountState = AmountState.Valid("2.25", RequestCurrency.Zec),
amountState = AmountState("2.25", RequestCurrency.ZEC, true),
memoState = MemoState.Valid("", 0, "2.25"),
qrCodeState =
QrCodeState(
@ -143,7 +143,7 @@ private fun RequestBottomBar(
ZashiButton(
text = stringResource(id = R.string.request_amount_btn),
onClick = state.onDone,
enabled = state.request.amountState.isValid(),
enabled = state.request.amountState.isValid == true,
modifier =
Modifier
.fillMaxWidth()

View File

@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@Suppress("TooManyFunctions")
@ -68,7 +69,11 @@ class RequestViewModel(
internal val request =
MutableStateFlow(
Request(
amountState = AmountState.Default(defaultAmount, RequestCurrency.Zec),
amountState = AmountState(
amount = defaultAmount,
currency = RequestCurrency.ZEC,
isValid = null
),
memoState = MemoState.Valid(DEFAULT_MEMO, 0, defaultAmount),
qrCodeState = QrCodeState(DEFAULT_URI, defaultAmount, DEFAULT_MEMO),
)
@ -94,26 +99,13 @@ class RequestViewModel(
monetarySeparators = getMonetarySeparators(),
onAmount = { onAmount(resolveExchangeRateValue(exchangeRateUsd), it) },
onBack = { onBack() },
onDone = {
when (walletAddress) {
is WalletAddress.Transparent -> {
onAmountAndMemoDone(
walletAddress.address,
zip321BuildUriUseCase,
resolveExchangeRateValue(exchangeRateUsd)
)
}
is WalletAddress.Unified, is WalletAddress.Sapling -> {
onAmountDone(resolveExchangeRateValue(exchangeRateUsd))
}
else -> error("Unexpected address type")
}
},
onDone = { onNextClick(walletAddress, zip321BuildUriUseCase, exchangeRateUsd) },
onSwitch = { onSwitch(resolveExchangeRateValue(exchangeRateUsd), it) },
request = request,
zcashCurrency = getZcashCurrency(),
)
}
RequestStage.MEMO -> {
RequestState.Memo(
icon =
@ -129,6 +121,7 @@ class RequestViewModel(
zcashCurrency = getZcashCurrency(),
)
}
RequestStage.QR_CODE -> {
RequestState.QrCode(
icon =
@ -136,6 +129,7 @@ class RequestViewModel(
is KeystoneAccount ->
co.electriccoin.zcash.ui.design.R.drawable
.ic_item_keystone_qr
is ZashiAccount -> R.drawable.logo_zec_fill_stroke
},
fullScreenIcon =
@ -143,6 +137,7 @@ class RequestViewModel(
is KeystoneAccount ->
co.electriccoin.zcash.ui.design.R.drawable
.ic_item_keystone_qr_white
is ZashiAccount -> R.drawable.logo_zec_fill_stroke_white
},
walletAddress = walletAddress,
@ -169,6 +164,25 @@ class RequestViewModel(
val shareResultCommand = MutableSharedFlow<Boolean>()
private fun onNextClick(
walletAddress: WalletAddress,
zip321BuildUriUseCase: Zip321BuildUriUseCase,
exchangeRateUsd: ExchangeRateState
) {
when (walletAddress) {
is WalletAddress.Transparent -> onAmountAndMemoDone(
walletAddress.address,
zip321BuildUriUseCase,
resolveExchangeRateValue(exchangeRateUsd)
)
is WalletAddress.Unified, is WalletAddress.Sapling ->
onAmountDone(resolveExchangeRateValue(exchangeRateUsd))
else -> error("Unexpected address type")
}
}
private fun resolveExchangeRateValue(exchangeRateUsd: ExchangeRateState): FiatCurrencyConversion? =
when (exchangeRateUsd) {
is ExchangeRateState.Data -> {
@ -179,6 +193,7 @@ class RequestViewModel(
exchangeRateUsd.currencyConversion
}
}
else -> {
// Should not happen as the conversion rate related use cases should not be available
Twig.error { "Unexpected screen state" }
@ -186,53 +201,54 @@ class RequestViewModel(
}
}
private fun onAmount(
conversion: FiatCurrencyConversion?,
onAmount: OnAmount
) = viewModelScope.launch {
val newState =
when (onAmount) {
is OnAmount.Number -> {
if (request.value.amountState.amount == defaultAmount) {
// Special case with current value only zero
validateAmountState(conversion, onAmount.number.toString())
} else {
// Adding new number to the result string
validateAmountState(
conversion,
request.value.amountState.amount + onAmount.number
)
private fun onAmount(conversion: FiatCurrencyConversion?, onAmount: OnAmount) {
request.update {
val newState =
when (onAmount) {
is OnAmount.Number -> {
if (it.amountState.amount == defaultAmount) {
// Special case with current value only zero
validateAmountState(conversion, onAmount.number.toString())
} else {
// Adding new number to the result string
validateAmountState(
conversion,
it.amountState.amount + onAmount.number
)
}
}
is OnAmount.Delete -> {
if (it.amountState.amount.length == 1) {
// Deleting up to the last character
AmountState(
amount = defaultAmount,
currency = it.amountState.currency,
isValid = null
)
} else {
validateAmountState(
conversion,
it.amountState.amount.dropLast(1)
)
}
}
is OnAmount.Separator -> {
if (it.amountState.amount.contains(onAmount.separator)) {
// Separator already present
validateAmountState(conversion, it.amountState.amount)
} else {
validateAmountState(
conversion,
it.amountState.amount + onAmount.separator
)
}
}
}
is OnAmount.Delete -> {
if (request.value.amountState.amount.length == 1) {
// Deleting up to the last character
AmountState.Default(defaultAmount, request.value.amountState.currency)
} else {
validateAmountState(
conversion,
request.value.amountState.amount
.dropLast(1)
)
}
}
is OnAmount.Separator -> {
if (request.value.amountState.amount
.contains(onAmount.separator)
) {
// Separator already present
validateAmountState(conversion, request.value.amountState.amount)
} else {
validateAmountState(
conversion,
request.value.amountState.amount + onAmount.separator
)
}
}
}
request.emit(
request.value.copy(amountState = newState)
)
it.copy(amountState = newState)
}
}
// Validates only zeros and decimal separator
@ -252,31 +268,40 @@ class RequestViewModel(
): AmountState {
val newAmount =
if (resultAmount.contains(defaultAmountValidationRegex)) {
AmountState.Default(
AmountState(
// Check for the max decimals in the default (i.e. 0.000) number, too
if (!resultAmount.contains(allowedNumberFormatValidationRegex)) {
amount = if (!resultAmount.contains(allowedNumberFormatValidationRegex)) {
request.value.amountState.amount
} else {
resultAmount
},
request.value.amountState.currency
currency = request.value.amountState.currency,
isValid = null
)
} else if (!resultAmount.contains(allowedNumberFormatValidationRegex)) {
AmountState.Valid(request.value.amountState.amount, request.value.amountState.currency)
AmountState(
amount = request.value.amountState.amount,
currency = request.value.amountState.currency,
isValid = true
)
} else {
AmountState.Valid(resultAmount, request.value.amountState.currency)
AmountState(
amount = resultAmount,
currency = request.value.amountState.currency,
isValid = true
)
}
// Check for max Zcash supply
return newAmount.amount.convertToDouble()?.let { currentValue ->
val zecValue =
if (newAmount.currency == RequestCurrency.Fiat && conversion != null) {
if (newAmount.currency == RequestCurrency.FIAT && conversion != null) {
currentValue / conversion.priceOfZec
} else {
currentValue
}
if (zecValue > MAX_ZCASH_SUPPLY) {
newAmount.copyState(request.value.amountState.amount)
newAmount.copy(amount = request.value.amountState.amount)
} else {
newAmount
}
@ -299,89 +324,81 @@ class RequestViewModel(
}
}
internal fun onBack() =
viewModelScope.launch {
when (stage.value) {
RequestStage.AMOUNT -> {
navigationRouter.back()
}
RequestStage.MEMO -> {
stage.emit(RequestStage.AMOUNT)
}
RequestStage.QR_CODE -> {
when (ReceiveAddressType.fromOrdinal(addressTypeOrdinal)) {
ReceiveAddressType.Transparent -> {
stage.emit(RequestStage.AMOUNT)
}
ReceiveAddressType.Unified, ReceiveAddressType.Sapling -> {
stage.emit(RequestStage.MEMO)
}
}
}
internal fun onBack() {
when (stage.value) {
RequestStage.AMOUNT -> navigationRouter.back()
RequestStage.MEMO -> stage.update { RequestStage.AMOUNT }
RequestStage.QR_CODE -> when (ReceiveAddressType.fromOrdinal(addressTypeOrdinal)) {
ReceiveAddressType.Transparent -> stage.update { RequestStage.AMOUNT }
ReceiveAddressType.Unified, ReceiveAddressType.Sapling -> stage.update { RequestStage.MEMO }
}
}
}
private fun onClose() = navigationRouter.back()
private fun onAmountDone(conversion: FiatCurrencyConversion?) =
viewModelScope.launch {
private fun onAmountDone(conversion: FiatCurrencyConversion?) {
request.update {
val memoAmount =
when (request.value.amountState.currency) {
RequestCurrency.Fiat ->
when (it.amountState.currency) {
RequestCurrency.FIAT ->
if (conversion != null) {
request.value.amountState.toZecString(conversion)
it.amountState.toZecStringFloored(conversion)
} else {
Twig.error { "Unexpected screen state" }
request.value.amountState.amount
it.amountState.amount
}
RequestCurrency.Zec -> request.value.amountState.amount
RequestCurrency.ZEC -> it.amountState.amount
}
request.emit(request.value.copy(memoState = MemoState.new(DEFAULT_MEMO, memoAmount)))
stage.emit(RequestStage.MEMO)
}
private fun onMemoDone(
address: String,
zip321BuildUriUseCase: Zip321BuildUriUseCase
) = viewModelScope.launch {
request.emit(
request.value.copy(
it.copy(memoState = MemoState.new(DEFAULT_MEMO, memoAmount))
}
stage.update { RequestStage.MEMO }
}
private fun onMemoDone(address: String, zip321BuildUriUseCase: Zip321BuildUriUseCase) {
request.update {
it.copy(
qrCodeState =
QrCodeState(
requestUri =
createZip321Uri(
address = address,
amount = request.value.memoState.zecAmount,
memo = request.value.memoState.text,
amount = it.memoState.zecAmount,
memo = it.memoState.text,
zip321BuildUriUseCase = zip321BuildUriUseCase
),
zecAmount = request.value.memoState.zecAmount,
memo = request.value.memoState.text,
zecAmount = it.memoState.zecAmount,
memo = it.memoState.text,
)
)
)
stage.emit(RequestStage.QR_CODE)
}
stage.update { RequestStage.QR_CODE }
}
private fun onAmountAndMemoDone(
address: String,
zip321BuildUriUseCase: Zip321BuildUriUseCase,
conversion: FiatCurrencyConversion?
) = viewModelScope.launch {
val qrCodeAmount =
when (request.value.amountState.currency) {
RequestCurrency.Fiat ->
if (conversion != null) {
request.value.amountState.toZecString(conversion)
} else {
Twig.error { "Unexpected screen state" }
request.value.amountState.amount
}
RequestCurrency.Zec -> request.value.amountState.amount
}
val newRequest =
request.value.copy(
) {
request.update {
val qrCodeAmount =
when (it.amountState.currency) {
RequestCurrency.FIAT ->
if (conversion != null) {
it.amountState.toZecStringFloored(conversion)
} else {
Twig.error { "Unexpected screen state" }
it.amountState.amount
}
RequestCurrency.ZEC -> it.amountState.amount
}
it.copy(
qrCodeState =
QrCodeState(
requestUri =
@ -395,49 +412,39 @@ class RequestViewModel(
memo = DEFAULT_MEMO,
)
)
request.emit(newRequest)
stage.emit(RequestStage.QR_CODE)
}
stage.update { RequestStage.QR_CODE }
}
private fun onSwitch(
conversion: FiatCurrencyConversion?,
onSwitchTo: RequestCurrency
) = viewModelScope.launch {
if (conversion == null) {
return@launch
if (conversion == null) return@launch
request.update {
val newAmount =
when (onSwitchTo) {
RequestCurrency.FIAT -> it.amountState.toFiatString(
context = application.applicationContext,
conversion = conversion
)
RequestCurrency.ZEC -> it.amountState.toZecString(conversion)
}
it.copy(
amountState = if (newAmount.contains(defaultAmountValidationRegex)) {
it.amountState.copy(amount = defaultAmount, currency = onSwitchTo)
} else {
it.amountState.copy(amount = newAmount, currency = onSwitchTo)
}
)
}
val newAmount =
when (onSwitchTo) {
is RequestCurrency.Fiat -> {
request.value.amountState.toFiatString(
application.applicationContext,
conversion
)
}
is RequestCurrency.Zec -> {
request.value.amountState.toZecString(
conversion
)
}
}
// Check default value and shrink it to 0 if necessary
val newState =
if (newAmount.contains(defaultAmountValidationRegex)) {
request.value.amountState.copyState(defaultAmount, onSwitchTo)
} else {
request.value.amountState.copyState(newAmount, onSwitchTo)
}
request.emit(
request.value.copy(amountState = newState)
)
}
private fun onMemo(memoState: MemoState) =
viewModelScope.launch {
request.emit(request.value.copy(memoState = memoState))
}
private fun onMemo(memoState: MemoState) = request.update { it.copy(memoState = memoState) }
private fun createZip321Uri(
address: String,

View File

@ -192,7 +192,8 @@ internal fun WrapSend(
monetarySeparators = monetarySeparators,
value = amountState.value,
fiatValue = amountState.fiatValue,
exchangeRateState = exchangeRateState
exchangeRateState = exchangeRateState,
lastFieldChangedByUser = amountState.lastFieldChangedByUser
)
} else {
AmountState.newFromFiat(
@ -302,6 +303,7 @@ internal fun WrapSend(
onCreateZecSend = { newZecSend ->
viewModel.onCreateZecSendClick(
newZecSend = newZecSend,
amountState = amountState,
setSendStage = setSendStage
)
},

View File

@ -12,6 +12,8 @@ import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookArgs
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
import co.electriccoin.zcash.ui.screen.send.model.AmountField
import co.electriccoin.zcash.ui.screen.send.model.AmountState
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
import co.electriccoin.zcash.ui.screen.send.model.SendAddressBookState
import co.electriccoin.zcash.ui.screen.send.model.SendStage
@ -133,10 +135,11 @@ class SendViewModel(
@Suppress("TooGenericExceptionCaught")
fun onCreateZecSendClick(
newZecSend: ZecSend,
amountState: AmountState,
setSendStage: (SendStage) -> Unit
) = viewModelScope.launch {
try {
createProposal(newZecSend)
createProposal(zecSend = newZecSend, floor = amountState.lastFieldChangedByUser == AmountField.FIAT)
} catch (e: Exception) {
setSendStage(SendStage.SendFailure(e.cause?.message ?: e.message ?: ""))
Twig.error(e) { "Error creating proposal" }

View File

@ -16,16 +16,19 @@ import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
sealed interface AmountState {
val value: String
val fiatValue: String
val lastFieldChangedByUser: AmountField
data class Valid(
override val value: String,
override val fiatValue: String,
val zatoshi: Zatoshi
override val lastFieldChangedByUser: AmountField,
val zatoshi: Zatoshi,
) : AmountState
data class Invalid(
override val value: String,
override val fiatValue: String
override val fiatValue: String,
override val lastFieldChangedByUser: AmountField
) : AmountState
companion object {
@ -37,11 +40,12 @@ sealed interface AmountState {
fiatValue: String,
isTransparentOrTextRecipient: Boolean,
exchangeRateState: ExchangeRateState,
lastFieldChangedByUser: AmountField = AmountField.ZEC
): AmountState {
val isValid = validate(context, monetarySeparators, value)
if (!isValid) {
return Invalid(value, if (value.isBlank()) "" else fiatValue)
return Invalid(value, if (value.isBlank()) "" else fiatValue, lastFieldChangedByUser)
}
val zatoshi = Zatoshi.fromZecString(context, value, Locale.getDefault())
@ -57,8 +61,9 @@ sealed interface AmountState {
// Note that the zero funds sending is supported for sending a memo-only shielded transaction
return when {
(zatoshi == null) -> Invalid(value, if (value.isBlank()) "" else fiatValue)
(zatoshi.value == 0L && isTransparentOrTextRecipient) -> Invalid(value, fiatValue)
(zatoshi == null) -> Invalid(value, if (value.isBlank()) "" else fiatValue, lastFieldChangedByUser)
(zatoshi.value == 0L && isTransparentOrTextRecipient) ->
Invalid(value, fiatValue, lastFieldChangedByUser)
else -> {
Valid(
value = value,
@ -71,7 +76,8 @@ sealed interface AmountState {
currencyConversion = currencyConversion,
locale = Locale.getDefault(),
)
}
},
lastFieldChangedByUser = lastFieldChangedByUser
)
}
}
@ -89,7 +95,11 @@ sealed interface AmountState {
val isValid = validate(context, monetarySeparators, fiatValue)
if (!isValid) {
return Invalid(value = if (fiatValue.isBlank()) "" else value, fiatValue = fiatValue)
return Invalid(
value = if (fiatValue.isBlank()) "" else value,
fiatValue = fiatValue,
lastFieldChangedByUser = AmountField.FIAT
)
}
val zatoshi =
@ -101,16 +111,25 @@ sealed interface AmountState {
return when {
(zatoshi == null) -> {
Invalid(value = if (fiatValue.isBlank()) "" else value, fiatValue = fiatValue)
Invalid(
value = if (fiatValue.isBlank()) "" else value,
fiatValue = fiatValue,
lastFieldChangedByUser = AmountField.FIAT
)
}
(zatoshi.value == 0L && isTransparentOrTextRecipient) -> {
Invalid(if (fiatValue.isBlank()) "" else value, fiatValue)
Invalid(
value = if (fiatValue.isBlank()) "" else value,
fiatValue = fiatValue,
lastFieldChangedByUser = AmountField.FIAT
)
}
else -> {
Valid(
value = zatoshi.toZecString(),
zatoshi = zatoshi,
fiatValue = fiatValue
fiatValue = fiatValue,
lastFieldChangedByUser = AmountField.FIAT
)
}
}
@ -122,6 +141,7 @@ sealed interface AmountState {
private const val KEY_VALUE = "value" // $NON-NLS
private const val KEY_FIAT_VALUE = "fiat_value" // $NON-NLS
private const val KEY_ZATOSHI = "zatoshi" // $NON-NLS
private const val KEY_LAST_FIELD_CHANGED_BY_USER = "last_field_changed_by_user" // $NON-NLS
private fun validate(
context: Context,
@ -145,18 +165,22 @@ sealed interface AmountState {
val amountString = (it[KEY_VALUE] as String)
val fiatAmountString = (it[KEY_FIAT_VALUE] as String)
val type = (it[KEY_TYPE] as String)
val lastFieldChangedByUser =
AmountField.valueOf(it[KEY_LAST_FIELD_CHANGED_BY_USER] as String)
when (type) {
TYPE_VALID ->
Valid(
value = amountString,
fiatValue = fiatAmountString,
zatoshi = Zatoshi(it[KEY_ZATOSHI] as Long)
zatoshi = Zatoshi(it[KEY_ZATOSHI] as Long),
lastFieldChangedByUser = lastFieldChangedByUser
)
TYPE_INVALID ->
Invalid(
value = amountString,
fiatValue = fiatAmountString
fiatValue = fiatAmountString,
lastFieldChangedByUser = lastFieldChangedByUser
)
else -> null
@ -178,8 +202,11 @@ sealed interface AmountState {
}
saverMap[KEY_VALUE] = this.value
saverMap[KEY_FIAT_VALUE] = this.fiatValue
saverMap[KEY_LAST_FIELD_CHANGED_BY_USER] = this.lastFieldChangedByUser.name
return saverMap
}
}
}
enum class AmountField { ZEC, FIAT }

View File

@ -78,6 +78,7 @@ import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetState
import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.send.model.AmountField
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
@ -104,7 +105,8 @@ private fun PreviewSendForm() {
AmountState.Valid(
value = ZatoshiFixture.ZATOSHI_LONG.toString(),
fiatValue = "",
zatoshi = ZatoshiFixture.new()
zatoshi = ZatoshiFixture.new(),
lastFieldChangedByUser = AmountField.FIAT
),
setMemoState = {},
memoState = MemoState.new("Test message "),
@ -143,7 +145,8 @@ private fun SendFormTransparentAddressPreview() {
AmountState.Valid(
value = ZatoshiFixture.ZATOSHI_LONG.toString(),
fiatValue = "",
zatoshi = ZatoshiFixture.new()
zatoshi = ZatoshiFixture.new(),
lastFieldChangedByUser = AmountField.FIAT
),
setMemoState = {},
memoState = MemoState.new("Test message"),
@ -692,7 +695,7 @@ fun SendFormAmountTextField(
}
)
if (exchangeRateState is ExchangeRateState.Data) {
if (exchangeRateState is ExchangeRateState.Data && exchangeRateState.currencyConversion != null) {
Spacer(modifier = Modifier.width(12.dp))
Image(
modifier = Modifier.padding(top = 12.dp),