Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-18 18:05:18 +08:00
parent e98780ff93
commit 479d142807
283 changed files with 2338 additions and 2730 deletions

View file

@ -26,11 +26,11 @@ interface SendScreenActionUi : SendScreenAction
object ReleaseSendState : Action
data class PrepareSendScreen(
val coinAmount: Amount?,
val coinRate: BigDecimal?,
val walletManager: WalletManager?,
val tokenAmount: Amount? = null,
val tokenRate: BigDecimal? = null
val coinAmount: Amount?,
val coinRate: BigDecimal?,
val walletManager: WalletManager?,
val tokenAmount: Amount? = null,
val tokenRate: BigDecimal? = null,
) : SendScreenAction
// Address or PayId
@ -44,18 +44,17 @@ sealed class AddressPayIdActionUi : SendScreenActionUi {
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean) : AddressPayIdActionUi()
}
sealed class TransactionExtrasAction : SendScreenActionUi {
data class Prepare(
val blockchain: Blockchain,
val walletAddress: String,
val xrpTag: String?,
val blockchain: Blockchain,
val walletAddress: String,
val xrpTag: String?,
) : TransactionExtrasAction()
object Release : TransactionExtrasAction()
sealed class XlmMemo : TransactionExtrasAction() {
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
data class HandleUserInput(val data: String) : XlmMemo()
}
@ -81,7 +80,11 @@ sealed class AddressPayIdVerifyAction : SendScreenAction {
sealed class PayIdVerification : AddressPayIdVerifyAction() {
data class SetPayIdError(val error: Error?) : PayIdVerification()
data class SetPayIdWalletAddress(val payId: String, val payIdWalletAddress: String, val isUserInput: Boolean) : PayIdVerification()
data class SetPayIdWalletAddress(
val payId: String,
val payIdWalletAddress: String,
val isUserInput: Boolean,
) : PayIdVerification()
}
sealed class AddressVerification : AddressPayIdVerifyAction() {
@ -121,9 +124,9 @@ sealed class FeeAction : SendScreenAction {
}
data class ChangeLayoutVisibility(
val main: Boolean? = null,
val controls: Boolean? = null,
val chipGroup: Boolean? = null,
val main: Boolean? = null,
val controls: Boolean? = null,
val chipGroup: Boolean? = null,
) : FeeAction()
}
@ -147,14 +150,14 @@ sealed class SendAction : SendScreenAction {
sealed class Dialog : SendAction(), StateDialog {
data class TezosWarningDialog(
val reduceCallback: () -> Unit,
val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal,
val reduceCallback: () -> Unit,
val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal,
) : Dialog()
sealed class SendTransactionFails : Dialog() {
data class CardSdkError(val error: TangemSdkError): Dialog()
data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError): Dialog()
data class CardSdkError(val error: TangemSdkError) : Dialog()
data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog()
}
data class RequestFeeError(
@ -173,7 +176,6 @@ sealed class SendAction : SendScreenAction {
data class SendSpecificTransaction(
val sendAmount: String,
val destinationAddress: String,
val transactionId: String
val transactionId: String,
) : SendAction()
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.extensions.Result
@ -82,33 +81,4 @@ class RequestFeeMiddleware {
}
}
}
}
class FeeMock {
companion object {
suspend fun getFee(blockchain: Blockchain): List<Amount> {
return feeStandard(blockchain)
// return feeSingle(blockchain)
// return feeStellar(blockchain)
// return feeZero(blockchain)
}
suspend fun feeStellar(blockchain: Blockchain): List<Amount> = listOf(Amount(0.0001.toBigDecimal(), blockchain))
suspend fun feeZero(blockchain: Blockchain): List<Amount> = listOf(Amount(BigDecimal.ZERO, blockchain))
suspend fun feeStandard(blockchain: Blockchain): List<Amount> = listOf(
Amount(0.001500.toBigDecimal(), blockchain),
Amount(0.0030.toBigDecimal(), blockchain),
Amount(0.0045001.toBigDecimal(), blockchain),
)
suspend fun feeStandardBig(blockchain: Blockchain): List<Amount> = listOf(
Amount(1.76.toBigDecimal(), blockchain),
Amount(2.30.toBigDecimal(), blockchain),
Amount(3.45.toBigDecimal(), blockchain),
)
suspend fun feeSingle(blockchain: Blockchain): List<Amount> = listOf(Amount(0.0015.toBigDecimal(), blockchain))
}
}

View file

@ -13,11 +13,11 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogShow
@ -145,6 +145,7 @@ private fun verifyAndSendTransaction(
}
}
@Suppress("LongParameterList", "LongMethod", "ComplexMethod")
private fun sendTransaction(
action: SendActionUi.SendAmountToRecipient,
walletManager: WalletManager,
@ -235,7 +236,7 @@ private fun sendTransaction(
}
scope.launch(Dispatchers.IO) {
updateWallet(walletManager)
delay(11000) // more than 10000 to avoid throttling
delay(timeMillis = 11000) // more than 10000 to avoid throttling
updateWallet(walletManager)
}
}
@ -246,13 +247,13 @@ private fun sendTransaction(
feeAmount = feeAmount,
destinationAddress = destinationAddress,
)
val error = (sendResult.error as? BlockchainSdkError) ?: return@withMainContext
val error = sendResult.error as? BlockchainSdkError ?: return@withMainContext
Analytics.send(Token.Send.TransactionSent(currencyType, error))
when (error) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return@withMainContext
val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext
if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext
dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError))

View file

@ -19,7 +19,11 @@ class AddressPayIdReducer : SendInternalReducer {
else -> sendState
}
private fun handleUiAction(action: AddressPayIdActionUi, sendState: SendState, state: AddressPayIdState): SendState {
private fun handleUiAction(
action: AddressPayIdActionUi,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is AddressPayIdActionUi.HandleUserInput -> state
is AddressPayIdActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
@ -35,24 +39,28 @@ class AddressPayIdReducer : SendInternalReducer {
return updateLastState(sendState.copy(addressPayIdState = result), result)
}
private fun handleAction(action: AddressPayIdVerifyAction, sendState: SendState, state: AddressPayIdState): SendState {
private fun handleAction(
action: AddressPayIdVerifyAction,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is PayIdVerification.SetPayIdWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.payId, action.isUserInput),
normalFieldValue = action.payId,
truncatedFieldValue = state.truncate(action.payId),
destinationWalletAddress = action.payIdWalletAddress,
error = null
viewFieldValue = InputViewValue(action.payId, action.isUserInput),
normalFieldValue = action.payId,
truncatedFieldValue = state.truncate(action.payId),
destinationWalletAddress = action.payIdWalletAddress,
error = null,
)
}
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressPayIdVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)

View file

@ -21,7 +21,9 @@ class FeeReducer : SendInternalReducer {
private fun handleUiAction(action: FeeActionUi, sendState: SendState, state: FeeState): SendState {
val result = when (action) {
is FeeActionUi.ToggleControlsVisibility -> state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible)
is FeeActionUi.ToggleControlsVisibility -> {
state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible)
}
is FeeActionUi.ChangeSelectedFee -> {
val currentFee = createValueOfFeeAmount(action.feeType, state.feeList)
state.copy(

View file

@ -37,13 +37,12 @@ class ReceiptReducer : SendInternalReducer {
this.feeState = sendState.feeState
return when (action) {
is RefreshReceipt -> handleRefresh(action, sendState.receiptState)
is RefreshReceipt -> handleRefresh(sendState.receiptState)
else -> sendState
}
}
private fun handleRefresh(action: RefreshReceipt, state: ReceiptState): SendState {
private fun handleRefresh(state: ReceiptState): SendState {
val wallet = sendState.walletManager?.wallet ?: return sendState
val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount)
@ -55,7 +54,7 @@ class ReceiptReducer : SendInternalReducer {
fiat = createFiatType(symbols, showBlank),
crypto = createCryptoType(symbols, showBlank),
tokenFiat = createTokenFiatType(symbols, showBlank),
tokenCrypto = createTokenCryptoType(symbols, showBlank)
tokenCrypto = createTokenCryptoType(symbols, showBlank),
)
return updateLastState(sendState.copy(receiptState = result), result)
}
@ -76,7 +75,7 @@ class ReceiptReducer : SendInternalReducer {
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
symbols = symbols
symbols = symbols,
)
} else {
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
@ -87,7 +86,7 @@ class ReceiptReducer : SendInternalReducer {
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = totalAmountCrypto.stripZeroPlainString(),
symbols = symbols
symbols = symbols,
)
}
}
@ -106,7 +105,7 @@ class ReceiptReducer : SendInternalReducer {
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(amountState.amountToSendCrypto),
symbols = symbols
symbols = symbols,
)
} else {
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
@ -116,7 +115,7 @@ class ReceiptReducer : SendInternalReducer {
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(totalCrypto),
symbols = symbols
symbols = symbols,
)
}
}
@ -140,7 +139,7 @@ class ReceiptReducer : SendInternalReducer {
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
symbols = symbols,
)
} else {
ReceiptTokenFiat(
@ -149,7 +148,7 @@ class ReceiptReducer : SendInternalReducer {
totalFiat = UNKNOWN_AMOUNT_SIGN,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
symbols = symbols,
)
}
}
@ -170,17 +169,16 @@ class ReceiptReducer : SendInternalReducer {
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols
symbols = symbols,
)
} else {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = UNKNOWN_AMOUNT_SIGN,
symbols = symbols
symbols = symbols,
)
}
}
private fun determineSymbols(wallet: Wallet, amountType: AmountType): ReceiptSymbols {
@ -190,7 +188,7 @@ class ReceiptReducer : SendInternalReducer {
token = when (amountType) {
is AmountType.Token -> amountType.token.symbol
else -> null
}
},
)
}
@ -211,9 +209,15 @@ class ReceiptReducer : SendInternalReducer {
private fun convertToFiatPrecision(value: BigDecimal, isToken: Boolean = false): String {
return when {
!isToken && sendState.coinIsConvertible() -> sendState.coinConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
isToken && sendState.tokenIsConvertible() -> sendState.tokenConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
else -> UNKNOWN_AMOUNT_SIGN
!isToken && sendState.coinIsConvertible() -> {
sendState.coinConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
}
isToken && sendState.tokenIsConvertible() -> {
sendState.tokenConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
}
else -> {
UNKNOWN_AMOUNT_SIGN
}
}
}

View file

@ -28,25 +28,23 @@ interface SendInternalReducer {
fun handle(action: SendScreenAction, sendState: SendState): SendState
}
class SendScreenReducer {
companion object {
fun reduce(incomingAction: Action, sendState: SendState): SendState {
if (incomingAction is ReleaseSendState) return SendState()
val action = incomingAction as? SendScreenAction ?: return sendState
object SendScreenReducer {
fun reduce(incomingAction: Action, sendState: SendState): SendState {
if (incomingAction is ReleaseSendState) return SendState()
val action = incomingAction as? SendScreenAction ?: return sendState
val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer()
is ReceiptAction -> ReceiptReducer()
is SendAction -> SendReducer()
else -> EmptyReducer()
}
return reducer.handle(action, sendState)
val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer()
is ReceiptAction -> ReceiptReducer()
is SendAction -> SendReducer()
else -> EmptyReducer()
}
return reducer.handle(action, sendState)
}
}
@ -70,19 +68,19 @@ private class SendReducer : SendInternalReducer {
}
private fun handleSendSpecificTransactionAction(
action: SendAction.SendSpecificTransaction, state: SendState
action: SendAction.SendSpecificTransaction, state: SendState,
): SendState {
return state.copy(
externalTransactionData =
ExternalTransactionData(action.sendAmount, action.destinationAddress, action.transactionId),
feeState = state.feeState.copy(
includeFeeSwitcherIsEnabled = false
includeFeeSwitcherIsEnabled = false,
),
amountState = state.amountState.copy(
inputIsEnabled = false
inputIsEnabled = false,
),
addressPayIdState = state.addressPayIdState.copy(
inputIsEnabled = false
inputIsEnabled = false,
),
)
}
@ -100,17 +98,17 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer {
val decimals = amountToExtract.decimals
return sendState.copy(
walletManager = walletManager,
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) },
amountState = sendState.amountState.copy(
amountToExtract = amountToExtract,
typeOfAmount = amountToExtract.type,
balanceCrypto = amountToExtract.value ?: BigDecimal.ZERO
),
feeState = sendState.feeState.copy(
includeFeeSwitcherIsEnabled = amountToExtract.type == AmountType.Coin
)
walletManager = walletManager,
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) },
amountState = sendState.amountState.copy(
amountToExtract = amountToExtract,
typeOfAmount = amountToExtract.type,
balanceCrypto = amountToExtract.value ?: BigDecimal.ZERO,
),
feeState = sendState.feeState.copy(
includeFeeSwitcherIsEnabled = amountToExtract.type == AmountType.Coin,
),
)
}
}

