Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-21 09:44:38 +00:00
commit 64af848bd9
25 changed files with 536 additions and 396 deletions

View file

@ -64,7 +64,7 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation 'com.tangem:blockchain:1.31.0'
implementation 'com.tangem:blockchain:1.35.0'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"

View file

@ -7,7 +7,6 @@
android:name="android.hardware.nfc"
android:required="true" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.USE_BIOMETRICT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.NFC" />

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

@ -41,7 +41,7 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByCard = state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true
val notEmpty = state.wallet?.transactions?.isNullOrEmpty() != true ||
val notEmpty = state.wallet?.recentTransactions?.isNullOrEmpty() != true ||
state.wallet.amounts.toList().unzip().second.map { it.value?.isZero() }.contains(false)
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard

View file

@ -26,14 +26,16 @@ data class PrepareSendScreen(
// Address or PayId
sealed class AddressPayIdActionUi : SendScreenActionUi {
data class ChangeAddressOrPayId(val data: String) : AddressPayIdActionUi()
data class HandleUserInput(val data: String) : AddressPayIdActionUi()
data class PasteAddressPayId(val data: String) : AddressPayIdActionUi()
data class CheckClipboard(val data: String?) : AddressPayIdActionUi()
object CheckAddressPayId: AddressPayIdActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi()
}
sealed class AddressPayIdVerifyAction : SendScreenAction {
enum class Error {
IS_NOT_PAY_ID,
PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN,
PAY_ID_NOT_REGISTERED,
PAY_ID_REQUEST_FAILED,
@ -41,38 +43,31 @@ sealed class AddressPayIdVerifyAction : SendScreenAction {
ADDRESS_SAME_AS_WALLET
}
data class VerifyClipboard(val data: String?) : AddressPayIdVerifyAction()
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressPayIdVerifyAction()
sealed class PayIdVerification : AddressPayIdVerifyAction() {
data class SetError(val payId: String, val error: Error) : PayIdVerification()
data class SetPayIdWalletAddress(val payId: String, val payIdWalletAddress: String) : PayIdVerification()
data class SetPayIdError(val error: Error?) : PayIdVerification()
data class SetPayIdWalletAddress(val payId: String, val payIdWalletAddress: String, val isUserInput: Boolean) : PayIdVerification()
}
sealed class AddressVerification : AddressPayIdVerifyAction() {
data class SetError(val address: String, val error: Error) : AddressVerification()
data class SetWalletAddress(val address: String) : AddressVerification()
data class SetAddressError(val error: Error?) : AddressVerification()
data class SetWalletAddress(val address: String, val isUserInput: Boolean) : AddressVerification()
}
}
// Amount to send
sealed class AmountActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AmountActionUi()
object CheckAmountToSend : AmountActionUi()
object SetMaxAmount : AmountActionUi()
data class CheckAmountToSend(val data: String? = null) : AmountActionUi()
data class SetMainCurrency(val mainCurrency: MainCurrencyType) : AmountActionUi()
object ToggleMainCurrency : AmountActionUi()
}
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 SetAmount(val amount: BigDecimal, val isUserInput: Boolean) : AmountAction()
data class SetAmountError(val error: TapError?) : AmountAction()
}
// Fee

View file

