Updated on 2026-08-14
This commit is contained in:
commit
64af848bd9
25 changed files with 536 additions and 396 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue