Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-20 18:22:00 +03:00
parent f8a1e3f2fe
commit 52391d0dad
11 changed files with 120 additions and 110 deletions

View file

@ -57,17 +57,16 @@ sealed class AddressPayIdVerifyAction : SendScreenAction {
// Amount to send
sealed class AmountActionUi : SendScreenActionUi {
data class HandleUserInput(val proposedAmount: 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 {
sealed class AmountVerification : AmountAction() {
data class SetAmount(val amount: BigDecimal) : AmountVerification()
data class SetError(val amount: BigDecimal, val error: TapError) : AmountVerification()
}
data class SetAmount(val amount: BigDecimal, val isUserInput: Boolean) : AmountAction()
data class SetAmountError(val error: TapError?) : AmountAction()
}
// Fee

View file

@ -3,12 +3,14 @@ package com.tangem.tap.features.send.redux.middlewares
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.store
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import org.rekotlin.Action
import java.math.BigDecimal
@ -17,12 +19,20 @@ import java.math.BigDecimal
*/
class AmountMiddleware {
fun handle(rawData: String?, appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
fun handle(action: AmountActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AmountActionUi.HandleUserInput -> handleUserInput(action.proposedAmount, appState, dispatch)
is AmountActionUi.CheckAmountToSend -> checkAmountToSend(appState, dispatch)
is AmountActionUi.SetMaxAmount -> setMaxAmount(appState, dispatch)
is AmountActionUi.ToggleMainCurrency -> toggleMainCurrency(appState, dispatch)
is AmountActionUi.SetMainCurrency -> return
}
}
val rawData = rawData ?: store.state.sendState.amountState.viewAmountValue
val data = if (rawData == ".") "0.0" else rawData
private fun handleUserInput(data: String, appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val data = if (data == ".") "0.0" else data
val inputValue = when {
data.isEmpty() || data == "0" -> BigDecimal.ZERO
else -> BigDecimal(data)
@ -30,7 +40,17 @@ class AmountMiddleware {
if (inputValue.isZero() && sendState.amountState.amountToSendCrypto.isZero()) return
val inputCrypto = sendState.convertInputValueToCrypto(inputValue)
dispatch(AmountAction.SetAmount(inputValue, true))
dispatch(AmountActionUi.CheckAmountToSend)
}
private fun checkAmountToSend(appState: AppState?, dispatch: (Action) -> Unit, isUserInput: Boolean = false) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val inputCrypto = sendState.amountState.amountToSendCrypto
if (sendState.amountState.viewAmountValue.value == "0" && inputCrypto.isZero()) return
val feeCrypto = sendState.feeState.getCurrentFee()
val feeAmount = Amount(feeCrypto, walletManager.wallet.blockchain)
@ -38,7 +58,7 @@ class AmountMiddleware {
val transactionErrors = walletManager.validateTransaction(totalAmount, feeAmount)
if (transactionErrors.isEmpty()) {
dispatch(AmountAction.AmountVerification.SetAmount(inputValue))
dispatch(AmountAction.SetAmountError(null))
} else {
val tapErrors = transactionErrors.map {
when (it) {
@ -53,10 +73,36 @@ class AmountMiddleware {
}
}
val error = TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
dispatch(AmountAction.AmountVerification.SetError(inputValue, error))
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
val maxAmount = if (sendState.feeState.feeIsIncluded) {
amountState.balanceCrypto
} else {
val balanceExtractFee = amountState.balanceCrypto.minus(sendState.feeState.getCurrentFee())
if (balanceExtractFee.isNegative()) BigDecimal.ZERO
else balanceExtractFee
}
val currentCurrencyValue = if (amountState.mainCurrency.type == MainCurrencyType.CRYPTO) maxAmount
else sendState.convertToFiat(maxAmount)
dispatch(AmountAction.SetAmount(currentCurrencyValue, false))
}
private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) {
val amountState = appState?.sendState?.amountState ?: return
val type = if (amountState.mainCurrency.type == MainCurrencyType.FIAT) MainCurrencyType.CRYPTO
else MainCurrencyType.FIAT
dispatch(AmountActionUi.SetMainCurrency(type))
}
}

View file

@ -9,7 +9,7 @@ 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.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
@ -42,7 +42,7 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
}
}
}
is CheckAmountToSend -> AmountMiddleware().handle(action.data, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient -> verifyAndSendTransaction(appState(), dispatch)
}

View file

@ -1,15 +1,16 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.common.extensions.isZero
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.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.ViewAmountValue
import java.math.BigDecimal
/**
@ -24,12 +25,6 @@ class AmountReducer : SendInternalReducer {
private fun handleUiAction(action: AmountActionUi, sendState: SendState, state: AmountState): SendState {
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 -> {
@ -37,42 +32,25 @@ class AmountReducer : SendInternalReducer {
else sendState.convertToFiat(state.amountToSendCrypto)
val rescaledBalance = sendState.convertToFiat(state.balanceCrypto, true)
state.copy(
viewAmountValue = fiatToSend.stripZeroPlainString(),
viewAmountValue = ViewAmountValue(fiatToSend.stripZeroPlainString()),
viewBalanceValue = rescaledBalance.stripZeroPlainString(),
mainCurrency = state.createMainCurrencyValue(action.mainCurrency),
mainCurrency = state.createMainCurrency(action.mainCurrency),
maxLengthOfAmount = sendState.getDecimals(action.mainCurrency),
cursorAtTheSamePosition = false
)
}
MainCurrencyType.CRYPTO -> {
state.copy(
viewAmountValue = state.amountToSendCrypto.stripZeroPlainString(),
viewAmountValue = ViewAmountValue(state.amountToSendCrypto.stripZeroPlainString()),
viewBalanceValue = state.balanceCrypto.stripZeroPlainString(),
mainCurrency = state.createMainCurrencyValue(action.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 sendState.convertToFiat(maxAmount)
state.copy(
viewAmountValue = etFieldValue.stripZeroPlainString(),
amountToSendCrypto = maxAmount,
cursorAtTheSamePosition = false
)
}
is CheckAmountToSend -> return sendState
else -> return sendState
}
return updateLastState(sendState.copy(amountState = result), result)
@ -80,35 +58,31 @@ class AmountReducer : SendInternalReducer {
private fun handleAction(action: AmountAction, sendState: SendState, state: AmountState): SendState {
val result = when (action) {
is AmountAction.AmountVerification.SetAmount -> {
setAmount(sendState, action.amount, state)
is AmountAction.SetAmount -> {
when (state.mainCurrency.type) {
MainCurrencyType.FIAT -> {
val amountCrypto = sendState.convertToCrypto(action.amount)
state.copy(
viewAmountValue = ViewAmountValue(action.amount.scaleToFiat(false).stripZeroPlainString(), action.isUserInput),
amountToSendCrypto = amountCrypto,
cursorAtTheSamePosition = true,
error = null
)
}
MainCurrencyType.CRYPTO -> {
state.copy(
viewAmountValue = ViewAmountValue(action.amount.stripZeroPlainString(), action.isUserInput),
amountToSendCrypto = action.amount,
cursorAtTheSamePosition = true,
error = null
)
}
}
}
is AmountAction.AmountVerification.SetError -> {
setAmount(sendState, 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(sendState: SendState, amount: BigDecimal, state: AmountState): AmountState {
return when (state.mainCurrency.value) {
MainCurrencyType.FIAT -> {
val amountCrypto = sendState.convertToCrypto(amount)
state.copy(
viewAmountValue = amount.stripZeroPlainString(),
amountToSendCrypto = amountCrypto,
cursorAtTheSamePosition = true,
error = null
)
}
MainCurrencyType.CRYPTO -> {
state.copy(
viewAmountValue = amount.stripZeroPlainString(),
amountToSendCrypto = amount,
cursorAtTheSamePosition = true,
error = null
)
}
}
}
}

View file

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

View file

@ -30,12 +30,12 @@ class ReceiptReducer : SendInternalReducer {
val amountState = sendState.amountState
val feeState = sendState.feeState
val layoutType = determineLayoutType(amountState.mainCurrency.value, sendState.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,
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),
@ -111,7 +111,7 @@ class ReceiptReducer : SendInternalReducer {
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = feeFiat.stripZeroPlainString(),
willSentFiat = converter.toFiat(totalCrypto).stripZeroPlainString(),
willSentFiat = converter.toFiatWithPrecision(totalCrypto).stripZeroPlainString(),
symbols = symbols
)
}

View file

@ -14,7 +14,7 @@ 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 = false,
@ -27,5 +27,5 @@ data class FeeState(
fun isReady(): Boolean = error == null && currentFee != null
fun getCurrentFee(): BigDecimal = currentFee?.value?.value ?: BigDecimal.ZERO
fun getCurrentFee(): BigDecimal = currentFee?.value ?: BigDecimal.ZERO
}

View file

@ -11,7 +11,7 @@ data class ReceiptState(
val crypto: ReceiptCrypto? = null,
val tokenFiat: ReceiptTokenFiat? = null,
val tokenCrypto: ReceiptTokenCrypto? = null,
val mainCurrencyType: Value<MainCurrencyType>? = null
val mainCurrency: MainCurrency? = null
) : SendScreenState {
override val stateId: StateId = StateId.RECEIPT
}

View file

@ -63,14 +63,7 @@ data class SendState(
fun convertToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
val converter = getConverter()
return if (scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
fun convertInputValueToCrypto(inputValue: BigDecimal): BigDecimal {
return when (amountState.mainCurrency.value) {
MainCurrencyType.FIAT -> convertToCrypto(inputValue)
MainCurrencyType.CRYPTO -> inputValue
}
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
fun getButtonState(): SendButtonState = if (isReadyToSend()) SendButtonState.ENABLED else SendButtonState.DISABLED
@ -88,9 +81,9 @@ enum class SendButtonState {
data class AmountState(
val walletAmount: Amount? = null,
val typeOfAmount: AmountType = AmountType.Coin,
val viewAmountValue: String = BigDecimal.ZERO.toPlainString(),
val viewAmountValue: ViewAmountValue = ViewAmountValue(BigDecimal.ZERO.toPlainString()),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.DEFAULT_FIAT_CURRENCY),
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, TapCurrency.DEFAULT_FIAT_CURRENCY),
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
val cursorAtTheSamePosition: Boolean = true,
@ -104,19 +97,20 @@ data class AmountState(
fun isCoinAmount(): Boolean = typeOfAmount == AmountType.Coin
fun createMainCurrencyValue(type: MainCurrencyType): Value<MainCurrencyType> {
fun createMainCurrency(type: MainCurrencyType): MainCurrency {
return when (type) {
MainCurrencyType.FIAT -> Value(type, store.state.globalState.appCurrency)
MainCurrencyType.CRYPTO -> Value(type, walletAmount?.currencySymbol ?: "NONE")
MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency)
MainCurrencyType.CRYPTO -> MainCurrency(type, walletAmount?.currencySymbol ?: "NONE")
}
}
}
data class ViewAmountValue(val value: String, val isUserInput: Boolean = false)
enum class MainCurrencyType {
FIAT, CRYPTO
}
data class Value<T>(
val value: T,
val displayedValue: String
data class MainCurrency(
val type: MainCurrencyType,
val currencySymbol: String
)

View file

@ -128,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())
@ -160,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(HandleUserInput(it)) }
.launchIn(mainScope)
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
@ -184,11 +184,11 @@ 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)
}
}

View file

@ -116,7 +116,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
fg.etAmountToSend.filters = arrayOf(DecimalDigitsInputFilter(12, state.maxLengthOfAmount))
val amountToSend = state.viewAmountValue
fg.etAmountToSend.update(amountToSend)
if (!amountToSend.isUserInput) fg.etAmountToSend.update(amountToSend.value)
// fg.tvAmountToSendShadow.text = amountToSend
// if (amountToSend.length > 10) {
@ -134,11 +134,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)
}