Dust notes
This commit is contained in:
parent
0d30109a25
commit
fe43bcad1c
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in New Issue