@ -1,52 +1,77 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.common.network.Result
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.isPayIdSupported
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.scope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
/**
[REDACTED_AUTHOR]
*/
internal class AddressPayIdMiddleware {
fun handle(data: String?, appState: AppState?, dispatch: DispatchFunction) {
if (data == null) {
dispatch(AddressVerification.SetError("", Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN))
return
}
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
if (data == sendState.addressPayIdState.etFieldValue) return
if (PayIdManager.isPayId(data)) {
verifyPayId(data, walletManager, dispatch)
} else {
val supposedAddress = extractAddressFromShareUri(data)
dispatch(PayIdVerification.SetError(supposedAddress, Error.IS_NOT_PAY_ID))
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(walletManager.wallet, supposedAddress)
if (failReason == null) {
dispatch(AddressVerification.SetWalletAddress(supposedAddress))
} else {
dispatch(AddressVerification.SetError(supposedAddress, failReason))
}
fun handle(action: AddressPayIdActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AddressPayIdActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressPayIdActionUi.PasteAddressPayId -> pasteAddressPayId(action.data, dispatch)
is AddressPayIdActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressPayIdActionUi.CheckAddressPayId -> verifyAddressPayId(appState, dispatch)
else -> return
}
}
private fun verifyPayId(payId: String, walletManager: WalletManager, dispatch: DispatchFunction) {
val blockchain = walletManager.wallet.blockchain
private fun handleUserInput(input: String, appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
if (input == sendState.addressPayIdState.viewFieldValue.value) return
setAddressAndCheck(input, true, dispatch)
}
private fun pasteAddressPayId(data: String, dispatch: (Action) -> Unit) {
setAddressAndCheck(data, false, dispatch)
}
private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) {
if (PayIdManager.isPayId(data)) {
dispatch(SetPayIdWalletAddress(data, "", isUserInput))
} else {
dispatch(SetWalletAddress(data, isUserInput))
}
dispatch(AddressPayIdActionUi.CheckAddressPayId)
}
private fun verifyAddressPayId(appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val wallet = sendState.walletManager?.wallet ?: return
val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return
val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput
if (PayIdManager.isPayId(addressPayId)) {
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
} else {
verifyAddress(addressPayId, wallet, isUserInput, dispatch)
}
}
private fun verifyPayId(payId: String, wallet: Wallet, isUserInput: Boolean, dispatch: DispatchFunction) {
val blockchain = wallet.blockchain
if (!blockchain.isPayIdSupported()) {
dispatch(PayIdVerification.SetError(payId, Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN))
dispatch(SetPayIdError(Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN))
return
}
@ -57,27 +82,37 @@ internal class AddressPayIdMiddleware {
is Result.Success -> {
val address = result.data.getAddress()
if (address == null) {
dispatch(PayIdVerification.SetError(payId, Error.PAY_ID_NOT_REGISTERED))
dispatch(SetPayIdError(Error.PAY_ID_NOT_REGISTERED))
return@withContext
}
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(walletManager.wallet, address)
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, address)
if (failReason == null) {
dispatch(PayIdVerification.SetPayIdWalletAddress(payId, address))
dispatch(SetPayIdWalletAddress(payId, address, isUserInput))
dispatch(FeeAction.RequestFee)
} else {
dispatch(AddressVerification.SetError(payId, failReason))
dispatch(SetAddressError(failReason))
}
}
is Result.Failure -> {
dispatch(PayIdVerification.SetError(payId, Error.PAY_ID_REQUEST_FAILED))
dispatch(SetPayIdError(Error.PAY_ID_REQUEST_FAILED))
}
}
}
}
}
private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val supposedAddress = extractAddressFromShareUri(address)
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress)
if (failReason == null) {
dispatch(SetWalletAddress(supposedAddress, isUserInput))
} else {
dispatch(SetAddressError(failReason))
}
}
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.address != address) {
@ -97,4 +132,27 @@ internal class AddressPayIdMiddleware {
return if (prefixes.isEmpty()) shareUri
else shareUri.replace(prefixes[0], "")
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val addressPayId = input ?: return
val wallet = appState?.sendState?.walletManager?.wallet ?: return
val internalDispatcher: (Action) -> Unit = {
when (it) {
is SetWalletAddress, is SetPayIdWalletAddress -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(true))
}
is SetAddressError, is SetPayIdError -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(false))
}
}
}
if (PayIdManager.isPayId(addressPayId)) {
verifyPayId(addressPayId, wallet, false, internalDispatcher)
} else {
verifyAddress(addressPayId, wallet, false, internalDispatcher)
}
}
}

View file