View file

@ -24,7 +24,7 @@ class TransactionExtrasReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
return when (action) {
is Prepare -> handleInitialization(action, sendState)
Release -> handleRelease(action, sendState)
Release -> handleRelease(sendState)
is XlmMemo -> handleXlmMemo(action, sendState, sendState.transactionExtrasState)
is BinanceMemo -> handleBinanceMemo(action, sendState, sendState.transactionExtrasState)
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
@ -43,8 +43,10 @@ class TransactionExtrasReducer : SendInternalReducer {
if (tag == null) {
TransactionExtrasState(xrpDestinationTag = XrpDestinationTagState())
} else {
TransactionExtrasState(xrpDestinationTag = XrpDestinationTagState(
InputViewValue("$tag", false), tag)
TransactionExtrasState(
xrpDestinationTag = XrpDestinationTagState(
InputViewValue("$tag", false), tag,
),
)
}
} else {
@ -58,7 +60,7 @@ class TransactionExtrasReducer : SendInternalReducer {
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleRelease(action: SendScreenAction, sendState: SendState): SendState {
private fun handleRelease(sendState: SendState): SendState {
val result = TransactionExtrasState()
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}

View file

@ -59,6 +59,7 @@ data class XlmMemoState(
else -> XlmMemoType.TEXT
}
@Suppress("MagicNumber")
fun isAssignableValue(value: String): Boolean = when (determineMemoType(value)) {
XlmMemoType.TEXT -> {
// from org.stellar.sdk.MemoText
@ -67,7 +68,7 @@ data class XlmMemoState(
XlmMemoType.ID -> {
try {
// from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
value.toBigInteger() in BigInteger.ZERO..(Long.MAX_VALUE.toBigInteger() * 2.toBigInteger())
value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
} catch (ex: NumberFormatException) {
false
}

View file

@ -30,19 +30,19 @@ enum class StateId {
interface SendScreenState : StateType, IdStateHolder
data class SendState(
val walletManager: WalletManager? = null,
val coinConverter: CurrencyConverter? = null,
val tokenConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
val receiptState: ReceiptState = ReceiptState(),
val sendWarningsList: List<WarningMessage> = listOf(),
val sendButtonState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.DISABLED),
val dialog: StateDialog? = null,
val externalTransactionData: ExternalTransactionData? = null
val walletManager: WalletManager? = null,
val coinConverter: CurrencyConverter? = null,
val tokenConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
val receiptState: ReceiptState = ReceiptState(),
val sendWarningsList: List<WarningMessage> = listOf(),
val sendButtonState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.DISABLED),
val dialog: StateDialog? = null,
val externalTransactionData: ExternalTransactionData? = null,
) : SendScreenState {
override val stateId: StateId = StateId.SEND_SCREEN
@ -114,7 +114,7 @@ data class SendState(
fun isReadyToRequestFee(): Boolean = addressPayIdIsReady() && amountIsReady()
fun isReadyToSend(): Boolean = addressPayIdIsReady() && amountIsReady() &&
store.state.sendState.feeState.isReady()
store.state.sendState.feeState.isReady()
}
}
@ -144,10 +144,13 @@ data class AmountState(
fun isCoinAmount(): Boolean = typeOfAmount == AmountType.Coin
fun createMainCurrency(type: MainCurrencyType, canSwitched: Boolean): MainCurrency {
return if (!canSwitched) MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE", false)
else when (type) {
MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency.code)
MainCurrencyType.CRYPTO -> MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE")
return if (!canSwitched) {
MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE", false)
} else {
when (type) {
MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency.code)
MainCurrencyType.CRYPTO -> MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE")
}
}
}
@ -166,9 +169,9 @@ enum class MainCurrencyType {
}
data class MainCurrency(
val type: MainCurrencyType,
val currencySymbol: String,
val isEnabled: Boolean = true
val type: MainCurrencyType,
val currencySymbol: String,
val isEnabled: Boolean = true,
)
data class ExternalTransactionData(
@ -176,5 +179,5 @@ data class ExternalTransactionData(
val destinationAddress: String,
val transactionId: String,
val canAmountBeModified: Boolean = false,
val canDestinationAddressBeModified: Boolean = false
val canDestinationAddressBeModified: Boolean = false,
)

View file

@ -1,3 +1,5 @@
@file:Suppress("MagicNumber")
package com.tangem.tap.features.send.ui
import android.content.Context
@ -14,9 +16,9 @@ import androidx.recyclerview.widget.RecyclerView
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.core.analytics.Analytics
import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.getFromClipboard
@ -30,10 +32,25 @@ import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.CheckClipboard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.PasteAddressPayId
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.SetTruncateHandler
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.TruncateOrRestore
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
import com.tangem.tap.features.send.redux.AmountActionUi.SetMaxAmount
import com.tangem.tap.features.send.redux.AmountActionUi.ToggleMainCurrency
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeIncludeFee
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeSelectedFee
import com.tangem.tap.features.send.redux.FeeActionUi.ToggleControlsVisibility
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.ReleaseSendState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
@ -45,7 +62,12 @@ import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentSendBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.text.DecimalFormatSymbols
/**
@ -85,13 +107,16 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private fun initSendButtonStates() = with(binding) {
btnSend.setOnClickListener {
store.dispatch(SendActionUi.SendAmountToRecipient(
Message(getString(R.string.initial_message_sign_header))
))
store.dispatch(
SendActionUi.SendAmountToRecipient(
Message(getString(R.string.initial_message_sign_header)),
),
)
}
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
@Suppress("MagicNumber")
private fun setupAddressOrPayIdLayout() = with(binding.lSendAddressPayid) {
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
@ -100,12 +125,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
store.dispatch(TruncateOrRestore(!hasFocus))
}
etAddressOrPayId.inputtedTextAsFlow()
.debounce(400)
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
}
.launchIn(mainScope)
.debounce(400)
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
}
.launchIn(mainScope)
imvPaste.setOnClickListener {
Analytics.send(Token.Send.ButtonPaste())
@ -116,29 +141,29 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
Analytics.send(Token.Send.ButtonQRCode())
startActivityForResult(
Intent(requireContext(), ScanQrCodeActivity::class.java),
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE,
)
}
}
private fun setupTransactionExtrasLayout() = with(binding.lSendAddressPayid) {
etXlmMemo.inputtedTextAsFlow()
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xlmMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
.launchIn(mainScope)
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xlmMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
.launchIn(mainScope)
etDestinationTag.inputtedTextAsFlow()
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xrpDestinationTag?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
.launchIn(mainScope)
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xrpDestinationTag?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
.launchIn(mainScope)
etBinanceMemo.inputtedTextAsFlow()
.debounce(400)
@ -159,10 +184,13 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
binding.lSendAddressPayid.imvQrCode.postDelayed({
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused))
}, 200)
binding.lSendAddressPayid.imvQrCode.postDelayed(
{
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused))
},
200,
)
}
private fun setupAmountLayout() {
@ -221,10 +249,10 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
etAmountToSend.inputtedTextAsFlow()
.debounce(400)
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
.launchIn(mainScope)
.debounce(400)
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
.launchIn(mainScope)
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
it.hideSoftKeyboard()
@ -287,7 +315,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
store.dispatch(NavigationAction.PopBackTo())
} else {
store.dispatch(
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId),
)
}
}
@ -309,24 +337,22 @@ fun EditText.inputtedTextAsFlow(): Flow<String> = callbackFlow {
awaitClose { removeTextChangedListener(watcher) }
}
class FeeUiHelper {
companion object {
fun toId(fee: FeeType): Int {
return when (fee) {
FeeType.SINGLE -> View.NO_ID
FeeType.LOW -> R.id.chipLow
FeeType.NORMAL -> R.id.chipNormal
FeeType.PRIORITY -> R.id.chipPriority
}
object FeeUiHelper {
fun toId(fee: FeeType): Int {
return when (fee) {
FeeType.SINGLE -> View.NO_ID
FeeType.LOW -> R.id.chipLow
FeeType.NORMAL -> R.id.chipNormal
FeeType.PRIORITY -> R.id.chipPriority
}
}
fun toType(id: Int): FeeType {
return when (id) {
R.id.chipLow -> FeeType.LOW
R.id.chipNormal -> FeeType.NORMAL
R.id.chipPriority -> FeeType.PRIORITY
else -> FeeType.NORMAL
}
fun toType(id: Int): FeeType {
return when (id) {
R.id.chipLow -> FeeType.LOW
R.id.chipNormal -> FeeType.NORMAL
R.id.chipPriority -> FeeType.PRIORITY
else -> FeeType.NORMAL
}
}
}

View file

@ -11,22 +11,19 @@ import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class RequestFeeErrorDialog {
object RequestFeeErrorDialog {
fun create(context: Context, dialog: SendAction.Dialog.RequestFeeError): AlertDialog {
val errorMessage = dialog.error.customMessage
companion object {
fun create(context: Context, dialog: SendAction.Dialog.RequestFeeError): AlertDialog {
val errorMessage = dialog.error.customMessage
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_fee_error)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
setNeutralButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_fee_error)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
setNeutralButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
}

View file

@ -16,28 +16,26 @@ import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class SendTransactionFailsDialog {
companion object {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog {
return create(context, dialog.error.localizedDescription(context))
}
object SendTransactionFailsDialog {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog {
return create(context, dialog.error.localizedDescription(context))
}
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog {
val errorConverter = BlockchainSdkErrorConverter(context)
return create(context, errorConverter.convert(dialog.error))
}
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog {
val errorConverter = BlockchainSdkErrorConverter(context)
return create(context, errorConverter.convert(dialog.error))
}
private fun create(context: Context, errorMessage: String): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
private fun create(context: Context, errorMessage: String): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
}

View file

@ -69,6 +69,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
}
}
@Suppress("ComplexMethod")
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
with(fg.binding.lSendAddressPayid) {
@ -220,7 +221,11 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
tilAmountToSend.enableError(false)
}
val filter = DecimalDigitsInputFilter(12, state.maxLengthOfAmount, state.decimalSeparator)
val filter = DecimalDigitsInputFilter(
digitsBeforeDecimal = 12,
digitsAfterDecimal = state.maxLengthOfAmount,
decimalSeparator = state.decimalSeparator,
)
etAmountToSend.filters = arrayOf(filter)
val amountToSend = state.viewAmountValue
if (!amountToSend.isFromUserInput) etAmountToSend.update(amountToSend.value)
@ -249,8 +254,8 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
tvAmountCurrency.setTextColor(fg.getColor(textColor))
}
@Suppress("MagicNumber")
private fun handleFeeState(fg: SendFragment, state: FeeState) = with(fg.binding.clNetworkFee) {
// chipGroup.fitChipsByGroupWidth()
fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible)
flExpandCollapse.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f
llFeeControlsContainer.show(state.controlsLayoutIsVisible)
@ -265,6 +270,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId)
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleReceiptState(
fg: SendFragment,
state: ReceiptState,