Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-19 21:58:22 +03:00
parent a8dd36e98c
commit 9e399946d7
17 changed files with 304 additions and 198 deletions

View file

@ -1,23 +1,36 @@
package com.tangem.tap.common
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.scaleToFiat
import java.math.BigDecimal
import java.math.RoundingMode
/**
[REDACTED_AUTHOR]
*/
class CurrencyConverter(var rateValue: BigDecimal) {
class CurrencyConverter(
private val rateValue: BigDecimal,
private val decimals: Int
) {
private val roundingMode = RoundingMode.DOWN
fun toFiat(crypto: BigDecimal, decimals: Int = 2): BigDecimal {
return rateValue.multiply(crypto).setScale(decimals, RoundingMode.DOWN)
fun toFiat(crypto: BigDecimal, fiatDecimals: Int = 2): BigDecimal {
return toFiatUnscaled(crypto).setScale(fiatDecimals, roundingMode)
}
fun toCrypto(fiat: BigDecimal, decimals: Int): BigDecimal {
if (fiat.isZero() || rateValue.isZero()) return fiat
fun toFiatUnscaled(crypto: BigDecimal): BigDecimal {
return rateValue.multiply(crypto).setScale(decimals, roundingMode)
}
val scaledRateValue = rateValue.setScale(decimals, RoundingMode.DOWN)
val scaledFiat = fiat.setScale(decimals, RoundingMode.DOWN)
fun toFiatWithPrecision(crypto: BigDecimal): BigDecimal {
return toFiatUnscaled(crypto).scaleToFiat(true)
}
fun toCrypto(fiat: BigDecimal): BigDecimal {
if (fiat.isZero()) return fiat
val scaledRateValue = rateValue.setScale(decimals, roundingMode)
val scaledFiat = fiat.setScale(decimals, roundingMode)
return scaledFiat.divide(scaledRateValue, RoundingMode.UP)
}
}

View file

@ -6,6 +6,7 @@ import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.math.BigDecimal
@ -58,8 +59,28 @@ fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: FiatCurrenc
return "≈ ${fiatCurrencyName} $fiatValue"
}
fun FiatCurrency.toFormattedString(): String = "${this.name} (${this.symbol}) - ${this.sign}"
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
// 0.00 -> 0.00
// 0.00002345 -> 0.00002
// 1.00002345 -> 1.00
// 1.45002345 -> 1.45
fun BigDecimal.scaleToFiat(applyPrecision: Boolean = false): BigDecimal {
if (this.isZero()) return this
val scaledFiat = this.setScale(2, RoundingMode.DOWN)
return if (scaledFiat.isZero() && applyPrecision) this.setPrecision(1)
else scaledFiat
}
fun BigDecimal.setPrecision(precision: Int, roundingMode: RoundingMode = RoundingMode.DOWN): BigDecimal {
if (precision == precision() || scale() <= precision) return this
return this.setScale(scale() - precision() + precision, roundingMode)
}
fun BigDecimal.isPositive(): Boolean = this.compareTo(BigDecimal.ZERO) == 1
fun BigDecimal.isNegative(): Boolean = this.compareTo(BigDecimal.ZERO) == -1
fun BigDecimal.isGreaterThan(value: BigDecimal): Boolean = this.compareTo(value) == 1
@ -73,6 +94,4 @@ fun BigDecimal.isGreaterThanOrEqual(value: BigDecimal): Boolean {
fun BigDecimal.isLessThanOrEqual(value: BigDecimal): Boolean {
val compareResult = this.compareTo(value)
return compareResult == -1 || compareResult == 0
}
fun FiatCurrency.toFormattedString(): String = "${this.name} (${this.symbol}) - ${this.sign}"
}

View file

@ -3,7 +3,9 @@ package com.tangem.tap.common.redux
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.assembleErrorIds
import com.tangem.tap.notificationsHandler
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -39,19 +41,30 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
Toast.makeText(it.context, it.context.getString(message), Toast.LENGTH_LONG).show()
}
}
fun showNotification(errorList: List<Int>, builder: (List<String>) -> String) {
val context = baseLayout.get()?.context ?: return
val message = builder(errorList.map { context.getString(it) })
showNotification(message)
}
}
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is NotificationAction) {
notificationsHandler?.showNotification(action.messageResource)
}
if (action is ToastNotificationAction) {
notificationsHandler?.showToastNotification(action.messageResource)
}
if (action is ErrorAction) {
notificationsHandler?.showNotification(action.error.localizedMessage)
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrorIds(), multiError.builder)
}
else -> notificationsHandler?.showNotification(action.error.localizedMessage)
}
}
}
next(action)
}