@ -1,14 +1,16 @@
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.extensions.isNegative
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.AmountActionUi
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
@ -17,83 +19,93 @@ import java.math.BigDecimal
*/
class AmountMiddleware {
fun handle(rawData: String?, appState: AppState?, dispatch: (Action) -> Unit) {
fun handle(action: AmountActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AmountActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AmountActionUi.CheckAmountToSend -> checkAmountToSend(appState, dispatch)
is AmountActionUi.SetMaxAmount -> setMaxAmount(appState, dispatch)
is AmountActionUi.ToggleMainCurrency -> toggleMainCurrency(appState, dispatch)
is AmountActionUi.SetMainCurrency -> return
}
}
private fun handleUserInput(data: String, appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val rawData = rawData ?: store.state.sendState.amountState.viewAmountValue
val data = if (rawData == ".") "0.0" else rawData
val proposedAmountValue = when {
val data = if (data == ".") "0.0" else data
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
dispatch(AmountAction.SetAmount(inputValue, true))
dispatch(AmountActionUi.CheckAmountToSend)
}
private fun checkAmountToSend(appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val inputCrypto = sendState.amountState.amountToSendCrypto
if (sendState.amountState.viewAmountValue.value == "0" && inputCrypto.isZero()) {
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return
}
val checkResult = amountChecker.check(
proposedAmountValue,
sendState.feeState.getCurrentFee(),
sendState.feeState.feeIsIncluded
)
if (checkResult.error == null) {
dispatch(AmountAction.AmountVerification.SetAmount(checkResult.amount))
val feeCrypto = sendState.feeState.getCurrentFee()
val feeAmount = Amount(feeCrypto, walletManager.wallet.blockchain)
val totalAmount = Amount(sendState.getTotalAmountToSend(inputCrypto), walletManager.wallet.blockchain)
val transactionErrors = walletManager.validateTransaction(totalAmount, feeAmount)
if (transactionErrors.isEmpty()) {
dispatch(AmountAction.SetAmountError(null))
} 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.SetAmountError(error))
}
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
}
}
private fun setMaxAmount(appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val amountState = sendState.amountState
interface AmountChecker {
data class Result(val amount: BigDecimal, val error: AmountAction.Error? = null)
val maxAmount = if (sendState.feeState.feeIsIncluded) amountState.balanceCrypto
else amountState.balanceCrypto.minus(sendState.feeState.getCurrentFee())
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)
}
if (maxAmount.isNegative()) {
dispatch(AmountAction.SetAmountError(TapError.FeeExceedsBalance))
} 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)
}
val currentCurrencyValue = if (amountState.mainCurrency.type == MainCurrencyType.CRYPTO) maxAmount
else sendState.convertToFiat(maxAmount)
dispatch(AmountAction.SetAmount(currentCurrencyValue, false))
}
}
protected abstract fun convert(value: BigDecimal): BigDecimal
}
private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) {
val amountState = appState?.sendState?.amountState ?: return
class CryptoAmountChecker(
balanceCrypto: BigDecimal
) : BaseAmountChecker(balanceCrypto) {
override fun convert(value: BigDecimal): BigDecimal = value
}
val type = if (amountState.mainCurrency.type == MainCurrencyType.FIAT) MainCurrencyType.CRYPTO
else MainCurrencyType.FIAT
class FiatAmountChecker(
balanceCrypto: BigDecimal,
private val converter: CurrencyConverter
) : BaseAmountChecker(balanceCrypto) {
override fun convert(value: BigDecimal): BigDecimal = converter.toFiat(value)
dispatch(AmountActionUi.SetMainCurrency(type))
}
}

View file

