diff --git a/app/build.gradle b/app/build.gradle index 7a6c354f62..56fa51dc9b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99243416f0..0def014c91 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ android:name="android.hardware.nfc" android:required="true" /> - diff --git a/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt b/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt index 07ac39e6dc..0ca9b929bd 100644 --- a/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index 46072d5765..925969d32f 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -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}" \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt index 177089ffba..7e84963643 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt @@ -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, builder: (List) -> String) { + val context = baseLayout.get()?.context ?: return + + val message = builder(errorList.map { context.getString(it) }) + showNotification(message) + } } val notificationsMiddleware: Middleware = { 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) } diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 4f98257223..4bd45c181d 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -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 + val builder: (List) -> 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, + override val builder: (List) -> String + ) : TapError(-1), MultiMessageError +} + +fun TapErrors.assembleErrorIds(): MutableList { + val idList = mutableListOf() + when (this) { + is MultiMessageError -> this.errorList.forEach { idList.addAll(it.assembleErrorIds()) } + is TapError -> idList.add(this.localizedMessage) + } + return idList } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index ba2a2b0387..9cc2fbb831 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 2dba8c385f..bd93f890d5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt index b0169f3df7..f9e70c740f 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt @@ -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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt index 259051b8a4..3c001122bd 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt @@ -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)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index f1d202cf46..5ba0bc7bc2 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 3f55dfddd5..cdf07f2690 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -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 = { 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 diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt index cc5fa3430f..c20db4385b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt index 5129a95646..db8fe61e47 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt @@ -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 - ) - } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index 883ca9f708..f5da58ab54 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -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?): Value? { + private fun createValueOfFeeAmount(feeType: FeeType, list: List?): 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 { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt index 91726cb199..0d92ab9a57 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt @@ -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 ) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 858b626b15..8a18db9d5e 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt index e7b081c183..dfcc6ca4c5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt @@ -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 - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt index 2c95438ca3..72575e6a6b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt @@ -14,15 +14,18 @@ enum class FeeType { data class FeeState( val selectedFeeType: FeeType = FeeType.NORMAL, val feeList: List? = null, - val currentFee: Value? = 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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt index 321cfe43b8..90e22a0e9e 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt @@ -11,9 +11,10 @@ data class ReceiptState( val crypto: ReceiptCrypto? = null, val tokenFiat: ReceiptTokenFiat? = null, val tokenCrypto: ReceiptTokenCrypto? = null, - val mainCurrencyType: Value? = 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 ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index 7d3df6c928..5742c5c60d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -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 = 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 = 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( - val value: T, - val displayedValue: String +data class MainCurrency( + val type: MainCurrencyType, + val currencySymbol: String ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 8201b03f38..de7856d3e6 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index c4ee8d767a..278b8d62b4 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt index 233a6c5500..c348a46df0 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt @@ -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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9373ea5d54..22a73a85d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,11 +57,15 @@ Address is invalid or unsupported by blockchain Address is the same as wallet address Network fee request is failed - Fee greater than amount - Amount with fee greater than balance Insufficient balance Blockchain internal error - + amount_exceeds_balance + fee_exceeds_balance + total_exceeds_balance + invalid_amount_value + invalid_fee_value + dust_amount + dust_change Send Address or PayID