View file

@ -3,13 +3,39 @@ package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.wallet.R
sealed class TapError(@StringRes val localizedMessage: Int): Throwable() {
object PayIdAlreadyCreated: TapError(R.string.error_payid_already_created)
object PayIdCreatingError: TapError(R.string.error_creating_payid)
object PayIdEmptyField: TapError(R.string.wallet_create_payid_empty)
object UnknownBlockchain: TapError(R.string.wallet_unknown_blockchain)
object NoInternetConnection: TapError(R.string.notification_no_internet)
object InsufficientBalance: TapError(R.string.error_insufficient_balance)
object BlockchainInternalError: TapError(R.string.error_blockchain_internal)
object UnknownError: TapError(R.string.error_unknown)
interface TapErrors
interface MultiMessageError : TapErrors {
val errorList: List<TapError>
val builder: (List<String>) -> String
}
sealed class TapError(@StringRes val localizedMessage: Int) : Throwable(), TapErrors {
object UnknownError : TapError(R.string.error_unknown)
object PayIdAlreadyCreated : TapError(R.string.error_payid_already_created)
object PayIdCreatingError : TapError(R.string.error_creating_payid)
object PayIdEmptyField : TapError(R.string.wallet_create_payid_empty)
object UnknownBlockchain : TapError(R.string.wallet_unknown_blockchain)
object NoInternetConnection : TapError(R.string.notification_no_internet)
object InsufficientBalance : TapError(R.string.error_insufficient_balance)
object BlockchainInternalError : TapError(R.string.error_blockchain_internal)
object AmountExceedsBalance : TapError(R.string.amount_exceeds_balance)
object FeeExceedsBalance : TapError(R.string.fee_exceeds_balance)
object TotalExceedsBalance : TapError(R.string.total_exceeds_balance)
object InvalidAmountValue : TapError(R.string.invalid_amount_value)
object InvalidFeeValue : TapError(R.string.invalid_fee_value)
object DustAmount : TapError(R.string.dust_amount)
object DustChang : TapError(R.string.dust_change)
data class ValidateTransactionErrors(
override val errorList: List<TapError>,
override val builder: (List<String>) -> String
) : TapError(-1), MultiMessageError
}
fun TapErrors.assembleErrorIds(): MutableList<Int> {
val idList = mutableListOf<Int>()
when (this) {
is MultiMessageError -> this.errorList.forEach { idList.addAll(it.assembleErrorIds()) }
is TapError -> idList.add(this.localizedMessage)
}
return idList
}

View file

@ -64,14 +64,9 @@ sealed class AmountActionUi : SendScreenActionUi {
}
sealed class AmountAction : SendScreenAction {
enum class Error {
FEE_GREATER_THAN_AMOUNT,
AMOUNT_WITH_FEE_GREATER_THAN_BALANCE
}
sealed class AmountVerification : AmountAction() {
data class SetAmount(val amount: BigDecimal) : AmountVerification()
data class SetError(val amount: BigDecimal, val error: Error) : AmountVerification()
data class SetError(val amount: BigDecimal, val error: TapError) : AmountVerification()
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.extensions.isGreaterThanOrEqual
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionError
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.store
import org.rekotlin.Action
import java.math.BigDecimal
@ -19,81 +19,47 @@ class AmountMiddleware {
fun handle(rawData: String?, appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val rawData = rawData ?: store.state.sendState.amountState.viewAmountValue
val data = if (rawData == ".") "0.0" else rawData
val proposedAmountValue = when {
val inputValue = when {
data.isEmpty() || data == "0" -> BigDecimal.ZERO
else -> BigDecimal(data)
}
val balanceCrypto = sendState.amount?.value ?: BigDecimal.ZERO
val amountChecker = when (sendState.amountState.mainCurrency.value) {
MainCurrencyType.FIAT -> FiatAmountChecker(balanceCrypto, sendState.currencyConverter)
MainCurrencyType.CRYPTO -> CryptoAmountChecker(balanceCrypto)
}
if (inputValue.isZero() && sendState.amountState.amountToSendCrypto.isZero()) return
val checkResult = amountChecker.check(
proposedAmountValue,
sendState.feeState.getCurrentFee(),
sendState.feeState.feeIsIncluded
)
if (checkResult.error == null) {
dispatch(AmountAction.AmountVerification.SetAmount(checkResult.amount))
val inputCrypto = sendState.convertInputValueToCrypto(inputValue)
val fee = sendState.feeState.getCurrentFee()
val needToExtractFee = sendState.amountState.isCoinAmount() && sendState.feeState.feeIsIncluded
val totalToSend = if (needToExtractFee) inputCrypto.minus(fee) else inputCrypto
val feeAmount = Amount(fee, walletManager.wallet.blockchain)
val totalAmount = Amount(totalToSend, walletManager.wallet.blockchain)
val transactionErrors = walletManager.validateTransaction(totalAmount, feeAmount)
if (transactionErrors.isEmpty()) {
dispatch(AmountAction.AmountVerification.SetAmount(inputValue))
} else {
dispatch(AmountAction.AmountVerification.SetError(checkResult.amount, checkResult.error))
val tapErrors = transactionErrors.map {
when (it) {
TransactionError.AmountExceedsBalance -> TapError.AmountExceedsBalance
TransactionError.FeeExceedsBalance -> TapError.FeeExceedsBalance
TransactionError.TotalExceedsBalance -> TapError.TotalExceedsBalance
TransactionError.InvalidAmountValue -> TapError.InvalidAmountValue
TransactionError.InvalidFeeValue -> TapError.InvalidFeeValue
TransactionError.DustAmount -> TapError.DustAmount
TransactionError.DustChange -> TapError.DustChang
else -> TapError.UnknownError
}
}
val error = TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
dispatch(AmountAction.AmountVerification.SetError(inputValue, error))
}
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
}
}
interface AmountChecker {
data class Result(val amount: BigDecimal, val error: AmountAction.Error? = null)
fun check(value: BigDecimal, feeCrypto: BigDecimal, feeIsIncluded: Boolean): Result
}
abstract class BaseAmountChecker(
protected val balanceCrypto: BigDecimal
) : AmountChecker {
override fun check(value: BigDecimal, feeCrypto: BigDecimal, feeIsIncluded: Boolean): AmountChecker.Result {
val convertedFee = convert(feeCrypto)
val convertedBalance = convert(balanceCrypto)
if (feeIsIncluded) {
return if (value.isGreaterThan(convertedFee)) {
if (convertedBalance.isGreaterThanOrEqual(value)) {
AmountChecker.Result(value)
} else {
AmountChecker.Result(value, AmountAction.Error.FEE_GREATER_THAN_AMOUNT)
}
} else {
AmountChecker.Result(value, AmountAction.Error.AMOUNT_WITH_FEE_GREATER_THAN_BALANCE)
}
} else {
val amountWithFee = value.plus(convertedFee)
return if (convertedBalance.isGreaterThanOrEqual(amountWithFee)) {
AmountChecker.Result(value)
} else {
AmountChecker.Result(value, AmountAction.Error.AMOUNT_WITH_FEE_GREATER_THAN_BALANCE)
}
}
}
protected abstract fun convert(value: BigDecimal): BigDecimal
}
class CryptoAmountChecker(
balanceCrypto: BigDecimal
) : BaseAmountChecker(balanceCrypto) {
override fun convert(value: BigDecimal): BigDecimal = value
}
class FiatAmountChecker(
balanceCrypto: BigDecimal,
private val converter: CurrencyConverter
) : BaseAmountChecker(balanceCrypto) {
override fun convert(value: BigDecimal): BigDecimal = converter.toFiat(value)
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.extensions.isNegative
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.AmountAction
@ -11,8 +10,6 @@ import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.Value
import com.tangem.tap.store
import java.math.BigDecimal
/**
@ -26,7 +23,6 @@ class AmountReducer : SendInternalReducer {
}
private fun handleUiAction(action: AmountActionUi, sendState: SendState, state: AmountState): SendState {
val converter = sendState.currencyConverter
val result = when (action) {
is ToggleMainCurrency -> {
val type = if (state.mainCurrency.value == MainCurrencyType.FIAT) MainCurrencyType.CRYPTO
@ -38,21 +34,21 @@ class AmountReducer : SendInternalReducer {
when (action.mainCurrency) {
MainCurrencyType.FIAT -> {
val fiatToSend = if (state.amountToSendCrypto.isZero()) BigDecimal.ZERO
else converter.toFiat(state.amountToSendCrypto)
else sendState.convertToFiat(state.amountToSendCrypto)
val rescaledBalance = sendState.convertToFiat(state.balanceCrypto, true)
state.copy(
viewAmountValue = fiatToSend.stripZeroPlainString(),
viewBalanceValue = converter.toFiat(state.balanceCrypto).stripZeroPlainString(),
mainCurrency = Value(MainCurrencyType.FIAT, store.state.globalState.appCurrency),
viewBalanceValue = rescaledBalance.stripZeroPlainString(),
mainCurrency = state.createMainCurrencyValue(action.mainCurrency),
maxLengthOfAmount = sendState.getDecimals(action.mainCurrency),
cursorAtTheSamePosition = false
)
}
MainCurrencyType.CRYPTO -> {
val mainCurrency = Value(MainCurrencyType.CRYPTO, sendState.amount?.currencySymbol ?: "null")
state.copy(
viewAmountValue = state.amountToSendCrypto.stripZeroPlainString(),
viewBalanceValue = state.balanceCrypto.stripZeroPlainString(),
mainCurrency = mainCurrency,
mainCurrency = state.createMainCurrencyValue(action.mainCurrency),
maxLengthOfAmount = sendState.getDecimals(action.mainCurrency),
cursorAtTheSamePosition = false
)
@ -69,7 +65,7 @@ class AmountReducer : SendInternalReducer {
}
val etFieldValue = if (state.mainCurrency.value == MainCurrencyType.CRYPTO) maxAmount
else converter.toFiat(maxAmount)
else sendState.convertToFiat(maxAmount)
state.copy(
viewAmountValue = etFieldValue.stripZeroPlainString(),
amountToSendCrypto = maxAmount,
@ -83,25 +79,21 @@ class AmountReducer : SendInternalReducer {
}
private fun handleAction(action: AmountAction, sendState: SendState, state: AmountState): SendState {
val decimals = sendState.amount?.decimals ?: return sendState
val result = when (action) {
is AmountAction.AmountVerification.SetAmount -> {
setAmount(sendState.currencyConverter, decimals, action.amount, state)
setAmount(sendState, action.amount, state)
}
is AmountAction.AmountVerification.SetError -> {
setAmount(sendState.currencyConverter, decimals, action.amount, state).copy(error = action.error)
setAmount(sendState, action.amount, state).copy(error = action.error)
}
}
return updateLastState(sendState.copy(amountState = result), result)
}
private fun setAmount(converter: CurrencyConverter, decimals: Int, amount: BigDecimal, state: AmountState): AmountState {
private fun setAmount(sendState: SendState, amount: BigDecimal, state: AmountState): AmountState {
return when (state.mainCurrency.value) {
MainCurrencyType.FIAT -> {
val amountCrypto = converter.toCrypto(amount, decimals).stripTrailingZeros()
val amountCrypto = sendState.convertToCrypto(amount)
state.copy(
viewAmountValue = amount.stripZeroPlainString(),
amountToSendCrypto = amountCrypto,

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.extensions.scaleToFiat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
@ -24,20 +25,21 @@ class ReceiptReducer : SendInternalReducer {
private fun handleRefresh(action: RefreshReceipt, sendState: SendState, state: ReceiptState): SendState {
val wallet = sendState.walletManager?.wallet ?: return sendState
val converter = sendState.currencyConverter
val coinConverter = sendState.coinConverter
val tokenConverter = sendState.tokenConverter
val amountState = sendState.amountState
val feeState = sendState.feeState
val layoutType = determineLayoutType(amountState.mainCurrency.value, amountState.typeOfAmount)
val layoutType = determineLayoutType(amountState.mainCurrency.value, sendState.amountState.typeOfAmount)
val symbols = determineSymbols(wallet)
val showBlank = !sendState.isReadyToSend()
val result = state.copy(
visibleTypeOfReceipt = layoutType,
mainCurrencyType = sendState.amountState.mainCurrency,
fiat = createFiatType(converter, amountState, feeState, symbols, showBlank),
crypto = createCryptoType(converter, amountState, feeState, symbols, showBlank),
tokenFiat = createTokenFiatType(converter, amountState, feeState, symbols, showBlank),
tokenCrypto = createTokenCryptoType(converter, amountState, feeState, symbols, showBlank)
fiat = createFiatType(coinConverter, amountState, feeState, symbols, showBlank),
crypto = createCryptoType(coinConverter, amountState, feeState, symbols, showBlank),
tokenFiat = createTokenFiatType(coinConverter, tokenConverter, amountState, feeState, symbols, showBlank),
tokenCrypto = createTokenCryptoType(coinConverter, tokenConverter, amountState, feeState, symbols, showBlank)
)
return updateLastState(sendState.copy(receiptState = result), result)
}
@ -50,15 +52,15 @@ class ReceiptReducer : SendInternalReducer {
showBlank: Boolean
): ReceiptFiat {
val feeCrypto = feeState.getCurrentFee()
val feeFiat = converter.toFiat(feeCrypto)
val feeFiat = converter.toFiatWithPrecision(feeCrypto)
if (showBlank) {
return ReceiptFiat("0", feeFiat.stripZeroPlainString(), feeFiat.stripZeroPlainString(), "0", symbols)
return ReceiptFiat("0", feeFiat.stripZeroPlainString(), "0", "0", symbols)
}
return if (feeState.feeIsIncluded) {
val amountFiat = converter.toFiat(amountState.amountToSendCrypto.minus(feeCrypto))
val totalFiat = converter.toFiat(amountState.amountToSendCrypto)
val amountFiat = converter.toFiatWithPrecision(amountState.amountToSendCrypto.minus(feeCrypto))
val totalFiat = converter.toFiatWithPrecision(amountState.amountToSendCrypto)
ReceiptFiat(
amountFiat = amountFiat.stripZeroPlainString(),
feeFiat = feeFiat.stripZeroPlainString(),
@ -68,8 +70,8 @@ class ReceiptReducer : SendInternalReducer {
)
} else {
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
val amountFiat = converter.toFiat(amountState.amountToSendCrypto)
val totalFiat = converter.toFiat(totalAmountCrypto)
val amountFiat = converter.toFiatWithPrecision(amountState.amountToSendCrypto)
val totalFiat = converter.toFiatWithPrecision(totalAmountCrypto)
ReceiptFiat(
amountFiat = amountFiat.stripZeroPlainString(),
feeFiat = feeFiat.stripZeroPlainString(),
@ -88,9 +90,9 @@ class ReceiptReducer : SendInternalReducer {
showBlank: Boolean
): ReceiptCrypto {
val feeCrypto = feeState.getCurrentFee()
val feeFiat = converter.toFiatWithPrecision(feeCrypto)
if (showBlank) {
return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", converter.toFiat(feeCrypto).stripZeroPlainString(), "0", symbols)
return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols)
}
if (feeState.feeIsIncluded) {
@ -98,8 +100,8 @@ class ReceiptReducer : SendInternalReducer {
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeFiat = converter.toFiat(feeCrypto).stripZeroPlainString(),
willSentFiat = converter.toFiat(amountState.amountToSendCrypto).stripZeroPlainString(),
feeFiat = feeFiat.stripZeroPlainString(),
willSentFiat = converter.toFiatWithPrecision(amountState.amountToSendCrypto).stripZeroPlainString(),
symbols = symbols
)
} else {
@ -108,7 +110,7 @@ class ReceiptReducer : SendInternalReducer {
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = converter.toFiat(feeCrypto).stripZeroPlainString(),
feeFiat = feeFiat.stripZeroPlainString(),
willSentFiat = converter.toFiat(totalCrypto).stripZeroPlainString(),
symbols = symbols
)
@ -116,47 +118,55 @@ class ReceiptReducer : SendInternalReducer {
}
private fun createTokenFiatType(
converter: CurrencyConverter,
coinConverter: CurrencyConverter,
tokenConverter: CurrencyConverter,
amountState: AmountState,
feeState: FeeState,
symbols: ReceiptSymbols,
showBlank: Boolean
): ReceiptTokenFiat {
val feeCrypto = feeState.getCurrentFee()
val amountFiat = converter.toFiat(amountState.amountToSendCrypto)
val feeFiat = converter.toFiat(feeCrypto)
val feeCoin = feeState.getCurrentFee()
val feeFiat = coinConverter.toFiatWithPrecision(feeCoin)
if (showBlank) {
return ReceiptTokenFiat("0", feeFiat.stripZeroPlainString(), "0", "0", "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
val amountFiat = tokenConverter.toFiatUnscaled(tokensToSend)
val totalFiat = amountFiat.plus(feeFiat)
return ReceiptTokenFiat(
amountFiat = amountFiat.stripZeroPlainString(),
amountFiat = amountFiat.scaleToFiat().stripZeroPlainString(),
feeFiat = feeFiat.stripZeroPlainString(),
totalFiat = amountFiat.plus(feeFiat).stripZeroPlainString(),
willSentTokenCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
willSentFeeCrypto = feeCrypto.stripZeroPlainString(),
totalFiat = totalFiat.scaleToFiat().stripZeroPlainString(),
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
)
}
private fun createTokenCryptoType(
converter: CurrencyConverter,
coinConverter: CurrencyConverter,
tokenConverter: CurrencyConverter,
amountState: AmountState,
feeState: FeeState,
symbols: ReceiptSymbols,
showBlank: Boolean
): ReceiptTokenCrypto {
val feeCrypto = feeState.getCurrentFee()
val totalFiat = converter.toFiat(amountState.amountToSendCrypto).plus(converter.toFiat(feeCrypto))
val feeCoin = feeState.getCurrentFee()
if (showBlank) {
return ReceiptTokenCrypto("0", feeCrypto.stripZeroPlainString(), "0", symbols)
return ReceiptTokenCrypto("0", feeCoin.stripZeroPlainString(), "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
val tokenFiat = tokenConverter.toFiatUnscaled(tokensToSend)
val feeFiat = coinConverter.toFiatUnscaled(feeCoin)
val totalFiat = tokenFiat.plus(feeFiat)
return ReceiptTokenCrypto(
amountToken = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalFiat = totalFiat.stripZeroPlainString(),
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
totalFiat = totalFiat.scaleToFiat().stripZeroPlainString(),
symbols = symbols
)
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.states.IdStateHolder
@ -54,19 +54,34 @@ private class EmptyReducer : SendInternalReducer {
private class PrepareSendScreenStatesReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val amount = (action as PrepareSendScreen).tokenAmount ?: action.coinAmount
val prepareAction = action as PrepareSendScreen
val walletManager = store.state.globalState.scanNoteResponse!!.walletManager!!
val walletAmount = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = walletAmount.decimals
val coinConverter = createCurrencyConverter(walletManager.wallet.blockchain.currency, decimals)
val tokenConverter = createCurrencyConverter(prepareAction.tokenAmount?.currencySymbol ?: "", decimals)
if (coinConverter == null || (walletAmount.type == AmountType.Token && tokenConverter == null)) {
return sendState.copy(hasInitializationError = true)
}
return sendState.copy(
amount = amount,
walletManager = walletManager,
currencyConverter = createCurrencyConverter(walletManager),
amountState = sendState.amountState.copy(balanceCrypto = amount?.value ?: BigDecimal.ZERO)
coinConverter = coinConverter,
tokenConverter = tokenConverter ?: CurrencyConverter(BigDecimal.ONE, decimals),
amountState = sendState.amountState.copy(
walletAmount = walletAmount,
typeOfAmount = walletAmount.type,
balanceCrypto = walletAmount.value ?: BigDecimal.ZERO
),
feeState = sendState.feeState.copy(
includeFeeSwitcherIsEnabled = walletAmount.type == AmountType.Coin
)
)
}
private fun createCurrencyConverter(walletManager: WalletManager): CurrencyConverter {
val rate = store.state.globalState.conversionRates.getRate(walletManager.wallet.blockchain.currency)
return if (rate == null) CurrencyConverter(BigDecimal.ONE) else CurrencyConverter(rate)
private fun createCurrencyConverter(currency: String, decimals: Int): CurrencyConverter? {
val rate = store.state.globalState.conversionRates.getRate(currency)
return if (rate == null) null else CurrencyConverter(rate, decimals)
}
}

View file

@ -9,10 +9,11 @@ data class AddressPayIdState(
val recipientWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val pasteIsEnabled: Boolean = false,
override val stateId: StateId = StateId.ADDRESS_PAY_ID
val pasteIsEnabled: Boolean = false
) : SendScreenState {
override val stateId: StateId = StateId.ADDRESS_PAY_ID
fun isReady(): Boolean = error == null && recipientWalletAddress?.isNotEmpty() ?: false
fun isPayIdState(): Boolean = recipientWalletAddress != null && recipientWalletAddress != normalFieldValue

View file

@ -19,9 +19,12 @@ data class FeeState(
val mainLayoutIsVisible: Boolean = false,
val controlsLayoutIsVisible: Boolean = false,
val feeChipGroupIsVisible: Boolean = true,
val includeFeeSwitcherIsEnabled: Boolean = true,
val error: FeeAction.Error? = null,
override val stateId: StateId = StateId.FEE
) : SendScreenState {
override val stateId: StateId = StateId.FEE
fun isReady(): Boolean = error == null && currentFee != null
fun getCurrentFee(): BigDecimal = currentFee?.value?.value ?: BigDecimal.ZERO

View file

@ -11,9 +11,10 @@ data class ReceiptState(
val crypto: ReceiptCrypto? = null,
val tokenFiat: ReceiptTokenFiat? = null,
val tokenCrypto: ReceiptTokenCrypto? = null,
val mainCurrencyType: Value<MainCurrencyType>? = null,
override val stateId: StateId = StateId.RECEIPT
) : SendScreenState
val mainCurrencyType: Value<MainCurrencyType>? = null
) : SendScreenState {
override val stateId: StateId = StateId.RECEIPT
}
data class ReceiptSymbols(
val fiat: String,
@ -40,7 +41,7 @@ data class ReceiptCrypto(
data class ReceiptTokenCrypto(
val amountToken: String,
val feeCrypto: String,
val feeCoin: String,
val totalFiat: String,
val symbols: ReceiptSymbols
)
@ -49,7 +50,7 @@ data class ReceiptTokenFiat(
val amountFiat: String,
val feeFiat: String,
val totalFiat: String,
val willSentTokenCrypto: String,
val willSentFeeCrypto: String,
val willSentToken: String,
val willSentFeeCoin: String,
val symbols: ReceiptSymbols
)

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
@ -26,18 +26,20 @@ enum class StateId {
interface SendScreenState : StateType, IdStateHolder
data class SendState(
val amount: Amount? = null,
val walletManager: WalletManager? = null,
val currencyConverter: CurrencyConverter = CurrencyConverter(BigDecimal.ONE),
val coinConverter: CurrencyConverter = CurrencyConverter(BigDecimal.ONE, 2),
val tokenConverter: CurrencyConverter = CurrencyConverter(BigDecimal.ONE, 2),
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
val receiptState: ReceiptState = ReceiptState(),
val sendButtonState: SendButtonState = SendButtonState.DISABLED,
override val stateId: StateId = StateId.SEND_SCREEN
val hasInitializationError: Boolean = false
) : SendScreenState {
override val stateId: StateId = StateId.SEND_SCREEN
fun isReadyToSend(): Boolean {
val sendState = store.state.sendState
return addressPayIdIsReady() && sendState.amountState.isReady() && sendState.feeState.isReady()
@ -47,7 +49,28 @@ data class SendState(
fun getDecimals(type: MainCurrencyType): Int = when (type) {
MainCurrencyType.FIAT -> 2
MainCurrencyType.CRYPTO -> amount?.decimals ?: 0
MainCurrencyType.CRYPTO -> amountState.walletAmount?.decimals ?: 0
}
fun getConverter(): CurrencyConverter {
return if (amountState.typeOfAmount == AmountType.Coin) coinConverter
else tokenConverter
}
fun convertToCrypto(value: BigDecimal): BigDecimal {
return getConverter().toCrypto(value)
}
fun convertToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
val converter = getConverter()
return if (scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
fun convertInputValueToCrypto(inputValue: BigDecimal): BigDecimal {
return when (amountState.mainCurrency.value) {
MainCurrencyType.FIAT -> convertToCrypto(inputValue)
MainCurrencyType.CRYPTO -> inputValue
}
}
fun getButtonState(): SendButtonState = if (isReadyToSend()) SendButtonState.ENABLED else SendButtonState.DISABLED
@ -58,18 +81,30 @@ enum class SendButtonState {
}
data class AmountState(
val walletAmount: Amount? = null,
val typeOfAmount: AmountType = AmountType.Coin,
val viewAmountValue: String = BigDecimal.ZERO.toPlainString(),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.DEFAULT_FIAT_CURRENCY),
val typeOfAmount: AmountType = AmountType.Coin,
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
val cursorAtTheSamePosition: Boolean = true,
val maxLengthOfAmount: Int = 2,
val error: AmountAction.Error? = null,
override val stateId: StateId = StateId.AMOUNT
val error: TapError? = null
) : SendScreenState {
override val stateId: StateId = StateId.AMOUNT
fun isReady(): Boolean = error == null && !amountToSendCrypto.isZero()
fun isCoinAmount(): Boolean = typeOfAmount == AmountType.Coin
fun createMainCurrencyValue(type: MainCurrencyType): Value<MainCurrencyType> {
return when (type) {
MainCurrencyType.FIAT -> Value(type, store.state.globalState.appCurrency)
MainCurrencyType.CRYPTO -> Value(type, walletAmount?.currencySymbol ?: "NONE")
}
}
}
enum class MainCurrencyType {

View file

@ -16,6 +16,7 @@ import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
import com.tangem.tap.common.toggleWidget.*
@ -57,6 +58,11 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (store.state.sendState.hasInitializationError) {
store.dispatch(NavigationAction.PopBackTo())
return
}
initSendButtonStates()
setupAddressOrPayIdLayout()
setupAmountLayout()
@ -187,6 +193,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
override fun subscribeToStore() {
if (store.state.sendState.hasInitializationError) return
store.subscribe(sendSubscriber) { appState ->
appState.skipRepeats { oldState, newState ->
oldState.sendState == newState.sendState

View file

@ -10,9 +10,10 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.update
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.common.toggleWidget.ProgressState
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrorIds
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
@ -96,19 +97,21 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
private fun handleAmountState(fg: BaseStoreFragment, state: AmountState) {
when (state.error) {
AmountAction.Error.FEE_GREATER_THAN_AMOUNT -> {
fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(true, fg.getString(R.string.error_fee_greater_than_amount))
}
AmountAction.Error.AMOUNT_WITH_FEE_GREATER_THAN_BALANCE -> {
fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(true, fg.getString(R.string.error_amount_with_fee_greater_than_balance))
}
null -> {
if (fg.tilAmountToSend.isErrorEnabled) fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(false)
if (state.error != null) {
val context = fg.requireContext()
val message = when (state.error) {
is MultiMessageError -> {
val multiError = state.error as MultiMessageError
val messageList = multiError.assembleErrorIds().map { context.getString(it) }
multiError.builder(messageList)
}
else -> context.getString(state.error.localizedMessage)
}
fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(true, message)
} else {
if (fg.tilAmountToSend.isErrorEnabled) fg.amountContainer.parent?.beginDelayedTransition()
fg.tilAmountToSend.enableError(false)
}
fg.etAmountToSend.filters = arrayOf(DecimalDigitsInputFilter(12, state.maxLengthOfAmount))
@ -162,6 +165,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
}
fg.swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled
if (fg.swIncludeFee.isChecked != state.feeIsIncluded) {
fg.swIncludeFee.isChecked = state.feeIsIncluded
}
@ -182,6 +186,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val totalTokenLayout = fg.flTotalTokenCrypto as ViewGroup
fun getString(id: Int): String = mainLayout.context.getString(id)
val rough = getString(R.string.sign_rough)
when (state.visibleTypeOfReceipt) {
ReceiptLayoutType.FIAT -> {
val receipt = state.fiat ?: return
@ -190,7 +195,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalTokenLayout.show(false)
fg.tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
fg.tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
totalLayout.tvTotalValue.update("${receipt.totalFiat} ${receipt.symbols.fiat}")
totalLayout.tvTotalValue.update("$rough ${receipt.totalFiat} ${receipt.symbols.fiat}")
val willSent = SpannableStringBuilder()
.bold { append(receipt.willSentCrypto) }.append(" ")
@ -210,7 +215,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val willSent = SpannableStringBuilder()
.bold {
append(getString(R.string.sign_rough)).append(" ")
append(rough).append(" ")
append(receipt.willSentFiat).append(" ")
append(receipt.symbols.fiat)
append(" (fee: ${receipt.feeFiat} ")
@ -231,11 +236,11 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
.bold {
append(receipt.symbols.token)
append(" ")
append(receipt.willSentFeeCrypto)
append(receipt.willSentToken)
}.append(" ").append(getString(R.string.generic_and)).append(" ")
.bold {
append(receipt.symbols.crypto).append(" ")
append(receipt.willSentFeeCrypto).append(" ")
append(receipt.willSentFeeCoin).append(" ")
}
.append(mainLayout.context.getString(R.string.send_total_will_be_sent))
totalLayout.tvWillBeSentValue.update(willSent.toString())
@ -247,7 +252,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalTokenLayout.show(true)
fg.tvReceiptAmountValue.update("${receipt.amountToken} ${receipt.symbols.token}")
fg.tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.crypto}")
fg.tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.crypto}")
val willSent = SpannableStringBuilder()
.bold {