@ -28,7 +28,7 @@ class RequestFeeMiddleware {
if (!sendState.addressPayIdIsReady()) {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
dispatch(FeeAction.ChangeLayoutVisibility(main = false, controls = true, chipGroup = true))
dispatch(FeeAction.ChangeLayoutVisibility(main = false, chipGroup = true))
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return
@ -60,7 +60,7 @@ class RequestFeeMiddleware {
dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false))
}
} else {
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true, chipGroup = true))
dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true))
}
}
is Result.Failure -> {

View file

@ -7,9 +7,8 @@ import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.ChangeAddressOrPayId
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.*
import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
@ -29,20 +28,8 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is ChangeAddressOrPayId -> AddressPayIdMiddleware().handle(action.data, appState(), dispatch)
is VerifyClipboard -> {
AddressPayIdMiddleware().handle(action.data, appState()) {
when (it) {
is AddressVerification.SetWalletAddress, is PayIdVerification.SetPayIdWalletAddress -> {
dispatch(ChangePasteBtnEnableState(true))
}
is AddressVerification.SetError, is PayIdVerification.SetError -> {
dispatch(ChangePasteBtnEnableState(false))
}
}
}
}
is CheckAmountToSend -> AmountMiddleware().handle(action.data, appState(), dispatch)
is AddressPayIdActionUi -> AddressPayIdMiddleware().handle(action, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient -> verifyAndSendTransaction(appState(), dispatch)
}
@ -59,7 +46,7 @@ private fun verifyAndSendTransaction(appState: AppState?, dispatch: (Action) ->
val recipientAddress = sendState.addressPayIdState.recipientWalletAddress!!
val feeAmount = Amount(sendState.feeState.getCurrentFee(), blockchain)
val amountToSend = Amount(sendState.amountState.amountToSendCrypto, blockchain, recipientAddress)
val amountToSend = Amount(sendState.getTotalAmountToSend(), blockchain, recipientAddress)
val txSender = walletManager as TransactionSender

View file

@ -6,6 +6,7 @@ import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerifi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
@ -20,24 +21,43 @@ class AddressPayIdReducer : SendInternalReducer {
private fun handleUiAction(action: AddressPayIdActionUi, sendState: SendState, state: AddressPayIdState): SendState {
val result = when (action) {
is AddressPayIdActionUi.ChangeAddressOrPayId -> state
is AddressPayIdActionUi.HandleUserInput -> state
is AddressPayIdActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressPayIdActionUi.TruncateOrRestore -> {
if (action.truncate) state.copy(etFieldValue = state.truncatedFieldValue)
else state.copy(etFieldValue = state.normalFieldValue)
val value = if (action.truncate) state.truncatedFieldValue ?: ""
else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressPayIdActionUi.PasteAddressPayId -> return sendState
is AddressPayIdActionUi.CheckClipboard -> return sendState
is AddressPayIdActionUi.CheckAddressPayId -> return sendState
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}
private fun handleAction(action: AddressPayIdVerifyAction, sendState: SendState, state: AddressPayIdState): SendState {
val result = when (action) {
is PayIdVerification.SetPayIdWalletAddress -> state.copyPayIdWalletAddress(action.payId, action.payIdWalletAddress)
is PayIdVerification.SetError -> state.copyPayIdError(action.payId, action.error)
is AddressVerification.SetWalletAddress -> state.copyWalletAddress(action.address)
is AddressVerification.SetError -> state.copyError(action.address, action.error)
is PayIdVerification.SetPayIdWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.payId, action.isUserInput),
normalFieldValue = action.payId,
truncatedFieldValue = state.truncate(action.payId),
recipientWalletAddress = action.payIdWalletAddress,
error = null
)
}
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
recipientWalletAddress = action.address,
error = null
)
}
is AddressPayIdVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressPayIdVerifyAction.VerifyClipboard -> state
is AddressVerification.SetAddressError -> state.copy(error = action.error, recipientWalletAddress = null)
is PayIdVerification.SetPayIdError -> state.copy(error = action.error, recipientWalletAddress = null)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}

View file

@ -1,18 +1,16 @@
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.scaleToFiat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
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.InputViewValue
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,97 +24,65 @@ 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
else MainCurrencyType.FIAT
return handleUiAction(SetMainCurrency(type), sendState, state)
}
is SetMainCurrency -> {
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),
viewAmountValue = InputViewValue(fiatToSend.stripZeroPlainString()),
viewBalanceValue = rescaledBalance.stripZeroPlainString(),
mainCurrency = state.createMainCurrency(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(),
viewAmountValue = InputViewValue(state.amountToSendCrypto.stripZeroPlainString()),
viewBalanceValue = state.balanceCrypto.stripZeroPlainString(),
mainCurrency = mainCurrency,
mainCurrency = state.createMainCurrency(action.mainCurrency),
maxLengthOfAmount = sendState.getDecimals(action.mainCurrency),
cursorAtTheSamePosition = false
)
}
}
}
is SetMaxAmount -> {
val maxAmount = if (sendState.feeState.feeIsIncluded) {
state.balanceCrypto
} else {
val balanceExtractFee = state.balanceCrypto.minus(sendState.feeState.getCurrentFee())
if (balanceExtractFee.isNegative()) BigDecimal.ZERO
else balanceExtractFee
}
val etFieldValue = if (state.mainCurrency.value == MainCurrencyType.CRYPTO) maxAmount
else converter.toFiat(maxAmount)
state.copy(
viewAmountValue = etFieldValue.stripZeroPlainString(),
amountToSendCrypto = maxAmount,
cursorAtTheSamePosition = false
)
}
is CheckAmountToSend -> return sendState
else -> return sendState
}
return updateLastState(sendState.copy(amountState = result), result)
}
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)
is AmountAction.SetAmount -> {
when (state.mainCurrency.type) {
MainCurrencyType.FIAT -> {
val amountCrypto = sendState.convertToCrypto(action.amount)
state.copy(
viewAmountValue = InputViewValue(action.amount.scaleToFiat(false).stripZeroPlainString(), action.isUserInput),
amountToSendCrypto = amountCrypto,
cursorAtTheSamePosition = true,
error = null
)
}
MainCurrencyType.CRYPTO -> {
state.copy(
viewAmountValue = InputViewValue(action.amount.stripZeroPlainString(), action.isUserInput),
amountToSendCrypto = action.amount,
cursorAtTheSamePosition = true,
error = null
)
}
}
}
is AmountAction.AmountVerification.SetError -> {
setAmount(sendState.currencyConverter, decimals, action.amount, state).copy(error = action.error)
is AmountAction.SetAmountError -> {
state.copy(error = action.error)
}
}
return updateLastState(sendState.copy(amountState = result), result)
}
private fun setAmount(converter: CurrencyConverter, decimals: Int, amount: BigDecimal, state: AmountState): AmountState {
return when (state.mainCurrency.value) {
MainCurrencyType.FIAT -> {
val amountCrypto = converter.toCrypto(amount, decimals).stripTrailingZeros()
state.copy(
viewAmountValue = amount.stripZeroPlainString(),
amountToSendCrypto = amountCrypto,
cursorAtTheSamePosition = true,
error = null
)
}
MainCurrencyType.CRYPTO -> {
state.copy(
viewAmountValue = amount.stripZeroPlainString(),
amountToSendCrypto = amount,
cursorAtTheSamePosition = true,
error = null
)
}
}
}
}

View file

@ -2,14 +2,12 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.Value
/**
[REDACTED_AUTHOR]
@ -86,9 +84,10 @@ class FeeReducer : SendInternalReducer {
return updateLastState(sendState.copy(feeState = result), result)
}
private fun createValueOfFeeAmount(feeType: FeeType, list: List<Amount>?): Value<Amount>? {
private fun createValueOfFeeAmount(feeType: FeeType, list: List<Amount>?): Amount? {
if (list == null || list.isEmpty()) return null
val feeAmount = if (list.size == 1) {
return if (list.size == 1) {
if (!list[0].isAboveZero()) return null
list[0]
@ -100,8 +99,6 @@ class FeeReducer : SendInternalReducer {
FeeType.PRIORITY -> list[2]
}
}
return Value(feeAmount, feeAmount.value?.stripZeroPlainString() ?: "")
}
private fun getCurrentFeeType(state: FeeState): FeeType {

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.type, 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)
mainCurrency = sendState.amountState.mainCurrency,
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,55 +110,63 @@ class ReceiptReducer : SendInternalReducer {
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = converter.toFiat(feeCrypto).stripZeroPlainString(),
willSentFiat = converter.toFiat(totalCrypto).stripZeroPlainString(),
feeFiat = feeFiat.stripZeroPlainString(),
willSentFiat = converter.toFiatWithPrecision(totalCrypto).stripZeroPlainString(),
symbols = symbols
)
}
}
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

@ -3,61 +3,20 @@ package com.tangem.tap.features.send.redux.states
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
data class AddressPayIdState(
val etFieldValue: String? = null,
val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null,
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 truncate(value: String): String = truncateHandler?.invoke(value) ?: value
fun isReady(): Boolean = error == null && recipientWalletAddress?.isNotEmpty() ?: false
fun isPayIdState(): Boolean = recipientWalletAddress != null && recipientWalletAddress != normalFieldValue
fun copyWalletAddress(address: String): AddressPayIdState {
val truncated = truncateHandler?.invoke(address) ?: address
return this.copy(
etFieldValue = address,
normalFieldValue = address,
truncatedFieldValue = truncated,
recipientWalletAddress = address,
error = null
)
}
fun copyError(address: String, error: AddressPayIdVerifyAction.Error): AddressPayIdState {
val truncated = truncateHandler?.invoke(address) ?: address
return this.copy(
etFieldValue = address,
normalFieldValue = address,
truncatedFieldValue = truncated,
error = error,
recipientWalletAddress = null
)
}
fun copyPayIdWalletAddress(payId: String, address: String): AddressPayIdState {
val truncated = truncateHandler?.invoke(address) ?: address
return this.copy(
etFieldValue = payId,
normalFieldValue = payId,
truncatedFieldValue = truncated,
recipientWalletAddress = address,
error = null
)
}
fun copyPayIdError(payId: String, error: AddressPayIdVerifyAction.Error): AddressPayIdState {
val truncated = truncateHandler?.invoke(payId) ?: payId
return this.copy(
etFieldValue = payId,
normalFieldValue = payId,
truncatedFieldValue = truncated,
error = error,
recipientWalletAddress = null
)
}
}

View file

@ -14,15 +14,18 @@ enum class FeeType {
data class FeeState(
val selectedFeeType: FeeType = FeeType.NORMAL,
val feeList: List<Amount>? = null,
val currentFee: Value<Amount>? = null,
val currentFee: Amount? = null,
val feeIsIncluded: Boolean = false,
val mainLayoutIsVisible: Boolean = false,
val controlsLayoutIsVisible: Boolean = true,
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
fun getCurrentFee(): BigDecimal = currentFee?.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 mainCurrency: MainCurrency? = 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,10 +49,29 @@ 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 getButtonState(): SendButtonState = if (isReadyToSend()) SendButtonState.ENABLED else SendButtonState.DISABLED
fun getTotalAmountToSend(value: BigDecimal = amountState.amountToSendCrypto): BigDecimal {
val needToExtractFee = amountState.isCoinAmount() && feeState.feeIsIncluded
return if (needToExtractFee) value.minus(feeState.getCurrentFee()) else value
}
}
enum class SendButtonState {
@ -58,25 +79,38 @@ enum class SendButtonState {
}
data class AmountState(
val viewAmountValue: String = BigDecimal.ZERO.toPlainString(),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.DEFAULT_FIAT_CURRENCY),
val walletAmount: Amount? = null,
val typeOfAmount: AmountType = AmountType.Coin,
val viewAmountValue: InputViewValue = InputViewValue(BigDecimal.ZERO.toPlainString()),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, TapCurrency.DEFAULT_FIAT_CURRENCY),
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 createMainCurrency(type: MainCurrencyType): MainCurrency {
return when (type) {
MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency)
MainCurrencyType.CRYPTO -> MainCurrency(type, walletAmount?.currencySymbol ?: "NONE")
}
}
}
data class InputViewValue(val value: String, val isFromUserInput: Boolean = false)
enum class MainCurrencyType {
FIAT, CRYPTO
}
data class Value<T>(
val value: T,
val displayedValue: String
data class MainCurrency(
val type: MainCurrencyType,
val currencySymbol: String
)

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()
@ -69,22 +75,22 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private fun setupAddressOrPayIdLayout() {
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
store.dispatch(AddressPayIdVerifyAction.VerifyClipboard(requireContext().getFromClipboard()?.toString()))
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
etAddressOrPayId.setOnFocusChangeListener { v, hasFocus ->
store.dispatch(TruncateOrRestore(!hasFocus))
}
etAddressOrPayId.inputtedTextAsFlow()
.debounce(400)
.filter { store.state.sendState.addressPayIdState.etFieldValue != it }
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.onEach {
store.dispatch(ChangeAddressOrPayId(it))
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
store.dispatch(FeeAction.RequestFee)
}
.launchIn(mainScope)
imvPaste.setOnClickListener {
store.dispatch(ChangeAddressOrPayId(requireContext().getFromClipboard()?.toString() ?: ""))
store.dispatch(PasteAddressPayId(requireContext().getFromClipboard()?.toString() ?: ""))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
}
@ -102,7 +108,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: ""
if (scannedCode.isEmpty()) return
store.dispatch(ChangeAddressOrPayId(scannedCode))
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
}
@ -122,7 +128,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
etAmountToSend.clearFocus()
etAmountToSend.postDelayed(200) { etAmountToSend.hideSoftKeyboard() }
store.dispatch(SetMaxAmount)
store.dispatch(CheckAmountToSend())
store.dispatch(CheckAmountToSend)
}
var snackbarControlledByChangingFocus = false
keyboardObserver = KeyboardObserver(requireActivity())
@ -154,14 +160,14 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val prevFocusChangeListener = etAmountToSend.onFocusChangeListener
etAmountToSend.setOnFocusChangeListener { v, hasFocus ->
prevFocusChangeListener.onFocusChange(v, hasFocus)
// if (hasFocus && etAmountToSend.text?.toString() == "0") etAmountToSend.setText("")
if (hasFocus && etAmountToSend.text?.toString() == "0") etAmountToSend.setText("")
if (!hasFocus && etAmountToSend.text?.toString() == "") etAmountToSend.setText("0")
}
etAmountToSend.inputtedTextAsFlow()
.debounce(400)
.filter { store.state.sendState.amountState.viewAmountValue != it && it.isNotEmpty() }
.onEach { store.dispatch(CheckAmountToSend(it)) }
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
.launchIn(mainScope)
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
@ -178,15 +184,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
if (checkedId == -1) return@setOnCheckedChangeListener
store.dispatch(ChangeSelectedFee(FeeUiHelper.idToFee(checkedId)))
store.dispatch(CheckAmountToSend())
store.dispatch(CheckAmountToSend)
}
swIncludeFee.setOnCheckedChangeListener { btn, isChecked ->
store.dispatch(ChangeIncludeFee(isChecked))
store.dispatch(CheckAmountToSend())
store.dispatch(CheckAmountToSend)
}
}
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
@ -66,7 +67,6 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
private fun handleAddressPayIdState(fg: BaseStoreFragment, state: AddressPayIdState) {
fun parseError(context: Context, error: Error?): String? {
val resId = when (error) {
Error.IS_NOT_PAY_ID -> R.string.error_payid_verification_failed
Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.error_payid_unsupported_by_blockchain
Error.PAY_ID_NOT_REGISTERED -> R.string.error_payid_not_registere
Error.PAY_ID_REQUEST_FAILED -> R.string.error_payid_request_failed
@ -89,31 +89,30 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
til.helperText = state.recipientWalletAddress
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
// prevent cycling
if (state.etFieldValue == null) return
et.update(state.etFieldValue)
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
}
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))
val amountToSend = state.viewAmountValue
fg.etAmountToSend.update(amountToSend)
if (!amountToSend.isFromUserInput) fg.etAmountToSend.update(amountToSend.value)
// fg.tvAmountToSendShadow.text = amountToSend
// if (amountToSend.length > 10) {
@ -131,11 +130,11 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
// if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length)
// }
fg.tvAmountCurrency.update(state.mainCurrency.displayedValue)
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.value)
fg.tvAmountCurrency.update(state.mainCurrency.currencySymbol)
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)
val balanceText = fg.getString(R.string.send_balance,
state.mainCurrency.displayedValue,
state.mainCurrency.currencySymbol,
state.viewBalanceValue)
fg.tvBalance.update(balanceText)
}
@ -162,6 +161,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
}
fg.swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled
if (fg.swIncludeFee.isChecked != state.feeIsIncluded) {
fg.swIncludeFee.isChecked = state.feeIsIncluded
}
@ -182,6 +182,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 +191,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 +211,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 +232,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 +248,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 {

View file

@ -104,7 +104,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
val fiatRate = state.globalState.conversionRates.getRate(action.wallet.blockchain.currency)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
val pendingTransactions = action.wallet.transactions
val pendingTransactions = action.wallet.recentTransactions
.toPendingTransactions(action.wallet.address)
val sendButtonEnabled = amount?.isZero() == false || token?.value?.isZero() == false

View file

@ -57,11 +57,15 @@
<string name="error_address_invalid_or_unsupported">Address is invalid or unsupported by blockchain</string>
<string name="error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="error_fee_request_failed">Network fee request is failed</string>
<string name="error_fee_greater_than_amount">Fee greater than amount</string>
<string name="error_amount_with_fee_greater_than_balance">Amount with fee greater than balance</string>
<string name="error_insufficient_balance">Insufficient balance</string>
<string name="error_blockchain_internal">Blockchain internal error</string>
<string name="amount_exceeds_balance">amount_exceeds_balance</string>
<string name="fee_exceeds_balance">fee_exceeds_balance</string>
<string name="total_exceeds_balance">total_exceeds_balance</string>
<string name="invalid_amount_value">invalid_amount_value</string>
<string name="invalid_fee_value">invalid_fee_value</string>
<string name="dust_amount">dust_amount</string>
<string name="dust_change">dust_change</string>
<string name="send_title">Send</string>
<string name="send_address_or_payid">Address or PayID</string>