Updated on 2026-08-14
This commit is contained in:
parent
459db3d0df
commit
f43f6299d1
62 changed files with 2 additions and 5914 deletions
|
|
@ -18,7 +18,6 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
|
|||
import com.tangem.domain.staking.FetchStakingTokensUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.features.main.model.MainScreenState
|
||||
|
|
@ -42,7 +41,6 @@ internal class MainViewModel @Inject constructor(
|
|||
private val blockchainSDKFactory: BlockchainSDKFactory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
|
|
@ -68,7 +66,6 @@ internal class MainViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
|
||||
|
||||
updateAppCurrencies()
|
||||
updateSendFeatureToggle()
|
||||
observeFlips()
|
||||
displayBalancesHidingStatusToast()
|
||||
displayHiddenBalancesModalNotification()
|
||||
|
|
@ -130,12 +127,6 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateSendFeatureToggle() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
sendFeatureToggles.fetchNewSendEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeFlips() {
|
||||
listenToFlipsUseCase().launchIn(viewModelScope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux
|
||||
|
||||
import com.tangem.Message
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.FeePaidCurrency
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
|
||||
import com.tangem.tap.common.redux.ToastNotificationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.features.send.redux.states.ButtonState
|
||||
import com.tangem.tap.features.send.redux.states.FeeType
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SendScreenAction : Action
|
||||
interface SendScreenActionUi : SendScreenAction
|
||||
|
||||
object ReleaseSendState : Action
|
||||
|
||||
data class PrepareSendScreen(
|
||||
val walletManager: WalletManager,
|
||||
val feePaidCurrency: FeePaidCurrency,
|
||||
val currency: CryptoCurrency,
|
||||
val coinAmount: Amount?,
|
||||
val coinRate: BigDecimal?,
|
||||
val tokenAmount: Amount? = null,
|
||||
val tokenRate: BigDecimal? = null,
|
||||
val feeCurrencyRate: BigDecimal? = null,
|
||||
val feeCurrencyDecimals: Int = 0,
|
||||
) : SendScreenAction
|
||||
|
||||
// Address
|
||||
sealed class AddressActionUi : SendScreenActionUi {
|
||||
data class HandleUserInput(val data: String) : AddressActionUi()
|
||||
data class PasteAddress(val data: String, val sourceType: AddressEntered.SourceType) : AddressActionUi()
|
||||
data class CheckClipboard(val data: String?) : AddressActionUi()
|
||||
data class CheckAddress(val sourceType: AddressEntered.SourceType?) : AddressActionUi()
|
||||
data class SetTruncateHandler(val handler: (String) -> String) : AddressActionUi()
|
||||
data class TruncateOrRestore(val truncate: Boolean) : AddressActionUi()
|
||||
}
|
||||
|
||||
sealed class TransactionExtrasAction : SendScreenActionUi {
|
||||
data class Prepare(
|
||||
val blockchain: Blockchain,
|
||||
val walletAddress: String,
|
||||
val xrpTag: String?,
|
||||
) : TransactionExtrasAction()
|
||||
|
||||
object Release : TransactionExtrasAction()
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class XlmMemo : TransactionExtrasAction() {
|
||||
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
|
||||
data class HandleUserInput(val data: String) : XlmMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class BinanceMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : BinanceMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class XrpDestinationTag : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : XrpDestinationTag()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class TonMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : TonMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class CosmosMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : CosmosMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class HederaMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : HederaMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class AlgorandMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : AlgorandMemo()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AddressVerifyAction : SendScreenAction {
|
||||
enum class Error {
|
||||
ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN,
|
||||
ADDRESS_SAME_AS_WALLET,
|
||||
}
|
||||
|
||||
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressVerifyAction()
|
||||
|
||||
sealed class AddressVerification : AddressVerifyAction() {
|
||||
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 SetMainCurrency(val mainCurrency: MainCurrencyType) : AmountActionUi()
|
||||
object ToggleMainCurrency : AmountActionUi()
|
||||
}
|
||||
|
||||
sealed class AmountAction : SendScreenAction {
|
||||
data class SetAmount(val amountCrypto: BigDecimal, val isUserInput: Boolean) : AmountAction()
|
||||
data class SetAmountError(val error: TapError?) : AmountAction()
|
||||
data class SetDecimalSeparator(val separator: String) : AmountAction()
|
||||
data class HideBalance(val hide: Boolean) : AmountAction()
|
||||
}
|
||||
|
||||
// Fee
|
||||
sealed class FeeActionUi : SendScreenActionUi {
|
||||
object ToggleControlsVisibility : FeeActionUi()
|
||||
data class ChangeSelectedFee(val feeType: FeeType) : FeeActionUi()
|
||||
class ChangeIncludeFee(val isIncluded: Boolean) : FeeActionUi()
|
||||
}
|
||||
|
||||
sealed class FeeAction : SendScreenAction {
|
||||
|
||||
object RequestFee : FeeAction()
|
||||
sealed class FeeCalculation : FeeAction() {
|
||||
data class SetFeeResult(val fee: TransactionFee) : FeeCalculation()
|
||||
object ClearResult : FeeCalculation()
|
||||
}
|
||||
|
||||
data class ChangeLayoutVisibility(
|
||||
val main: Boolean? = null,
|
||||
val controls: Boolean? = null,
|
||||
val chipGroup: Boolean? = null,
|
||||
) : FeeAction()
|
||||
}
|
||||
|
||||
sealed class ReceiptAction : SendScreenAction {
|
||||
object RefreshReceipt : ReceiptAction()
|
||||
}
|
||||
|
||||
sealed class SendActionUi : SendScreenActionUi {
|
||||
data class SendAmountToRecipient(val messageForSigner: Message) : SendScreenActionUi
|
||||
object CheckIfTransactionDataWasProvided : SendScreenActionUi
|
||||
}
|
||||
|
||||
sealed class SendAction : SendScreenAction {
|
||||
|
||||
data class ChangeSendButtonState(val state: ButtonState) : SendAction()
|
||||
object SendSuccess : SendAction(), ToastNotificationAction {
|
||||
override val messageResource: Int = R.string.send_transaction_success
|
||||
}
|
||||
|
||||
sealed class Dialog : SendAction(), StateDialog {
|
||||
data class TezosWarningDialog(
|
||||
val reduceCallback: () -> Unit,
|
||||
val sendAllCallback: () -> Unit,
|
||||
val reduceAmount: BigDecimal,
|
||||
) : Dialog()
|
||||
|
||||
data class KaspaWarningDialog(
|
||||
val maxOutputs: Int,
|
||||
val maxAmount: BigDecimal,
|
||||
val onOk: () -> Unit,
|
||||
) : Dialog()
|
||||
|
||||
data class ChiaWarningDialog(
|
||||
val blockchainName: String,
|
||||
val maxOutputs: Int,
|
||||
val maxAmount: BigDecimal,
|
||||
val onOk: () -> Unit,
|
||||
) : Dialog()
|
||||
|
||||
sealed class SendTransactionFails : Dialog() {
|
||||
data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog()
|
||||
data class BlockchainSdkError(
|
||||
val error: com.tangem.blockchain.common.BlockchainSdkError,
|
||||
val scanResponse: ScanResponse,
|
||||
) : Dialog()
|
||||
}
|
||||
|
||||
data class RequestFeeError(
|
||||
val error: com.tangem.blockchain.common.BlockchainSdkError,
|
||||
val scanResponse: ScanResponse,
|
||||
val onRetry: () -> Unit,
|
||||
) : Dialog()
|
||||
|
||||
object Hide : Dialog()
|
||||
}
|
||||
|
||||
sealed class Warnings : SendAction() {
|
||||
object Update : SendAction()
|
||||
data class Set(val warningList: List<WarningMessage>) : SendAction()
|
||||
}
|
||||
|
||||
data class SendSpecificTransaction(
|
||||
val sendAmount: String,
|
||||
val destinationAddress: String,
|
||||
val transactionId: String,
|
||||
) : SendAction()
|
||||
}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
|
||||
import com.tangem.tap.mainScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AddressMiddleware {
|
||||
|
||||
private val addressValidator = AddressValidator()
|
||||
|
||||
fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
when (action) {
|
||||
is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
|
||||
is AddressActionUi.PasteAddress -> pasteAddress(action.data, action.sourceType, dispatch)
|
||||
is AddressActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
|
||||
is AddressActionUi.CheckAddress -> verifyAddress(action.sourceType, appState, dispatch)
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserInput(input: String, appState: AppState?, dispatch: DispatchFunction) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
if (input == sendState.addressState.viewFieldValue.value) return
|
||||
|
||||
setAddressAndCheck(data = input, sourceType = null, isUserInput = true, dispatch = dispatch)
|
||||
}
|
||||
|
||||
private fun pasteAddress(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
|
||||
setAddressAndCheck(data = data, sourceType = sourceType, isUserInput = false, dispatch = dispatch)
|
||||
}
|
||||
|
||||
private fun setAddressAndCheck(
|
||||
data: String,
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
isUserInput: Boolean,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
dispatch(SetWalletAddress(data, isUserInput))
|
||||
dispatch(AddressActionUi.CheckAddress(sourceType))
|
||||
}
|
||||
|
||||
private fun verifyAddress(
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
appState: AppState?,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val walletManager = sendState.walletManager ?: return
|
||||
val address = sendState.addressState.normalFieldValue ?: return
|
||||
val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput
|
||||
|
||||
verifyAddress(
|
||||
address = address,
|
||||
walletManager = walletManager,
|
||||
isUserInput = isUserInput,
|
||||
dispatch = dispatch,
|
||||
sourceType = sourceType,
|
||||
)
|
||||
}
|
||||
|
||||
private fun verifyAddress(
|
||||
address: String,
|
||||
walletManager: WalletManager,
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
isUserInput: Boolean,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
val wallet = walletManager.wallet
|
||||
|
||||
mainScope.launch {
|
||||
val failReason = withContext(Dispatchers.IO) {
|
||||
addressValidator.validateAddress(walletManager, address)
|
||||
}
|
||||
|
||||
if (failReason == null) {
|
||||
dispatchSuccessValidationActions(
|
||||
address = address,
|
||||
wallet = wallet,
|
||||
sourceType = sourceType,
|
||||
isUserInput = isUserInput,
|
||||
dispatch = dispatch,
|
||||
)
|
||||
} else {
|
||||
dispatchFailedValidationActions(
|
||||
failReason = failReason,
|
||||
sourceType = sourceType,
|
||||
dispatch = dispatch,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchSuccessValidationActions(
|
||||
address: String,
|
||||
wallet: Wallet,
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
isUserInput: Boolean,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
val addressSchemeSplit = when (wallet.blockchain) {
|
||||
Blockchain.BitcoinCash, Blockchain.Kaspa -> listOf(address)
|
||||
else -> address.split(":")
|
||||
}
|
||||
|
||||
val noSchemeAddress = when (addressSchemeSplit.size) {
|
||||
1 -> address // no scheme
|
||||
2 -> { // scheme
|
||||
if (wallet.blockchain.validateShareScheme(addressSchemeSplit[0])) {
|
||||
addressSchemeSplit[1]
|
||||
} else {
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Fail,
|
||||
),
|
||||
)
|
||||
}
|
||||
dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN))
|
||||
return
|
||||
}
|
||||
}
|
||||
else -> { // invalid URI
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Fail,
|
||||
),
|
||||
)
|
||||
}
|
||||
dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val supposedAddress = noSchemeAddress.removeShareUriQuery() // TODO: parse query?
|
||||
|
||||
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
|
||||
dispatch(AmountAction.SetAmount(it, false))
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
}
|
||||
dispatch(SetWalletAddress(supposedAddress, isUserInput))
|
||||
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
|
||||
dispatch(FeeAction.RequestFee)
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Success,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchFailedValidationActions(
|
||||
failReason: Error,
|
||||
sourceType: AddressEntered.SourceType?,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
dispatch(SetAddressError(failReason))
|
||||
dispatch(TransactionExtrasAction.Release)
|
||||
sourceType?.let {
|
||||
Analytics.send(
|
||||
event = AddressEntered(
|
||||
sourceType = sourceType,
|
||||
validationResult = AddressEntered.ValidationResult.Fail,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.removeShareUriQuery(): String = this.substringBefore("?")
|
||||
|
||||
private fun String.getQueryParameter(name: String): String? {
|
||||
return this.substringAfter("?").splitToMap("&", "=")[name]
|
||||
}
|
||||
|
||||
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
|
||||
val address = input ?: return
|
||||
val walletManager = appState?.sendState?.walletManager ?: return
|
||||
|
||||
val internalDispatcher: (Action) -> Unit = {
|
||||
when (it) {
|
||||
is SetWalletAddress -> {
|
||||
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(true))
|
||||
}
|
||||
is SetAddressError -> {
|
||||
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifyAddress(
|
||||
address = address,
|
||||
walletManager = walletManager,
|
||||
sourceType = null,
|
||||
isUserInput = false,
|
||||
dispatch = internalDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map<String, String> {
|
||||
return this
|
||||
.split(firstDelimiter)
|
||||
.map { it.split(secondDelimiter) }
|
||||
.associate { it.first() to it.last() }
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.blockchains.near.NearWalletManager
|
||||
import com.tangem.blockchain.blockchains.near.network.NearAccount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction
|
||||
|
||||
internal class AddressValidator {
|
||||
|
||||
suspend fun validateAddress(walletManager: WalletManager, address: String): AddressVerifyAction.Error? {
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
val wallet = walletManager.wallet
|
||||
|
||||
return if (blockchain.isNear()) {
|
||||
validateNearAddress(walletManager, address)
|
||||
} else {
|
||||
validateAddress(wallet, address)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateNearAddress(
|
||||
walletManager: WalletManager,
|
||||
address: String,
|
||||
): AddressVerifyAction.Error? {
|
||||
// implicit address validation
|
||||
if (address.length == NEAR_IMPLICIT_ADDRESS_LENGTH && hexRegex.matches(address)) {
|
||||
return validateAddress(walletManager.wallet, address)
|
||||
}
|
||||
|
||||
// named address validation
|
||||
if (address.length in NEAR_MIN_ADDRESS_LENGTH until NEAR_IMPLICIT_ADDRESS_LENGTH &&
|
||||
nearAddressRegex.matches(address)
|
||||
) {
|
||||
val result = (walletManager as? NearWalletManager)?.getAccount(address)
|
||||
return if (result is Result.Success && result.data is NearAccount.Full) {
|
||||
null
|
||||
} else {
|
||||
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
|
||||
}
|
||||
}
|
||||
|
||||
return AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
|
||||
}
|
||||
|
||||
private fun Blockchain.isNear(): Boolean {
|
||||
return this == Blockchain.Near || this == Blockchain.NearTestnet
|
||||
}
|
||||
|
||||
private fun validateAddress(wallet: Wallet, address: String): AddressVerifyAction.Error? {
|
||||
return if (wallet.blockchain.validateAddress(address)) {
|
||||
if (wallet.addresses.all { it.value != address }) {
|
||||
null
|
||||
} else {
|
||||
AddressVerifyAction.Error.ADDRESS_SAME_AS_WALLET
|
||||
}
|
||||
} else {
|
||||
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NEAR_MIN_ADDRESS_LENGTH = 2
|
||||
private const val NEAR_IMPLICIT_ADDRESS_LENGTH = 64
|
||||
private val hexRegex = Regex("^[0-9a-f]+$")
|
||||
private val nearAddressRegex = Regex("^(([a-z\\d]+[\\-_])*[a-z\\d]+\\.)*([a-z\\d]+[\\-_])*[a-z\\d]+\$")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
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.redux.AppState
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi
|
||||
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.features.send.redux.states.SendState
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AmountMiddleware {
|
||||
|
||||
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 amountState = sendState.amountState
|
||||
|
||||
var input = amountState.toBigDecimalSeparator(data)
|
||||
input = if (input == ".") "0.0" else input
|
||||
val inputValue = when {
|
||||
input.isEmpty() || input == "0" -> BigDecimal.ZERO
|
||||
else -> BigDecimal(input)
|
||||
}
|
||||
if (inputValue.isZero() && amountState.amountToSendCrypto.isZero()) return
|
||||
|
||||
val inputValueCrypto = if (amountState.mainCurrency.type == MainCurrencyType.CRYPTO) {
|
||||
inputValue
|
||||
} else {
|
||||
sendState.convertFiatToExtractCrypto(inputValue)
|
||||
}
|
||||
|
||||
dispatch(AmountAction.SetAmount(inputValueCrypto, true))
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
dispatch(FeeAction.RequestFee)
|
||||
}
|
||||
|
||||
private fun checkAmountToSend(appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val walletManager = sendState.walletManager ?: return
|
||||
val typedAmount = sendState.amountState.amountToExtract ?: return
|
||||
|
||||
val inputCrypto = sendState.amountState.amountToSendCrypto
|
||||
if (sendState.amountState.viewAmountValue.value == "0" && inputCrypto.isZero()) {
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
|
||||
return
|
||||
}
|
||||
|
||||
val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend(inputCrypto))
|
||||
val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee?.amount)
|
||||
val amountFieldErrors = filterErrorsForAmountField(transactionErrors)
|
||||
if (amountFieldErrors.isEmpty()) {
|
||||
dispatch(AmountAction.SetAmountError(null))
|
||||
} else {
|
||||
dispatch(AmountAction.SetAmountError(createValidateTransactionError(amountFieldErrors, walletManager)))
|
||||
}
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
|
||||
}
|
||||
|
||||
private fun setMaxAmount(appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
|
||||
dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false))
|
||||
if (sendState.amountState.canIncludeFee()) {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true))
|
||||
dispatch(FeeActionUi.ChangeIncludeFee(true))
|
||||
}
|
||||
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
if (SendState.isReadyToRequestFee()) dispatch(FeeAction.RequestFee)
|
||||
}
|
||||
|
||||
private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
val amountState = appState?.sendState?.amountState ?: return
|
||||
if (!appState.sendState.coinIsConvertible()) return
|
||||
|
||||
val type = if (amountState.mainCurrency.type == MainCurrencyType.FIAT) {
|
||||
MainCurrencyType.CRYPTO
|
||||
} else {
|
||||
MainCurrencyType.FIAT
|
||||
}
|
||||
|
||||
dispatch(AmountActionUi.SetMainCurrency(type))
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<TransactionError> {
|
||||
val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java)
|
||||
errors.forEach {
|
||||
when (it) {
|
||||
TransactionError.AmountExceedsBalance -> {
|
||||
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
|
||||
showIntoAmountField.add(it)
|
||||
}
|
||||
TransactionError.FeeExceedsBalance -> {
|
||||
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
|
||||
showIntoAmountField.add(it)
|
||||
}
|
||||
TransactionError.TotalExceedsBalance -> {
|
||||
val notAcceptable = listOf(TransactionError.AmountExceedsBalance, TransactionError.FeeExceedsBalance)
|
||||
if (!showIntoAmountField.containsAll(notAcceptable)) showIntoAmountField.add(it)
|
||||
showIntoAmountField.remove(TransactionError.AmountLowerExistentialDeposit)
|
||||
}
|
||||
else -> showIntoAmountField.add(it)
|
||||
}
|
||||
}
|
||||
showIntoAmountField.remove(TransactionError.TezosSendAll)
|
||||
|
||||
return showIntoAmountField
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.domain.demo.DemoTransactionSender
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
import com.tangem.tap.features.send.redux.ReceiptAction
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.DispatchFunction
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RequestFeeMiddleware {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun handle(appState: AppState?, dispatch: DispatchFunction) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val walletManager = sendState.walletManager ?: return
|
||||
val scanResponse = appState.globalState.scanResponse ?: return
|
||||
|
||||
if (!SendState.isReadyToRequestFee()) {
|
||||
dispatch(FeeAction.FeeCalculation.ClearResult)
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
|
||||
return
|
||||
}
|
||||
val typedAmount = sendState.amountState.amountToExtract ?: return
|
||||
|
||||
val destinationAddress = sendState.addressState.destinationWalletAddress!!
|
||||
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
|
||||
val txSender = if (scanResponse.isDemoCard()) {
|
||||
DemoTransactionSender(walletManager)
|
||||
} else {
|
||||
walletManager
|
||||
}
|
||||
scope.launch {
|
||||
val feeResult = txSender.getFee(destinationAmount, destinationAddress)
|
||||
withContext(Dispatchers.Main) {
|
||||
when (feeResult) {
|
||||
is Result.Success -> {
|
||||
val result = feeResult.data
|
||||
// val result = FeeMock.getFee(walletManager.wallet.blockchain)
|
||||
dispatch(FeeAction.FeeCalculation.SetFeeResult(result))
|
||||
when (result) {
|
||||
is TransactionFee.Single -> {
|
||||
val fee = result.normal.amount.value ?: BigDecimal.ZERO
|
||||
if (fee.isZero()) {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
|
||||
} else {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false))
|
||||
}
|
||||
}
|
||||
is TransactionFee.Choosable -> {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
dispatch(FeeAction.FeeCalculation.ClearResult)
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
|
||||
|
||||
val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles)
|
||||
if (featureToggles.isLocalLogsEnabled) {
|
||||
saveBlockchainError(feeResult, destinationAddress, destinationAmount, walletManager)
|
||||
} else {
|
||||
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
|
||||
walletManager = walletManager,
|
||||
amountToSend = destinationAmount,
|
||||
feeAmount = null,
|
||||
destinationAddress = destinationAddress,
|
||||
)
|
||||
}
|
||||
|
||||
val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext
|
||||
dispatch(
|
||||
SendAction.Dialog.RequestFeeError(
|
||||
error = blockchainSdkError,
|
||||
scanResponse = scanResponse,
|
||||
onRetry = { dispatch(FeeAction.RequestFee) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBlockchainError(
|
||||
feeResult: Result.Failure,
|
||||
destinationAddress: String,
|
||||
destinationAmount: Amount,
|
||||
walletManager: WalletManager,
|
||||
) {
|
||||
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = (feeResult.error as? BlockchainSdkError)?.customMessage
|
||||
?: "It isn't BlockchainSdkError",
|
||||
blockchainId = walletManager.wallet.blockchain.id,
|
||||
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "",
|
||||
destinationAddress = destinationAddress,
|
||||
tokenSymbol = if (destinationAmount.type is AmountType.Token) {
|
||||
destinationAmount.currencySymbol
|
||||
} else {
|
||||
""
|
||||
},
|
||||
amount = destinationAmount.value?.stripZeroPlainString() ?: "0",
|
||||
fee = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,455 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchainsdk.utils.minimalAmount
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.demo.DemoTransactionSender
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.analytics.events.Token.Send.SelectedCurrency.CurrencyType
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TangemSigner
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
|
||||
import com.tangem.tap.features.send.redux.states.*
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SendMiddleware {
|
||||
val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is AddressActionUi -> AddressMiddleware().handle(action, appState(), dispatch)
|
||||
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
|
||||
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
|
||||
is SendActionUi.SendAmountToRecipient ->
|
||||
verifyAndSendTransaction(action, appState(), dispatch)
|
||||
is SendAction.Warnings.Update -> updateWarnings(dispatch)
|
||||
is SendActionUi.CheckIfTransactionDataWasProvided -> {
|
||||
val transactionData = appState()?.sendState?.externalTransactionData
|
||||
if (transactionData != null) {
|
||||
store.dispatchOnMain(
|
||||
AddressVerifyAction.AddressVerification.SetWalletAddress(
|
||||
address = transactionData.destinationAddress,
|
||||
isUserInput = false,
|
||||
),
|
||||
)
|
||||
store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO))
|
||||
store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount))
|
||||
store.dispatchOnMain(
|
||||
AmountAction.SetAmount(
|
||||
transactionData.amount.toBigDecimal(),
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyAndSendTransaction(
|
||||
action: SendActionUi.SendAmountToRecipient,
|
||||
appState: AppState?,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val walletManager = sendState.walletManager ?: return
|
||||
val card = appState.globalState.scanResponse?.card ?: return
|
||||
val destinationAddress = sendState.addressState.destinationWalletAddress ?: return
|
||||
val typedAmount = sendState.amountState.amountToExtract ?: return
|
||||
val fee = sendState.feeState.currentFee ?: return
|
||||
|
||||
val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend())
|
||||
|
||||
val transactionErrors = walletManager.validateTransaction(amountToSend, fee.amount)
|
||||
when {
|
||||
transactionErrors.contains(TransactionError.TezosSendAll) -> {
|
||||
val reduceAmount = walletManager.wallet.blockchain.minimalAmount()
|
||||
dispatch(
|
||||
SendAction.Dialog.TezosWarningDialog(
|
||||
reduceCallback = {
|
||||
dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false))
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
},
|
||||
sendAllCallback = {
|
||||
sendTransaction(
|
||||
action = action,
|
||||
walletManager = walletManager,
|
||||
amountToSend = amountToSend,
|
||||
fee = fee,
|
||||
feeType = sendState.feeState.selectedFeeType,
|
||||
destinationAddress = destinationAddress,
|
||||
transactionExtras = sendState.transactionExtrasState,
|
||||
card = card,
|
||||
externalTransactionData = sendState.externalTransactionData,
|
||||
mainCurrencyType = sendState.amountState.mainCurrency.type,
|
||||
dispatch = dispatch,
|
||||
)
|
||||
},
|
||||
reduceAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
sendTransaction(
|
||||
action = action,
|
||||
walletManager = walletManager,
|
||||
amountToSend = amountToSend,
|
||||
fee = fee,
|
||||
feeType = sendState.feeState.selectedFeeType,
|
||||
destinationAddress = destinationAddress,
|
||||
transactionExtras = sendState.transactionExtrasState,
|
||||
card = card,
|
||||
externalTransactionData = sendState.externalTransactionData,
|
||||
mainCurrencyType = sendState.amountState.mainCurrency.type,
|
||||
dispatch = dispatch,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod", "ComplexMethod")
|
||||
private fun sendTransaction(
|
||||
action: SendActionUi.SendAmountToRecipient,
|
||||
walletManager: WalletManager,
|
||||
amountToSend: Amount,
|
||||
fee: Fee,
|
||||
feeType: FeeType,
|
||||
destinationAddress: String,
|
||||
transactionExtras: TransactionExtrasState,
|
||||
card: CardDTO,
|
||||
externalTransactionData: ExternalTransactionData?,
|
||||
mainCurrencyType: MainCurrencyType,
|
||||
dispatch: (Action) -> Unit,
|
||||
) {
|
||||
dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS))
|
||||
var txData = walletManager.createTransaction(amountToSend, fee, destinationAddress)
|
||||
|
||||
transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) }
|
||||
transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) }
|
||||
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) }
|
||||
transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) }
|
||||
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
|
||||
transactionExtras.hederaMemoState?.memo?.let { txData = txData.copy(extras = HederaTransactionExtras(it)) }
|
||||
transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) }
|
||||
|
||||
scope.launch {
|
||||
// TODO: Risky commented this part, unknown logic, need to test if removed
|
||||
// TODO: [REDACTED_JIRA]
|
||||
// val updateWalletResult = walletManager.safeUpdate()
|
||||
// if (updateWalletResult is Result.Failure) {
|
||||
// withMainContext {
|
||||
// when (val error = updateWalletResult.error) {
|
||||
// is TapError -> store.dispatchErrorNotification(error)
|
||||
// is BlockchainSdkError -> {
|
||||
// updateFeedbackManagerInfo(
|
||||
// walletManager = walletManager,
|
||||
// amountToSend = amountToSend,
|
||||
// feeAmount = fee.amount,
|
||||
// destinationAddress = destinationAddress,
|
||||
// )
|
||||
// dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error))
|
||||
// }
|
||||
// else -> {
|
||||
// val tapError = if (error.message == null) {
|
||||
// TapError.UnknownError
|
||||
// } else {
|
||||
// TapError.CustomError(error.message!!)
|
||||
// }
|
||||
// store.dispatchErrorNotification(tapError)
|
||||
// }
|
||||
// }
|
||||
// dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
|
||||
// }
|
||||
// return@launch
|
||||
// }
|
||||
|
||||
val tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk
|
||||
val linkedTerminalState = tangemSdk.config.linkedTerminal
|
||||
if (card.isStart2Coin) {
|
||||
tangemSdk.config.linkedTerminal = false
|
||||
}
|
||||
|
||||
val signer = TangemSigner(
|
||||
card = card,
|
||||
tangemSdk = tangemSdk,
|
||||
initialMessage = action.messageForSigner,
|
||||
) { signResponse ->
|
||||
store.dispatch(
|
||||
GlobalAction.UpdateWalletSignedHashes(
|
||||
walletSignedHashes = signResponse.totalSignedHashes,
|
||||
walletPublicKey = walletManager.wallet.publicKey.seedKey,
|
||||
remainingSignatures = signResponse.remainingSignatures,
|
||||
),
|
||||
)
|
||||
}
|
||||
val sendResult = try {
|
||||
if (card.isDemoCard()) {
|
||||
DemoTransactionSender(walletManager).send(txData, signer)
|
||||
} else {
|
||||
(walletManager as TransactionSender).send(txData, signer)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
FirebaseCrashlytics.getInstance().recordException(ex)
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
withMainContext {
|
||||
tangemSdk.config.linkedTerminal = linkedTerminalState
|
||||
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
|
||||
store.dispatchErrorNotification(TapError.CustomError(ex.localizedMessage ?: "Unknown error"))
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
withMainContext {
|
||||
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
|
||||
tangemSdk.config.linkedTerminal = linkedTerminalState
|
||||
|
||||
when (sendResult) {
|
||||
is Result.Success -> {
|
||||
dispatch(SendAction.SendSuccess)
|
||||
|
||||
if (externalTransactionData != null) {
|
||||
Analytics.send(
|
||||
Basic.TransactionSent(
|
||||
sentFrom = AnalyticsParam.TxSentFrom.Sell,
|
||||
memoType = getMemoType(transactionExtras),
|
||||
),
|
||||
)
|
||||
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
|
||||
dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
|
||||
} else {
|
||||
Analytics.send(
|
||||
Basic.TransactionSent(
|
||||
sentFrom = AnalyticsParam.TxSentFrom.Send(
|
||||
blockchain = walletManager.wallet.blockchain.fullName,
|
||||
token = amountToSend.currencySymbol,
|
||||
feeType = feeType.convertToAnalyticsFeeType(),
|
||||
),
|
||||
memoType = getMemoType(transactionExtras),
|
||||
),
|
||||
)
|
||||
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
updateFeedbackManagerInfo(
|
||||
sendResult = sendResult.error,
|
||||
walletManager = walletManager,
|
||||
amountToSend = amountToSend,
|
||||
feeAmount = fee.amount,
|
||||
destinationAddress = destinationAddress,
|
||||
)
|
||||
val error = sendResult.error as? BlockchainSdkError ?: return@withMainContext
|
||||
|
||||
when (error) {
|
||||
is BlockchainSdkError.WrappedTangemError -> {
|
||||
val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext
|
||||
if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext
|
||||
|
||||
dispatch(
|
||||
SendAction.Dialog.SendTransactionFails.CardSdkError(
|
||||
error = tangemSdkError,
|
||||
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
.selectedUserWalletSync?.scanResponse
|
||||
?: error("ScanResponse must be not null"),
|
||||
),
|
||||
)
|
||||
}
|
||||
is BlockchainSdkError.CreateAccountUnderfunded -> {
|
||||
// from XLM, XRP, Polkadot
|
||||
dispatch(
|
||||
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
|
||||
error = error,
|
||||
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
.selectedUserWalletSync?.scanResponse
|
||||
?: error("ScanResponse must be not null"),
|
||||
),
|
||||
)
|
||||
}
|
||||
is BlockchainSdkError.Kaspa.UtxoAmountError -> {
|
||||
dispatch(
|
||||
SendAction.Dialog.KaspaWarningDialog(
|
||||
maxOutputs = error.maxOutputs,
|
||||
maxAmount = error.maxAmount,
|
||||
onOk = {
|
||||
dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false))
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
is BlockchainSdkError.Chia.UtxoAmountError -> {
|
||||
dispatch(
|
||||
SendAction.Dialog.ChiaWarningDialog(
|
||||
blockchainName = Blockchain.Chia.fullName,
|
||||
maxOutputs = error.maxOutputs,
|
||||
maxAmount = error.maxAmount,
|
||||
onOk = {
|
||||
dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false))
|
||||
dispatch(AmountActionUi.CheckAmountToSend)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
error.customMessage.contains(DemoTransactionSender.ID) -> {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_done,
|
||||
messageId = R.string.alert_demo_feature_disabled,
|
||||
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
dispatch(
|
||||
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
|
||||
error = error,
|
||||
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
.selectedUserWalletSync?.scanResponse
|
||||
?: error("ScanResponse must be not null"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMemoType(transactionExtras: TransactionExtrasState): Basic.TransactionSent.MemoType {
|
||||
return when {
|
||||
transactionExtras.isEmpty() -> Basic.TransactionSent.MemoType.Empty
|
||||
transactionExtras.isNull() -> Basic.TransactionSent.MemoType.Null
|
||||
else -> Basic.TransactionSent.MemoType.Full
|
||||
}
|
||||
}
|
||||
|
||||
private fun Analytics.sendSelectedCurrencyEvent(mainCurrencyType: MainCurrencyType) {
|
||||
send(
|
||||
Token.Send.SelectedCurrency(
|
||||
currency = when (mainCurrencyType) {
|
||||
MainCurrencyType.FIAT -> CurrencyType.AppCurrency
|
||||
MainCurrencyType.CRYPTO -> CurrencyType.Token
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateFeedbackManagerInfo(
|
||||
walletManager: WalletManager,
|
||||
amountToSend: Amount,
|
||||
feeAmount: Amount,
|
||||
destinationAddress: String,
|
||||
sendResult: BlockchainError,
|
||||
) {
|
||||
val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles)
|
||||
if (featureToggles.isLocalLogsEnabled) {
|
||||
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = (sendResult as? BlockchainSdkError)?.customMessage ?: "It isn't BlockchainSdkError",
|
||||
blockchainId = walletManager.wallet.blockchain.id,
|
||||
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "",
|
||||
destinationAddress = destinationAddress,
|
||||
tokenSymbol = if (amountToSend.type is AmountType.Token) {
|
||||
amountToSend.currencySymbol
|
||||
} else {
|
||||
""
|
||||
},
|
||||
amount = amountToSend.value?.stripZeroPlainString() ?: "0",
|
||||
fee = feeAmount.value?.stripZeroPlainString() ?: "0",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
|
||||
walletManager = walletManager,
|
||||
amountToSend = amountToSend,
|
||||
feeAmount = feeAmount,
|
||||
destinationAddress = destinationAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createValidateTransactionError(
|
||||
errorList: EnumSet<TransactionError>,
|
||||
walletManager: WalletManager,
|
||||
): TapError.ValidateTransactionErrors {
|
||||
val tapErrors = errorList.map {
|
||||
when (it) {
|
||||
TransactionError.AmountExceedsBalance -> TapError.AmountExceedsBalance
|
||||
TransactionError.AmountLowerExistentialDeposit -> {
|
||||
if (walletManager is ExistentialDepositProvider) {
|
||||
val args = listOf(walletManager.getExistentialDeposit().stripZeroPlainString())
|
||||
TapError.AmountLowerExistentialDeposit(args)
|
||||
} else {
|
||||
TapError.UnknownError
|
||||
}
|
||||
}
|
||||
TransactionError.FeeExceedsBalance -> TapError.FeeExceedsBalance
|
||||
TransactionError.TotalExceedsBalance -> TapError.TotalExceedsBalance
|
||||
TransactionError.InvalidAmountValue -> TapError.InvalidAmountValue
|
||||
TransactionError.InvalidFeeValue -> TapError.InvalidFeeValue
|
||||
TransactionError.DustAmount -> {
|
||||
val args = listOf(walletManager.dustValue?.stripZeroPlainString() ?: "0")
|
||||
TapError.DustAmount(args)
|
||||
}
|
||||
TransactionError.DustChange -> TapError.DustChange
|
||||
else -> TapError.UnknownError
|
||||
}
|
||||
}
|
||||
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
|
||||
}
|
||||
|
||||
private fun updateWarnings(dispatch: (Action) -> Unit) {
|
||||
val warningsManager = store.state.globalState.warningManager ?: return
|
||||
val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return
|
||||
|
||||
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
|
||||
dispatch(SendAction.Warnings.Set(warnings))
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.tap.features.send.redux.AddressActionUi
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification
|
||||
import com.tangem.tap.features.send.redux.SendScreenAction
|
||||
import com.tangem.tap.features.send.redux.states.AddressState
|
||||
import com.tangem.tap.features.send.redux.states.InputViewValue
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddressReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
|
||||
is AddressActionUi -> handleUiAction(action, sendState, sendState.addressState)
|
||||
is AddressVerifyAction -> handleAction(action, sendState, sendState.addressState)
|
||||
else -> sendState
|
||||
}
|
||||
|
||||
private fun handleUiAction(action: AddressActionUi, sendState: SendState, state: AddressState): SendState {
|
||||
val result = when (action) {
|
||||
is AddressActionUi.HandleUserInput -> state
|
||||
is AddressActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
|
||||
is AddressActionUi.TruncateOrRestore -> {
|
||||
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
|
||||
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
|
||||
}
|
||||
is AddressActionUi.PasteAddress -> return sendState
|
||||
is AddressActionUi.CheckClipboard -> return sendState
|
||||
is AddressActionUi.CheckAddress -> return sendState
|
||||
}
|
||||
return updateLastState(sendState.copy(addressState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAction(action: AddressVerifyAction, sendState: SendState, state: AddressState): SendState {
|
||||
val result = when (action) {
|
||||
is AddressVerification.SetWalletAddress -> {
|
||||
state.copy(
|
||||
viewFieldValue = InputViewValue(action.address, action.isUserInput),
|
||||
normalFieldValue = action.address,
|
||||
truncatedFieldValue = state.truncate(action.address),
|
||||
destinationWalletAddress = action.address,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
is AddressVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
|
||||
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
|
||||
}
|
||||
return updateLastState(sendState.copy(addressState = result), result)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.common.extensions.isZero
|
||||
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.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.utils.StringsSigns.STARS
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AmountReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
|
||||
is AmountActionUi -> handleUiAction(action, sendState, sendState.amountState)
|
||||
is AmountAction -> handleAction(action, sendState, sendState.amountState)
|
||||
else -> sendState
|
||||
}
|
||||
|
||||
private fun handleUiAction(action: AmountActionUi, sendState: SendState, state: AmountState): SendState {
|
||||
val result = when (action) {
|
||||
is SetMainCurrency -> {
|
||||
val currencyCanBeSwitched = sendState.mainCurrencyCanBeSwitched()
|
||||
|
||||
when (val currency = if (currencyCanBeSwitched) action.mainCurrency else MainCurrencyType.CRYPTO) {
|
||||
MainCurrencyType.FIAT -> {
|
||||
val fiatToSend = if (state.amountToSendCrypto.isZero()) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
sendState.convertExtractCryptoToFiat(state.amountToSendCrypto)
|
||||
}
|
||||
val rescaledBalance = sendState.convertExtractCryptoToFiat(state.balanceCrypto, true)
|
||||
val viewValue = state.restoreDecimalSeparator(fiatToSend.stripZeroPlainString())
|
||||
state.copy(
|
||||
viewAmountValue = InputViewValue(viewValue),
|
||||
viewBalanceValue = if (state.hideBalance) STARS else rescaledBalance.stripZeroPlainString(),
|
||||
mainCurrency = state.createMainCurrency(currency, true),
|
||||
maxLengthOfAmount = sendState.getDecimals(currency),
|
||||
cursorAtTheSamePosition = false,
|
||||
)
|
||||
}
|
||||
MainCurrencyType.CRYPTO -> {
|
||||
val viewValue = state.restoreDecimalSeparator(state.amountToSendCrypto.stripZeroPlainString())
|
||||
state.copy(
|
||||
viewAmountValue = InputViewValue(viewValue),
|
||||
viewBalanceValue = state.balanceCrypto.stripZeroPlainString(),
|
||||
mainCurrency = state.createMainCurrency(currency, currencyCanBeSwitched),
|
||||
maxLengthOfAmount = sendState.getDecimals(currency),
|
||||
cursorAtTheSamePosition = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> return sendState
|
||||
}
|
||||
|
||||
return updateLastState(sendState.copy(amountState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAction(action: AmountAction, sendState: SendState, state: AmountState): SendState {
|
||||
val result = when (action) {
|
||||
is AmountAction.SetAmount -> {
|
||||
val amount = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) {
|
||||
action.amountCrypto
|
||||
} else {
|
||||
sendState.convertExtractCryptoToFiat(action.amountCrypto, true)
|
||||
}
|
||||
val viewValue = state.restoreDecimalSeparator(amount.stripZeroPlainString())
|
||||
state.copy(
|
||||
viewAmountValue = InputViewValue(viewValue, action.isUserInput),
|
||||
amountToSendCrypto = action.amountCrypto,
|
||||
cursorAtTheSamePosition = true,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
is AmountAction.SetAmountError -> state.copy(error = action.error)
|
||||
is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator)
|
||||
is AmountAction.HideBalance -> {
|
||||
val rescaledBalance = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) {
|
||||
state.balanceCrypto
|
||||
} else {
|
||||
sendState.convertExtractCryptoToFiat(state.balanceCrypto, true)
|
||||
}
|
||||
|
||||
state.copy(
|
||||
hideBalance = action.hide,
|
||||
viewBalanceValue = if (action.hide) STARS else rescaledBalance.stripZeroPlainString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(amountState = result), result)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
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
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeeReducer : SendInternalReducer {
|
||||
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
|
||||
is FeeActionUi -> handleUiAction(action, sendState, sendState.feeState)
|
||||
is FeeAction -> handleAction(action, sendState, sendState.feeState)
|
||||
else -> sendState
|
||||
}
|
||||
|
||||
private fun handleUiAction(action: FeeActionUi, sendState: SendState, state: FeeState): SendState {
|
||||
val result = when (action) {
|
||||
is FeeActionUi.ToggleControlsVisibility -> {
|
||||
state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible)
|
||||
}
|
||||
is FeeActionUi.ChangeSelectedFee -> {
|
||||
val currentFee = state.fees?.let {
|
||||
createValueOfFeeAmount(action.feeType, it)
|
||||
}
|
||||
state.copy(
|
||||
selectedFeeType = action.feeType,
|
||||
currentFee = currentFee,
|
||||
)
|
||||
}
|
||||
is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded)
|
||||
}
|
||||
return updateLastState(sendState.copy(feeState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState {
|
||||
val result = when (action) {
|
||||
is FeeAction.RequestFee -> {
|
||||
state.copy(progressState = ProgressState.Loading)
|
||||
}
|
||||
is FeeAction.ChangeLayoutVisibility -> {
|
||||
fun getVisibility(current: Boolean, proposed: Boolean?): Boolean = proposed ?: current
|
||||
state.copy(
|
||||
mainLayoutIsVisible = getVisibility(state.mainLayoutIsVisible, action.main),
|
||||
controlsLayoutIsVisible = getVisibility(state.controlsLayoutIsVisible, action.controls),
|
||||
feeChipGroupIsVisible = getVisibility(state.feeChipGroupIsVisible, action.chipGroup),
|
||||
)
|
||||
}
|
||||
is FeeAction.FeeCalculation.SetFeeResult -> {
|
||||
when (val fees = action.fee) {
|
||||
is TransactionFee.Single -> {
|
||||
val feeType = FeeType.SINGLE
|
||||
val currentFee = createValueOfFeeAmount(feeType, fees)
|
||||
|
||||
state.copy(
|
||||
selectedFeeType = feeType,
|
||||
fees = fees,
|
||||
currentFee = currentFee,
|
||||
feeIsApproximate = isFeeApproximate(sendState),
|
||||
)
|
||||
}
|
||||
is TransactionFee.Choosable -> {
|
||||
val feeType = getCurrentFeeType(state)
|
||||
val currentFee = createValueOfFeeAmount(feeType, fees)
|
||||
|
||||
state.copy(
|
||||
selectedFeeType = feeType,
|
||||
fees = fees,
|
||||
currentFee = currentFee,
|
||||
feeIsApproximate = isFeeApproximate(sendState),
|
||||
)
|
||||
}
|
||||
}.copy(
|
||||
progressState = ProgressState.Done,
|
||||
)
|
||||
}
|
||||
FeeAction.FeeCalculation.ClearResult -> {
|
||||
state.copy(
|
||||
fees = null,
|
||||
currentFee = null,
|
||||
progressState = ProgressState.Done,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return updateLastState(sendState.copy(feeState = result), result)
|
||||
}
|
||||
|
||||
private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Fee {
|
||||
return when (transactionFee) {
|
||||
is TransactionFee.Single -> {
|
||||
transactionFee.normal
|
||||
}
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeType) {
|
||||
FeeType.SINGLE -> transactionFee.normal
|
||||
FeeType.LOW -> transactionFee.minimum
|
||||
FeeType.NORMAL -> transactionFee.normal
|
||||
FeeType.PRIORITY -> transactionFee.priority
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFeeApproximate(sendState: SendState): Boolean {
|
||||
val blockchain = sendState.walletManager?.wallet?.blockchain ?: return false
|
||||
|
||||
val amountType = sendState.amountState.typeOfAmount
|
||||
return blockchain.isFeeApproximate(amountType)
|
||||
}
|
||||
|
||||
private fun getCurrentFeeType(state: FeeState): FeeType {
|
||||
return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType
|
||||
}
|
||||
}
|
||||
|
|
@ -1,408 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.FeePaidCurrency
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
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
|
||||
import com.tangem.tap.features.send.redux.states.*
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.StringsSigns.LOWER_SIGN
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
class ReceiptReducer : SendInternalReducer {
|
||||
|
||||
private lateinit var sendState: SendState
|
||||
private lateinit var amountState: AmountState
|
||||
private lateinit var feeState: FeeState
|
||||
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
|
||||
this.sendState = sendState
|
||||
this.amountState = sendState.amountState
|
||||
this.feeState = sendState.feeState
|
||||
|
||||
return when (action) {
|
||||
is RefreshReceipt -> handleRefresh(sendState.receiptState)
|
||||
else -> sendState
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRefresh(state: ReceiptState): SendState {
|
||||
val wallet = sendState.walletManager?.wallet ?: return sendState
|
||||
val feePaidCurrency = wallet.blockchain.feePaidCurrency()
|
||||
|
||||
val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount, feePaidCurrency)
|
||||
val symbols = determineSymbols(wallet, amountState.typeOfAmount, feePaidCurrency)
|
||||
val showBlank = !SendState.isReadyToSend()
|
||||
val result = state.copy(
|
||||
visibleTypeOfReceipt = layoutType,
|
||||
mainCurrency = amountState.mainCurrency,
|
||||
fiat = createFiatType(symbols, showBlank),
|
||||
crypto = createCryptoType(symbols, showBlank),
|
||||
tokenFiat = createTokenFiatType(symbols, showBlank),
|
||||
tokenCrypto = createTokenCryptoType(symbols, showBlank),
|
||||
customTokenFiat = createCustomTokenFiat(symbols, showBlank),
|
||||
customTokenCrypto = createCustomTokenCrypto(symbols, showBlank),
|
||||
sameCurrencyFiat = createSameCurrencyFiat(symbols, showBlank),
|
||||
sameCurrencyCrypto = createSameCurrencyCrypto(symbols, showBlank),
|
||||
)
|
||||
return updateLastState(sendState.copy(receiptState = result), result)
|
||||
}
|
||||
|
||||
private fun createFiatType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptFiat {
|
||||
val feeCrypto = feeState.getCurrentFeeValue()
|
||||
val feeFiat = convertToFiatPrecision(feeCrypto)
|
||||
|
||||
if (showBlank) {
|
||||
return ReceiptFiat("0", feeFiat, "0", "0", symbols)
|
||||
}
|
||||
|
||||
return if (feeState.feeIsIncluded) {
|
||||
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto))
|
||||
val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto)
|
||||
ReceiptFiat(
|
||||
amountFiat = amountFiat,
|
||||
feeFiat = feeFiat,
|
||||
totalFiat = totalFiat,
|
||||
willSentCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
|
||||
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto)
|
||||
val totalFiat = convertToFiatPrecision(totalAmountCrypto)
|
||||
ReceiptFiat(
|
||||
amountFiat = amountFiat,
|
||||
feeFiat = feeFiat,
|
||||
totalFiat = totalFiat,
|
||||
willSentCrypto = totalAmountCrypto.stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCryptoType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptCrypto {
|
||||
val feeCrypto = feeState.getCurrentFeeValue()
|
||||
val feeFiat = convertToFiatPrecision(feeCrypto)
|
||||
if (showBlank) {
|
||||
return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols)
|
||||
}
|
||||
|
||||
if (feeState.feeIsIncluded) {
|
||||
return ReceiptCrypto(
|
||||
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
|
||||
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
|
||||
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
|
||||
feeFiat = feeFiat,
|
||||
willSentFiat = convertToFiatPrecision(amountState.amountToSendCrypto),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
|
||||
return ReceiptCrypto(
|
||||
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
|
||||
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
|
||||
totalCrypto = totalCrypto.stripZeroPlainString(),
|
||||
feeFiat = feeFiat,
|
||||
willSentFiat = convertToFiatPrecision(totalCrypto),
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTokenFiatType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenFiat {
|
||||
val feeCoin = feeState.getCurrentFeeValue()
|
||||
if (showBlank) {
|
||||
val feeFiat = convertToFiatPrecision(feeCoin)
|
||||
return ReceiptTokenFiat("0", feeFiat, "0", "0", "0", symbols)
|
||||
}
|
||||
|
||||
val tokensToSend = amountState.amountToSendCrypto
|
||||
|
||||
return if (sendState.coinIsConvertible() && sendState.tokenIsConvertible()) {
|
||||
val feeFiat = sendState.coinConverter!!.toFiatUnscaled(feeCoin)
|
||||
val amountFiat = sendState.tokenConverter!!.toFiatUnscaled(tokensToSend)
|
||||
val totalFiat = amountFiat.plus(feeFiat)
|
||||
ReceiptTokenFiat(
|
||||
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
|
||||
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString().addPrecisionSign(),
|
||||
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
|
||||
willSentToken = tokensToSend.stripZeroPlainString(),
|
||||
willSentFeeCoin = feeCoin.stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
ReceiptTokenFiat(
|
||||
amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
willSentToken = tokensToSend.stripZeroPlainString(),
|
||||
willSentFeeCoin = feeCoin.stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTokenCryptoType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenCrypto {
|
||||
val feeCoin = feeState.getCurrentFeeValue()
|
||||
if (showBlank) {
|
||||
return ReceiptTokenCrypto("0", feeCoin.stripZeroPlainString(), "0", symbols)
|
||||
}
|
||||
val tokensToSend = amountState.amountToSendCrypto
|
||||
|
||||
return if (sendState.coinIsConvertible() && sendState.tokenIsConvertible()) {
|
||||
val tokenFiat = sendState.tokenConverter!!.toFiatUnscaled(tokensToSend)
|
||||
val feeFiat = sendState.coinConverter!!.toFiatUnscaled(feeCoin)
|
||||
val totalFiat = tokenFiat.plus(feeFiat)
|
||||
|
||||
ReceiptTokenCrypto(
|
||||
amountToken = tokensToSend.stripZeroPlainString(),
|
||||
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
|
||||
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
ReceiptTokenCrypto(
|
||||
amountToken = tokensToSend.stripZeroPlainString(),
|
||||
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
|
||||
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCustomTokenCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenCrypto {
|
||||
val feeValue = feeState.getCurrentFeeValue()
|
||||
if (showBlank) {
|
||||
return ReceiptTokenCrypto("0", feeValue.stripZeroPlainString(), "0", symbols)
|
||||
}
|
||||
val tokensToSend = amountState.amountToSendCrypto
|
||||
val amountType = amountState.typeOfAmount
|
||||
val currencyConverter = when {
|
||||
amountType is AmountType.Token && sendState.feeIsConvertible() -> sendState.tokenConverter
|
||||
amountType is AmountType.Coin && sendState.feeIsConvertible() -> sendState.coinConverter
|
||||
else -> null
|
||||
}
|
||||
|
||||
return if (currencyConverter != null) {
|
||||
val currencyFiat = currencyConverter.toFiatUnscaled(tokensToSend)
|
||||
val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeValue)
|
||||
val totalFiat = currencyFiat.plus(feeFiat)
|
||||
|
||||
ReceiptTokenCrypto(
|
||||
amountToken = tokensToSend.stripZeroPlainString(),
|
||||
feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(),
|
||||
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
ReceiptTokenCrypto(
|
||||
amountToken = tokensToSend.stripZeroPlainString(),
|
||||
feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(),
|
||||
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCustomTokenFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenFiat {
|
||||
val feeCoin = feeState.getCurrentFeeValue()
|
||||
if (showBlank) {
|
||||
val feeFiat = convertToFiatPrecision(feeCoin)
|
||||
return ReceiptTokenFiat("0", feeFiat, "0", "0", "0", symbols)
|
||||
}
|
||||
|
||||
val tokensToSend = amountState.amountToSendCrypto
|
||||
|
||||
val amountType = amountState.typeOfAmount
|
||||
val currencyConverter = when {
|
||||
amountType is AmountType.Token && sendState.feeIsConvertible() -> sendState.tokenConverter
|
||||
amountType is AmountType.Coin && sendState.feeIsConvertible() -> sendState.coinConverter
|
||||
else -> null
|
||||
}
|
||||
|
||||
return if (currencyConverter != null) {
|
||||
val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeCoin)
|
||||
val amountFiat = currencyConverter.toFiatUnscaled(tokensToSend)
|
||||
val totalFiat = amountFiat.plus(feeFiat)
|
||||
ReceiptTokenFiat(
|
||||
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
|
||||
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString().addPrecisionSign(),
|
||||
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
|
||||
willSentToken = tokensToSend.scaleToFiat(true).stripZeroPlainString(),
|
||||
willSentFeeCoin = feeCoin.stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
ReceiptTokenFiat(
|
||||
amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
willSentToken = tokensToSend.stripZeroPlainString(),
|
||||
willSentFeeCoin = feeCoin.stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSameCurrencyCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptCrypto {
|
||||
val feeCrypto = feeState.getCurrentFeeValue()
|
||||
val feeFiat = convertToFiatPrecision(feeCrypto)
|
||||
if (showBlank) {
|
||||
return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols)
|
||||
}
|
||||
val isToken = amountState.typeOfAmount is AmountType.Token
|
||||
|
||||
if (feeState.feeIsIncluded) {
|
||||
return ReceiptCrypto(
|
||||
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
|
||||
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
|
||||
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
|
||||
feeFiat = feeFiat,
|
||||
willSentFiat = convertToFiatPrecision(value = amountState.amountToSendCrypto, isToken = isToken),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
|
||||
return ReceiptCrypto(
|
||||
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
|
||||
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
|
||||
totalCrypto = totalCrypto.stripZeroPlainString(),
|
||||
feeFiat = feeFiat,
|
||||
willSentFiat = convertToFiatPrecision(value = totalCrypto, isToken = isToken),
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSameCurrencyFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptFiat {
|
||||
val feeCrypto = feeState.getCurrentFeeValue()
|
||||
val isToken = amountState.typeOfAmount is AmountType.Token
|
||||
val feeFiat = convertToFiatPrecision(feeCrypto, isToken = isToken)
|
||||
|
||||
if (showBlank) {
|
||||
return ReceiptFiat("0", feeFiat, "0", "0", symbols)
|
||||
}
|
||||
|
||||
return if (feeState.feeIsIncluded) {
|
||||
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto), isToken = isToken)
|
||||
val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken)
|
||||
ReceiptFiat(
|
||||
amountFiat = amountFiat,
|
||||
feeFiat = feeFiat,
|
||||
totalFiat = totalFiat,
|
||||
willSentCrypto = amountState.amountToSendCrypto.scaleToFiat(true).stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
} else {
|
||||
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
|
||||
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken)
|
||||
val totalFiat = convertToFiatPrecision(totalAmountCrypto, isToken = isToken)
|
||||
ReceiptFiat(
|
||||
amountFiat = amountFiat,
|
||||
feeFiat = feeFiat,
|
||||
totalFiat = totalFiat,
|
||||
willSentCrypto = totalAmountCrypto.scaleToFiat().stripZeroPlainString(),
|
||||
symbols = symbols,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineSymbols(
|
||||
wallet: Wallet,
|
||||
amountType: AmountType,
|
||||
feePaidCurrency: FeePaidCurrency,
|
||||
): ReceiptSymbols {
|
||||
return ReceiptSymbols(
|
||||
fiat = store.state.globalState.appCurrency.code,
|
||||
crypto = wallet.blockchain.currency,
|
||||
token = when (amountType) {
|
||||
is AmountType.Token -> amountType.token.symbol
|
||||
else -> null
|
||||
},
|
||||
fee = when (feePaidCurrency) {
|
||||
FeePaidCurrency.Coin -> wallet.blockchain.currency
|
||||
FeePaidCurrency.SameCurrency -> store.state.sendState.currency?.symbol
|
||||
is FeePaidCurrency.Token -> feePaidCurrency.token.symbol
|
||||
is FeePaidCurrency.FeeResource -> feePaidCurrency.currency
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun determineLayoutType(
|
||||
mainCurrencyType: MainCurrencyType,
|
||||
amountType: AmountType,
|
||||
feePaidCurrency: FeePaidCurrency,
|
||||
): ReceiptLayoutType {
|
||||
return when (mainCurrencyType) {
|
||||
MainCurrencyType.FIAT -> determineFiatLayoutType(feePaidCurrency, amountType)
|
||||
MainCurrencyType.CRYPTO -> determineCryptoLayoutType(feePaidCurrency, amountType)
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineFiatLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType {
|
||||
return when (feePaidCurrency) {
|
||||
is FeePaidCurrency.Token -> {
|
||||
val amountToken = (amountType as? AmountType.Token)?.token
|
||||
val sameToken =
|
||||
feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true)
|
||||
if (sameToken) ReceiptLayoutType.SAME_CURRENCY_FIAT else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT
|
||||
}
|
||||
FeePaidCurrency.Coin -> when (amountType) {
|
||||
AmountType.Coin -> ReceiptLayoutType.FIAT
|
||||
is AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT
|
||||
is AmountType.FeeResource,
|
||||
AmountType.Reserve,
|
||||
-> ReceiptLayoutType.UNKNOWN
|
||||
}
|
||||
FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY_FIAT
|
||||
is FeePaidCurrency.FeeResource -> ReceiptLayoutType.SAME_CURRENCY
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineCryptoLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType {
|
||||
return when (feePaidCurrency) {
|
||||
is FeePaidCurrency.Token -> {
|
||||
val amountToken = (amountType as? AmountType.Token)?.token
|
||||
val sameToken =
|
||||
feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true)
|
||||
if (sameToken) ReceiptLayoutType.SAME_CURRENCY else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN
|
||||
}
|
||||
FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY
|
||||
FeePaidCurrency.Coin -> when (amountType) {
|
||||
AmountType.Coin -> ReceiptLayoutType.CRYPTO
|
||||
is AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO
|
||||
is AmountType.FeeResource,
|
||||
AmountType.Reserve,
|
||||
-> ReceiptLayoutType.UNKNOWN
|
||||
}
|
||||
is FeePaidCurrency.FeeResource -> ReceiptLayoutType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
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 -> {
|
||||
BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.addPrecisionSign(): String {
|
||||
val result = if (feeState.feeIsApproximate) "$LOWER_SIGN $this" else this
|
||||
return result.trim()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.FeePaidCurrency
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
|
||||
import com.tangem.tap.features.send.redux.states.IdStateHolder
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SendInternalReducer {
|
||||
fun handle(action: SendScreenAction, sendState: SendState): 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 AddressActionUi, is AddressVerifyAction -> AddressReducer()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private class SendReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
|
||||
val result = when (action) {
|
||||
is SendAction.ChangeSendButtonState -> {
|
||||
sendState.copy(sendButtonState = IndeterminateProgressButton(action.state))
|
||||
}
|
||||
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
|
||||
is SendAction.Dialog.KaspaWarningDialog -> sendState.copy(dialog = action)
|
||||
is SendAction.Dialog.ChiaWarningDialog -> sendState.copy(dialog = action)
|
||||
is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action)
|
||||
is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action)
|
||||
is SendAction.Dialog.RequestFeeError -> sendState.copy(dialog = action)
|
||||
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
|
||||
is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList)
|
||||
is SendAction.SendSpecificTransaction -> handleSendSpecificTransactionAction(action, sendState)
|
||||
is SendAction.SendSuccess -> sendState.copy(isSuccessSend = true)
|
||||
else -> return sendState
|
||||
}
|
||||
|
||||
return updateLastState(result, result)
|
||||
}
|
||||
|
||||
private fun handleSendSpecificTransactionAction(
|
||||
action: SendAction.SendSpecificTransaction,
|
||||
state: SendState,
|
||||
): SendState {
|
||||
return state.copy(
|
||||
externalTransactionData =
|
||||
ExternalTransactionData(action.sendAmount, action.destinationAddress, action.transactionId),
|
||||
feeState = state.feeState.copy(
|
||||
includeFeeSwitcherIsEnabled = false,
|
||||
),
|
||||
amountState = state.amountState.copy(
|
||||
inputIsEnabled = false,
|
||||
),
|
||||
addressState = state.addressState.copy(
|
||||
inputIsEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = sendState
|
||||
}
|
||||
|
||||
private class PrepareSendScreenStatesReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
|
||||
val prepareAction = action as PrepareSendScreen
|
||||
val walletManager = action.walletManager
|
||||
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
|
||||
val decimals = amountToExtract.decimals
|
||||
val canIncludeFee = canIncludeFee(
|
||||
typeOfAmount = amountToExtract.type,
|
||||
feePaidCurrency = action.feePaidCurrency,
|
||||
)
|
||||
|
||||
return sendState.copy(
|
||||
walletManager = walletManager,
|
||||
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
|
||||
tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) },
|
||||
customFeeConverter = action.feeCurrencyRate?.let { CurrencyConverter(it, action.feeCurrencyDecimals) },
|
||||
amountState = sendState.amountState.copy(
|
||||
amountToExtract = amountToExtract,
|
||||
typeOfAmount = amountToExtract.type,
|
||||
balanceCrypto = amountToExtract.value ?: BigDecimal.ZERO,
|
||||
),
|
||||
feeState = sendState.feeState.copy(includeFeeSwitcherIsEnabled = canIncludeFee),
|
||||
canIncludeFee = canIncludeFee,
|
||||
currency = action.currency,
|
||||
)
|
||||
}
|
||||
|
||||
private fun canIncludeFee(typeOfAmount: AmountType, feePaidCurrency: FeePaidCurrency): Boolean {
|
||||
return when (feePaidCurrency) {
|
||||
FeePaidCurrency.Coin -> typeOfAmount == AmountType.Coin
|
||||
FeePaidCurrency.SameCurrency -> true
|
||||
is FeePaidCurrency.Token -> {
|
||||
val sendToken = (typeOfAmount as? AmountType.Token)?.token ?: return false
|
||||
|
||||
sendToken.contractAddress.equals(feePaidCurrency.token.contractAddress, ignoreCase = true) &&
|
||||
sendToken.name.equals(feePaidCurrency.token.name, ignoreCase = true) &&
|
||||
sendToken.symbol.equals(feePaidCurrency.token.symbol, ignoreCase = true)
|
||||
}
|
||||
is FeePaidCurrency.FeeResource -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState {
|
||||
sendState.lastChangedStates.add(lastChangedState.stateId)
|
||||
return sendState
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.send.redux.SendScreenAction
|
||||
import com.tangem.tap.features.send.redux.TransactionExtrasAction.*
|
||||
import com.tangem.tap.features.send.redux.states.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TransactionExtrasReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
|
||||
return when (action) {
|
||||
is Prepare -> handleInitialization(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)
|
||||
is TonMemo -> handleTonMemo(action, sendState, sendState.transactionExtrasState)
|
||||
is CosmosMemo -> handleCosmosMemo(action, sendState, sendState.transactionExtrasState)
|
||||
is HederaMemo -> handleHederaMemo(action, sendState, sendState.transactionExtrasState)
|
||||
is AlgorandMemo -> handleAlgorandMemo(action, sendState, sendState.transactionExtrasState)
|
||||
else -> sendState
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInitialization(action: Prepare, sendState: SendState): SendState {
|
||||
val emptyResult = TransactionExtrasState()
|
||||
val result = when (action.blockchain) {
|
||||
Blockchain.XRP -> {
|
||||
val address = action.walletAddress.substringAfter(":")
|
||||
// 'r' - without tag, 'x' - with tag
|
||||
if (address.startsWith("r", true)) {
|
||||
val tag = action.xrpTag?.toLongOrNull()
|
||||
if (tag == null) {
|
||||
TransactionExtrasState(xrpDestinationTag = XrpDestinationTagState())
|
||||
} else {
|
||||
TransactionExtrasState(
|
||||
xrpDestinationTag = XrpDestinationTagState(
|
||||
viewFieldValue = InputViewValue("$tag", false),
|
||||
tag = tag,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptyResult
|
||||
}
|
||||
}
|
||||
Blockchain.Stellar -> TransactionExtrasState(xlmMemo = XlmMemoState())
|
||||
Blockchain.Binance -> TransactionExtrasState(binanceMemo = BinanceMemoState())
|
||||
Blockchain.TON, Blockchain.TONTestnet -> TransactionExtrasState(tonMemoState = TonMemoState())
|
||||
Blockchain.Cosmos,
|
||||
Blockchain.TerraV1,
|
||||
Blockchain.TerraV2,
|
||||
-> TransactionExtrasState(cosmosMemoState = CosmosMemoState())
|
||||
Blockchain.Hedera, Blockchain.HederaTestnet -> TransactionExtrasState(hederaMemoState = HederaMemoState())
|
||||
Blockchain.Algorand, Blockchain.AlgorandTestnet -> TransactionExtrasState(
|
||||
algorandMemoState = AlgorandMemoState(),
|
||||
)
|
||||
else -> emptyResult
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleRelease(sendState: SendState): SendState {
|
||||
val result = TransactionExtrasState()
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleXlmMemo(action: XlmMemo, sendState: SendState, infoState: TransactionExtrasState): SendState {
|
||||
fun clearMemo(memo: XlmMemoState): XlmMemoState = memo.copy(text = null, id = null, error = null)
|
||||
|
||||
val result = when (action) {
|
||||
is XlmMemo.HandleUserInput -> {
|
||||
val inputViewValue = InputViewValue(action.data, true)
|
||||
var memo = infoState.xlmMemo?.copy(viewFieldValue = inputViewValue)
|
||||
?: XlmMemoState(inputViewValue)
|
||||
memo = clearMemo(memo)
|
||||
memo = when (memo.selectedMemoType) {
|
||||
XlmMemoType.TEXT -> {
|
||||
if (XlmMemoState.isAssignableValue(action.data)) {
|
||||
memo.copy(text = StellarMemo.Text(action.data))
|
||||
} else {
|
||||
memo.copy(error = TransactionExtraError.INVALID_XLM_MEMO)
|
||||
}
|
||||
}
|
||||
XlmMemoType.ID -> {
|
||||
if (XlmMemoState.isAssignableValue(action.data)) {
|
||||
memo.copy(id = StellarMemo.Id(action.data.toBigInteger()))
|
||||
} else {
|
||||
memo.copy(error = TransactionExtraError.INVALID_XLM_MEMO)
|
||||
}
|
||||
}
|
||||
}
|
||||
infoState.copy(xlmMemo = memo)
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleBinanceMemo(
|
||||
action: BinanceMemo,
|
||||
sendState: SendState,
|
||||
infoState: TransactionExtrasState,
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is BinanceMemo.HandleUserInput -> {
|
||||
val tag = action.data.toBigIntegerOrNull()
|
||||
if (tag != null) {
|
||||
val input = InputViewValue(action.data, true)
|
||||
val tagState = BinanceMemoState(input, tag)
|
||||
infoState.copy(binanceMemo = tagState)
|
||||
} else {
|
||||
infoState
|
||||
}
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleXrpTag(
|
||||
action: XrpDestinationTag,
|
||||
sendState: SendState,
|
||||
infoState: TransactionExtrasState,
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is XrpDestinationTag.HandleUserInput -> {
|
||||
val tag = action.data.toLongOrNull()
|
||||
if (tag != null) {
|
||||
val input = InputViewValue(action.data, true)
|
||||
val tagState = if (tag <= XrpDestinationTagState.MAX_NUMBER) {
|
||||
XrpDestinationTagState(input, tag)
|
||||
} else {
|
||||
XrpDestinationTagState(input, error = TransactionExtraError.INVALID_DESTINATION_TAG)
|
||||
}
|
||||
infoState.copy(xrpDestinationTag = tagState)
|
||||
} else {
|
||||
infoState
|
||||
}
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleTonMemo(action: TonMemo, sendState: SendState, infoState: TransactionExtrasState): SendState {
|
||||
val result = when (action) {
|
||||
is TonMemo.HandleUserInput -> {
|
||||
val memo = action.data
|
||||
val input = InputViewValue(memo, true)
|
||||
infoState.copy(tonMemoState = TonMemoState(input, memo))
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleCosmosMemo(
|
||||
action: CosmosMemo,
|
||||
sendState: SendState,
|
||||
infoState: TransactionExtrasState,
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is CosmosMemo.HandleUserInput -> {
|
||||
val memo = action.data
|
||||
val input = InputViewValue(memo, true)
|
||||
infoState.copy(cosmosMemoState = CosmosMemoState(input, memo))
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleHederaMemo(
|
||||
action: HederaMemo,
|
||||
sendState: SendState,
|
||||
infoState: TransactionExtrasState,
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is HederaMemo.HandleUserInput -> {
|
||||
val memo = action.data
|
||||
val input = InputViewValue(memo, true)
|
||||
infoState.copy(hederaMemoState = HederaMemoState(input, memo))
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAlgorandMemo(
|
||||
action: AlgorandMemo,
|
||||
sendState: SendState,
|
||||
infoState: TransactionExtrasState,
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is AlgorandMemo.HandleUserInput -> {
|
||||
val memo = action.data
|
||||
val input = InputViewValue(memo, true)
|
||||
infoState.copy(algorandMemoState = AlgorandMemoState(input, memo))
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction
|
||||
import java.math.BigInteger
|
||||
|
||||
data class AddressState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val normalFieldValue: String? = null,
|
||||
val truncatedFieldValue: String? = null,
|
||||
val destinationWalletAddress: String? = null,
|
||||
val error: AddressVerifyAction.Error? = null,
|
||||
val truncateHandler: ((String) -> String)? = null,
|
||||
val pasteIsEnabled: Boolean = false,
|
||||
val inputIsEnabled: Boolean = true,
|
||||
) : SendScreenState {
|
||||
|
||||
override val stateId: StateId = StateId.ADDRESS_PAY_ID
|
||||
|
||||
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value
|
||||
|
||||
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
@Deprecated("Legacy")
|
||||
data class TransactionExtrasState(
|
||||
val xlmMemo: XlmMemoState? = null,
|
||||
val binanceMemo: BinanceMemoState? = null,
|
||||
val xrpDestinationTag: XrpDestinationTagState? = null,
|
||||
val tonMemoState: TonMemoState? = null,
|
||||
val cosmosMemoState: CosmosMemoState? = null,
|
||||
val hederaMemoState: HederaMemoState? = null,
|
||||
val algorandMemoState: AlgorandMemoState? = null,
|
||||
) : IdStateHolder {
|
||||
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
|
||||
|
||||
fun isNull(): Boolean {
|
||||
return xlmMemo == null && binanceMemo == null && xrpDestinationTag == null && tonMemoState == null &&
|
||||
cosmosMemoState == null && hederaMemoState == null && algorandMemoState == null
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
val isXlmEmpty = xlmMemo?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isBinanceEmpty = binanceMemo?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isXrpEmpty = xrpDestinationTag?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isTonEmpty = tonMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isCosmosEmpty = cosmosMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isHederaEmpty = hederaMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isAlgorandEmpty = algorandMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
|
||||
return isXlmEmpty ||
|
||||
isBinanceEmpty ||
|
||||
isXrpEmpty ||
|
||||
isTonEmpty ||
|
||||
isCosmosEmpty ||
|
||||
isHederaEmpty ||
|
||||
isAlgorandEmpty
|
||||
}
|
||||
}
|
||||
|
||||
enum class XlmMemoType {
|
||||
TEXT, ID
|
||||
}
|
||||
|
||||
data class XlmMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val text: StellarMemo.Text? = null,
|
||||
val id: StellarMemo.Id? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
) {
|
||||
val memo: StellarMemo?
|
||||
get() = when (selectedMemoType) {
|
||||
XlmMemoType.TEXT -> text
|
||||
XlmMemoType.ID -> id
|
||||
}
|
||||
|
||||
val selectedMemoType: XlmMemoType
|
||||
get() = determineMemoType(viewFieldValue.value)
|
||||
|
||||
companion object {
|
||||
fun determineMemoType(value: String): XlmMemoType = when {
|
||||
value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID
|
||||
else -> XlmMemoType.TEXT
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun isAssignableValue(value: String): Boolean = when (determineMemoType(value)) {
|
||||
XlmMemoType.TEXT -> {
|
||||
// from org.stellar.sdk.MemoText
|
||||
value.toByteArray().size <= 28
|
||||
}
|
||||
XlmMemoType.ID -> {
|
||||
try {
|
||||
// from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
|
||||
value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
|
||||
} catch (ex: NumberFormatException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class BinanceMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val memo: BigInteger? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
// tag must contains only digits
|
||||
data class XrpDestinationTagState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val tag: Long? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
) {
|
||||
companion object {
|
||||
const val MAX_NUMBER: Long = 4294967295
|
||||
}
|
||||
}
|
||||
|
||||
data class TonMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val memo: String? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
data class CosmosMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val memo: String? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
data class HederaMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val memo: String? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
data class AlgorandMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val memo: String? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
enum class TransactionExtraError {
|
||||
INVALID_DESTINATION_TAG,
|
||||
INVALID_XLM_MEMO,
|
||||
INVALID_BINANCE_MEMO,
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class FeeType {
|
||||
SINGLE, LOW, NORMAL, PRIORITY;
|
||||
}
|
||||
|
||||
fun FeeType.convertToAnalyticsFeeType(): AnalyticsParam.FeeType {
|
||||
return when (this) {
|
||||
FeeType.SINGLE -> AnalyticsParam.FeeType.Fixed
|
||||
FeeType.LOW -> AnalyticsParam.FeeType.Min
|
||||
FeeType.NORMAL -> AnalyticsParam.FeeType.Normal
|
||||
FeeType.PRIORITY -> AnalyticsParam.FeeType.Max
|
||||
}
|
||||
}
|
||||
|
||||
data class FeeState(
|
||||
val selectedFeeType: FeeType = FeeType.NORMAL,
|
||||
val fees: TransactionFee? = null,
|
||||
val currentFee: Fee? = null,
|
||||
val feeIsIncluded: Boolean = false,
|
||||
val feeIsApproximate: Boolean = false,
|
||||
val mainLayoutIsVisible: Boolean = false,
|
||||
val controlsLayoutIsVisible: Boolean = false,
|
||||
val feeChipGroupIsVisible: Boolean = true,
|
||||
val includeFeeSwitcherIsEnabled: Boolean = true,
|
||||
val progressState: ProgressState = ProgressState.Done,
|
||||
) : SendScreenState {
|
||||
|
||||
override val stateId: StateId = StateId.FEE
|
||||
|
||||
fun isReady(): Boolean = currentFee != null
|
||||
|
||||
fun getCurrentFeeValue(): BigDecimal = currentFee?.amount?.value ?: BigDecimal.ZERO
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
// Shows only one type of the layout
|
||||
enum class ReceiptLayoutType {
|
||||
UNKNOWN,
|
||||
FIAT,
|
||||
CRYPTO,
|
||||
TOKEN_FIAT,
|
||||
TOKEN_CRYPTO,
|
||||
FEE_IN_CUSTOM_TOKEN,
|
||||
FEE_IN_CUSTOM_TOKEN_FIAT,
|
||||
SAME_CURRENCY,
|
||||
SAME_CURRENCY_FIAT,
|
||||
}
|
||||
|
||||
data class ReceiptState(
|
||||
val visibleTypeOfReceipt: ReceiptLayoutType? = null,
|
||||
val fiat: ReceiptFiat? = null,
|
||||
val crypto: ReceiptCrypto? = null,
|
||||
val tokenFiat: ReceiptTokenFiat? = null,
|
||||
val tokenCrypto: ReceiptTokenCrypto? = null,
|
||||
val customTokenFiat: ReceiptTokenFiat? = null,
|
||||
val customTokenCrypto: ReceiptTokenCrypto? = null,
|
||||
val sameCurrencyFiat: ReceiptFiat? = null,
|
||||
val sameCurrencyCrypto: ReceiptCrypto? = null,
|
||||
val mainCurrency: MainCurrency? = null,
|
||||
) : SendScreenState {
|
||||
override val stateId: StateId = StateId.RECEIPT
|
||||
}
|
||||
|
||||
data class ReceiptSymbols(
|
||||
val fiat: String,
|
||||
val crypto: String,
|
||||
val token: String? = null,
|
||||
val fee: String? = null,
|
||||
)
|
||||
|
||||
data class ReceiptFiat(
|
||||
val amountFiat: String,
|
||||
val feeFiat: String,
|
||||
val totalFiat: String,
|
||||
val willSentCrypto: String,
|
||||
val symbols: ReceiptSymbols,
|
||||
)
|
||||
|
||||
data class ReceiptCrypto(
|
||||
val amountCrypto: String,
|
||||
val feeCrypto: String,
|
||||
val totalCrypto: String,
|
||||
val feeFiat: String,
|
||||
val willSentFiat: String,
|
||||
val symbols: ReceiptSymbols,
|
||||
)
|
||||
|
||||
data class ReceiptTokenCrypto(
|
||||
val amountToken: String,
|
||||
val feeCoin: String,
|
||||
val totalFiat: String,
|
||||
val symbols: ReceiptSymbols,
|
||||
)
|
||||
|
||||
data class ReceiptTokenFiat(
|
||||
val amountFiat: String,
|
||||
val feeFiat: String,
|
||||
val totalFiat: String,
|
||||
val willSentToken: String,
|
||||
val willSentFeeCoin: String,
|
||||
val symbols: ReceiptSymbols,
|
||||
)
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.common.text.DecimalDigitsInputFilter
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
interface IdStateHolder {
|
||||
val stateId: StateId
|
||||
}
|
||||
|
||||
enum class StateId {
|
||||
SEND_SCREEN, ADDRESS_PAY_ID, TRANSACTION_EXTRAS, AMOUNT, FEE, RECEIPT
|
||||
}
|
||||
|
||||
interface SendScreenState : StateType, IdStateHolder
|
||||
|
||||
data class SendState(
|
||||
val walletManager: WalletManager? = null,
|
||||
val currency: CryptoCurrency? = null,
|
||||
val coinConverter: CurrencyConverter? = null,
|
||||
val tokenConverter: CurrencyConverter? = null,
|
||||
val customFeeConverter: CurrencyConverter? = null,
|
||||
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
|
||||
val addressState: AddressState = AddressState(),
|
||||
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 isSuccessSend: Boolean = false,
|
||||
val canIncludeFee: Boolean = false,
|
||||
) : SendScreenState {
|
||||
|
||||
override val stateId: StateId = StateId.SEND_SCREEN
|
||||
|
||||
fun getDecimals(type: MainCurrencyType): Int = when (type) {
|
||||
MainCurrencyType.FIAT -> 2
|
||||
MainCurrencyType.CRYPTO -> amountState.amountToExtract?.decimals ?: 0
|
||||
}
|
||||
|
||||
private fun convertFiatToCoin(value: BigDecimal): BigDecimal {
|
||||
return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value)
|
||||
}
|
||||
|
||||
private fun convertFiatToToken(value: BigDecimal): BigDecimal {
|
||||
return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value)
|
||||
}
|
||||
|
||||
private fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
|
||||
if (!this.coinIsConvertible()) return value
|
||||
|
||||
val converter = coinConverter!!
|
||||
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
|
||||
}
|
||||
|
||||
private fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
|
||||
if (!this.tokenIsConvertible()) return value
|
||||
|
||||
val converter = tokenConverter!!
|
||||
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
|
||||
}
|
||||
|
||||
fun convertFiatToExtractCrypto(fiatValue: BigDecimal): BigDecimal = when (amountState.typeOfAmount) {
|
||||
AmountType.Coin -> convertFiatToCoin(fiatValue)
|
||||
is AmountType.Token -> convertFiatToToken(fiatValue)
|
||||
is AmountType.FeeResource,
|
||||
AmountType.Reserve,
|
||||
-> fiatValue
|
||||
}
|
||||
|
||||
fun convertExtractCryptoToFiat(cryptoValue: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
|
||||
return when (amountState.typeOfAmount) {
|
||||
AmountType.Coin -> convertCoinToFiat(cryptoValue, scaleWithPrecision)
|
||||
is AmountType.Token -> convertTokenToFiat(cryptoValue, scaleWithPrecision)
|
||||
is AmountType.FeeResource,
|
||||
AmountType.Reserve,
|
||||
-> cryptoValue
|
||||
}
|
||||
}
|
||||
|
||||
fun getButtonState(): ButtonState = if (isReadyToSend()) ButtonState.ENABLED else ButtonState.DISABLED
|
||||
|
||||
fun getTotalAmountToSend(value: BigDecimal = amountState.amountToSendCrypto): BigDecimal {
|
||||
val needToExtractFee = amountState.canIncludeFee() && feeState.feeIsIncluded
|
||||
return if (needToExtractFee) value.minus(feeState.getCurrentFeeValue()) else value
|
||||
}
|
||||
|
||||
fun coinIsConvertible(): Boolean = coinConverter != null
|
||||
fun tokenIsConvertible(): Boolean = tokenConverter != null
|
||||
fun feeIsConvertible(): Boolean = customFeeConverter != null
|
||||
|
||||
fun mainCurrencyCanBeSwitched(): Boolean {
|
||||
return when (amountState.typeOfAmount) {
|
||||
AmountType.Coin -> coinIsConvertible()
|
||||
is AmountType.Token -> tokenIsConvertible()
|
||||
is AmountType.FeeResource,
|
||||
AmountType.Reserve,
|
||||
-> false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun addressIsReady(): Boolean = store.state.sendState.addressState.isReady()
|
||||
|
||||
private fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
|
||||
|
||||
fun isReadyToRequestFee(): Boolean = addressIsReady() && amountIsReady()
|
||||
|
||||
fun isReadyToSend(): Boolean = addressIsReady() && amountIsReady() &&
|
||||
store.state.sendState.feeState.isReady()
|
||||
}
|
||||
}
|
||||
|
||||
enum class ButtonState {
|
||||
ENABLED, DISABLED, PROGRESS
|
||||
}
|
||||
|
||||
data class AmountState(
|
||||
val amountToExtract: 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, AppCurrency.Default.code),
|
||||
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val hideBalance: Boolean = false,
|
||||
val cursorAtTheSamePosition: Boolean = true,
|
||||
val maxLengthOfAmount: Int = 2,
|
||||
val decimalSeparator: String = ".",
|
||||
val error: TapError? = null,
|
||||
val inputIsEnabled: Boolean = true,
|
||||
) : SendScreenState {
|
||||
|
||||
override val stateId: StateId = StateId.AMOUNT
|
||||
|
||||
fun isReady(): Boolean = error == null && !amountToSendCrypto.isZero()
|
||||
|
||||
fun canIncludeFee(): Boolean = store.state.sendState.canIncludeFee
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toBigDecimalSeparator(value: String): String {
|
||||
return value.replace(",", ".")
|
||||
}
|
||||
|
||||
fun restoreDecimalSeparator(value: String): String {
|
||||
return DecimalDigitsInputFilter.setDecimalSeparator(value, decimalSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
data class InputViewValue(val value: String, val isFromUserInput: Boolean = false)
|
||||
enum class MainCurrencyType {
|
||||
FIAT, CRYPTO
|
||||
}
|
||||
|
||||
data class MainCurrency(
|
||||
val type: MainCurrencyType,
|
||||
val currencySymbol: String,
|
||||
val isEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
data class ExternalTransactionData(
|
||||
val amount: String,
|
||||
val destinationAddress: String,
|
||||
val transactionId: String,
|
||||
val canAmountBeModified: Boolean = false,
|
||||
val canDestinationAddressBeModified: Boolean = false,
|
||||
)
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui
|
||||
|
||||
import android.R
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
class EditTextCustomPaste : TextInputEditText {
|
||||
|
||||
private var onSystemPasteButtonClickListener: (() -> Unit)? = null
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
fun setOnSystemPasteButtonClickListener(callback: () -> Unit) {
|
||||
onSystemPasteButtonClickListener = callback
|
||||
}
|
||||
|
||||
override fun onTextContextMenuItem(id: Int): Boolean {
|
||||
val isConsumed = super.onTextContextMenuItem(id)
|
||||
|
||||
if (id == R.id.paste) onSystemPasteButtonClickListener?.invoke()
|
||||
|
||||
return isConsumed
|
||||
}
|
||||
}
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.tap.features.send.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.method.DigitsKeyListener
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.tangem.Message
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.sdk.extensions.hideSoftKeyboard
|
||||
import com.tangem.tap.common.KeyboardObserver
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.getFromClipboard
|
||||
import com.tangem.tap.common.extensions.setOnImeActionListener
|
||||
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
|
||||
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
|
||||
import com.tangem.tap.common.text.truncateMiddleWith
|
||||
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.AddressActionUi.*
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.*
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.*
|
||||
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.adapters.WarningMessagesAdapter
|
||||
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentSendBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DecimalFormatSymbols
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
@OptIn(FlowPreview::class)
|
||||
@AndroidEntryPoint
|
||||
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
|
||||
lateinit var sendBtn: ViewStateWidget
|
||||
|
||||
private lateinit var etAmountToSend: TextInputEditText
|
||||
private lateinit var warningsAdapter: WarningMessagesAdapter
|
||||
|
||||
private val sendSubscriber = SendStateSubscriber(this)
|
||||
private lateinit var keyboardObserver: KeyboardObserver
|
||||
|
||||
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
|
||||
|
||||
@Inject
|
||||
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
|
||||
|
||||
@Inject
|
||||
lateinit var parseQrCodeUseCase: ParseQrCodeUseCase
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
sendSubscriber.initViewModel(viewModel)
|
||||
Analytics.send(Token.Send.ScreenOpened())
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
subscribeToTransactionExtrasFields()
|
||||
subscribeToAddressField()
|
||||
subscribeToAmountField()
|
||||
}
|
||||
}
|
||||
|
||||
addBackPressHandler(this)
|
||||
|
||||
etAmountToSend = view.findViewById(R.id.etAmountToSend)
|
||||
|
||||
initSendButtonStates()
|
||||
setupAddressLayout()
|
||||
setupAmountLayout()
|
||||
setupFeeLayout()
|
||||
setupWarningMessages()
|
||||
store.dispatch(SendActionUi.CheckIfTransactionDataWasProvided)
|
||||
}
|
||||
|
||||
private fun initSendButtonStates() = with(binding) {
|
||||
btnSend.setOnClickListener {
|
||||
store.dispatch(
|
||||
SendActionUi.SendAmountToRecipient(
|
||||
Message(getString(R.string.initial_message_sign_header)),
|
||||
),
|
||||
)
|
||||
}
|
||||
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
|
||||
}
|
||||
|
||||
private fun setupAddressLayout() = with(binding.lSendAddress) {
|
||||
store.dispatch(SetTruncateHandler { etAddress.truncateMiddleWith(it, "...") })
|
||||
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
|
||||
|
||||
etAddress.apply {
|
||||
setOnSystemPasteButtonClickListener {
|
||||
store.dispatch(
|
||||
PasteAddress(
|
||||
data = requireContext().getFromClipboard()?.toString() ?: "",
|
||||
sourceType = Token.Send.AddressEntered.SourceType.PastePopup,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
setOnFocusChangeListener { _, hasFocus ->
|
||||
store.dispatch(TruncateOrRestore(!hasFocus))
|
||||
}
|
||||
}
|
||||
|
||||
imvPaste.setOnClickListener {
|
||||
Analytics.send(Token.Send.ButtonPaste())
|
||||
store.dispatch(
|
||||
PasteAddress(
|
||||
data = requireContext().getFromClipboard()?.toString() ?: "",
|
||||
sourceType = Token.Send.AddressEntered.SourceType.PasteButton,
|
||||
),
|
||||
)
|
||||
store.dispatch(TruncateOrRestore(!etAddress.isFocused))
|
||||
}
|
||||
imvQrCode.setOnClickListener {
|
||||
Analytics.send(Token.Send.ButtonQRCode())
|
||||
|
||||
store.dispatchNavigationAction {
|
||||
push(AppRoute.QrScanning(source = SourceType.SEND))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.subscribeToAddressField() = with(binding.lSendAddress) {
|
||||
etAddress.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
|
||||
.onEach {
|
||||
store.dispatch(AddressActionUi.HandleUserInput(it))
|
||||
}
|
||||
.launchIn(this@subscribeToAddressField)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.subscribeToAmountField() {
|
||||
etAmountToSend.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
|
||||
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.subscribeToTransactionExtrasFields() = with(binding.lSendAddress) {
|
||||
// TODO: [REDACTED_TASK_KEY]
|
||||
etXlmMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.xlmMemo?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
|
||||
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||
|
||||
etDestinationTag.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.xrpDestinationTag?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
|
||||
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||
|
||||
etBinanceMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.binanceMemo?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) }
|
||||
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||
|
||||
etTonMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.tonMemoState?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) }
|
||||
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||
|
||||
etCosmosMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.cosmosMemoState?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
|
||||
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||
|
||||
etHederaMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.hederaMemoState?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) }
|
||||
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||
|
||||
etAlgorandMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.algorandMemoState?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) }
|
||||
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||
}
|
||||
|
||||
private fun setupAmountLayout() {
|
||||
store.dispatch(SetMainCurrency(restoreMainCurrency()))
|
||||
store.dispatch(ReceiptAction.RefreshReceipt)
|
||||
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))
|
||||
|
||||
val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
store.dispatch(AmountAction.SetDecimalSeparator(decimalSeparator))
|
||||
|
||||
binding.lSendAmount.tvAmountCurrency.setOnClickListener {
|
||||
Analytics.send(Token.Send.ButtonSwapCurrency())
|
||||
store.dispatch(ToggleMainCurrency)
|
||||
store.dispatch(ReceiptAction.RefreshReceipt)
|
||||
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))
|
||||
}
|
||||
|
||||
val maxAmountSnackbar = MaxAmountSnackbar.make(etAmountToSend) {
|
||||
etAmountToSend.clearFocus()
|
||||
etAmountToSend.postDelayed(200) { etAmountToSend.hideSoftKeyboard() }
|
||||
store.dispatch(SetMaxAmount)
|
||||
}
|
||||
var snackbarControlledByChangingFocus = false
|
||||
keyboardObserver = KeyboardObserver(requireActivity())
|
||||
keyboardObserver.registerListener { isShow ->
|
||||
if (snackbarControlledByChangingFocus) return@registerListener
|
||||
|
||||
if (isShow) {
|
||||
if (etAmountToSend.isFocused && !maxAmountSnackbar.isShown) maxAmountSnackbar.show()
|
||||
} else {
|
||||
if (maxAmountSnackbar.isShown) maxAmountSnackbar.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
etAmountToSend.keyListener = DigitsKeyListener.getInstance("0123456789,.")
|
||||
etAmountToSend.setOnFocusChangeListener { _, hasFocus ->
|
||||
snackbarControlledByChangingFocus = true
|
||||
if (hasFocus) {
|
||||
etAmountToSend.postDelayed(200) {
|
||||
maxAmountSnackbar.show()
|
||||
snackbarControlledByChangingFocus = false
|
||||
}
|
||||
} else {
|
||||
etAmountToSend.postDelayed(350) {
|
||||
maxAmountSnackbar.dismiss()
|
||||
snackbarControlledByChangingFocus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() == "") etAmountToSend.setText("0")
|
||||
}
|
||||
|
||||
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
|
||||
it.hideSoftKeyboard()
|
||||
it.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFeeLayout() = with(binding.clNetworkFee) {
|
||||
flExpandCollapse.flExpandCollapse.setOnClickListener {
|
||||
store.dispatch(ToggleControlsVisibility)
|
||||
}
|
||||
chipGroup.check(FeeUiHelper.toId(FeeType.NORMAL))
|
||||
chipGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||
if (checkedId == -1) return@setOnCheckedChangeListener
|
||||
|
||||
store.dispatch(ChangeSelectedFee(FeeUiHelper.toType(checkedId)))
|
||||
store.dispatch(CheckAmountToSend)
|
||||
}
|
||||
swIncludeFee.setOnCheckedChangeListener { _, isChecked ->
|
||||
store.dispatch(ChangeIncludeFee(isChecked))
|
||||
store.dispatch(CheckAmountToSend)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupWarningMessages() = with(binding) {
|
||||
warningsAdapter = WarningMessagesAdapter()
|
||||
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
rvWarningMessages.layoutManager = layoutManager
|
||||
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
|
||||
rvWarningMessages.adapter = warningsAdapter
|
||||
|
||||
store.dispatch(SendAction.Warnings.Update)
|
||||
}
|
||||
|
||||
override fun subscribeToStore() {
|
||||
store.subscribe(sendSubscriber) { appState ->
|
||||
appState.skipRepeats { oldState, newState ->
|
||||
oldState.sendState == newState.sendState
|
||||
}.select { it.sendState }
|
||||
}
|
||||
storeSubscribersList.add(sendSubscriber)
|
||||
}
|
||||
|
||||
private fun restoreMainCurrency(): MainCurrencyType {
|
||||
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
|
||||
val mainCurrency = sp.getString("mainCurrency", AppCurrency.Default.code)
|
||||
return MainCurrencyType.values()
|
||||
.firstOrNull { it.name.equals(mainCurrency!!, ignoreCase = true) }
|
||||
?: MainCurrencyType.CRYPTO
|
||||
}
|
||||
|
||||
fun saveMainCurrency(type: MainCurrencyType) {
|
||||
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
|
||||
sp.edit().putString("mainCurrency", type.name).apply()
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
val externalTransactionData = store.state.sendState.externalTransactionData
|
||||
if (externalTransactionData == null) {
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
} else {
|
||||
store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
keyboardObserver.unregisterListener()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
store.dispatch(ReleaseSendState)
|
||||
lifecycle.removeObserver(viewModel)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
fun EditText.inputtedTextAsFlow(): Flow<String> = callbackFlow {
|
||||
val watcher = addTextChangedListener { editable -> trySend(editable?.toString() ?: "") }
|
||||
awaitClose { removeTextChangedListener(watcher) }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
|
||||
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.di.DelayedWork
|
||||
import com.tangem.tap.features.send.redux.AddressActionUi
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appStateHolder: AppStateHolder,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
|
||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||
private val parseQrCodeUseCase: ParseQrCodeUseCase,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
init {
|
||||
listenToQrScanningUseCase(SourceType.SEND)
|
||||
.getOrElse { emptyFlow() }
|
||||
.onEach(::onQRCodeScanned)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private val cryptoCurrency: CryptoCurrency? = savedStateHandle[AppRoute.Send.CRYPTO_CURRENCY_KEY]
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach {
|
||||
appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(it.isBalanceHidden))
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun updateCurrencyDelayed() {
|
||||
if (cryptoCurrency != null) {
|
||||
coroutineScope.launch {
|
||||
getSelectedWalletSyncUseCase()
|
||||
.fold(
|
||||
ifLeft = { Timber.e(it.toString()) },
|
||||
ifRight = { wallet ->
|
||||
// we should update network to find pending tx after 1 sec
|
||||
updateForPendingTx(wallet, cryptoCurrency.network)
|
||||
// we should update network for new balance
|
||||
updateForBalance(wallet, cryptoCurrency.network)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Timber.w("$TAG: cryptoCurrency is null, legacy flow")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateForPendingTx(userWallet: UserWallet, network: Network) {
|
||||
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(network))
|
||||
}
|
||||
|
||||
private suspend fun updateForBalance(userWallet: UserWallet, network: Network) {
|
||||
updateDelayedCurrencyStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = network,
|
||||
delayMillis = UPDATE_BALANCE_DELAY_MILLIS,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onQRCodeScanned(qrScanResult: String) {
|
||||
if (cryptoCurrency != null) {
|
||||
parseQrCodeUseCase(qrScanResult, cryptoCurrency).fold(
|
||||
ifRight = { parsedCode ->
|
||||
store.dispatch(
|
||||
AddressActionUi.PasteAddress(
|
||||
data = parsedCode.address,
|
||||
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
|
||||
),
|
||||
)
|
||||
parsedCode.amount?.let { amount ->
|
||||
store.dispatch(AmountAction.SetAmount(amount, isUserInput = false))
|
||||
}
|
||||
// parsedCode.memo?.let { }
|
||||
},
|
||||
ifLeft = {
|
||||
store.dispatch(
|
||||
AddressActionUi.PasteAddress(
|
||||
data = qrScanResult,
|
||||
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
|
||||
),
|
||||
)
|
||||
Timber.w(it)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
store.dispatch(
|
||||
AddressActionUi.PasteAddress(
|
||||
data = qrScanResult,
|
||||
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
store.dispatch(AddressActionUi.TruncateOrRestore(truncate = true))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||
private const val TAG = "SendViewModel"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.LayoutWarningCardActionBinding
|
||||
import timber.log.Timber
|
||||
|
||||
// TODO: Delete with SendFeatureToggles
|
||||
@Deprecated(message = "Used only in old send screen")
|
||||
class WarningMessagesAdapter : ListAdapter<WarningMessage, WarningMessageVH>(DiffUtilCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH {
|
||||
val binding = LayoutWarningCardActionBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false,
|
||||
)
|
||||
return WarningMessageVH(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: WarningMessageVH, position: Int) {
|
||||
holder.bind(currentList[position])
|
||||
}
|
||||
|
||||
object DiffUtilCallback : DiffUtil.ItemCallback<WarningMessage>() {
|
||||
override fun areContentsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(warning: WarningMessage) {
|
||||
setBgColor(warning.priority)
|
||||
setText(warning)
|
||||
setupControlButtons(warning)
|
||||
}
|
||||
|
||||
private fun setText(warning: WarningMessage) = with(binding.warningContentContainer) {
|
||||
fun getString(resId: Int?, default: String, formatArgs: String? = null) =
|
||||
if (resId == null) default else root.getString(resId, formatArgs)
|
||||
|
||||
tvTitle.text = getString(
|
||||
resId = warning.titleResId,
|
||||
default = warning.title,
|
||||
formatArgs = warning.titleFormatArg,
|
||||
)
|
||||
|
||||
tvMessage.text = getString(
|
||||
resId = warning.messageResId,
|
||||
default = warning.message,
|
||||
formatArgs = warning.messageFormatArg,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setBgColor(priority: WarningMessage.Priority) {
|
||||
val color = when (priority) {
|
||||
WarningMessage.Priority.Info -> R.color.warning_info
|
||||
WarningMessage.Priority.Warning -> R.color.warning_warning
|
||||
WarningMessage.Priority.Critical -> R.color.warning_critical
|
||||
}
|
||||
binding.warningCardAction.setCardBackgroundColor(binding.root.getColor(color))
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun setupControlButtons(warning: WarningMessage) = when (warning.type) {
|
||||
WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> {
|
||||
binding.groupControlsTemporary.hide()
|
||||
binding.btnClose.hide()
|
||||
}
|
||||
WarningMessage.Type.Temporary -> {
|
||||
binding.groupControlsTemporary.show()
|
||||
binding.btnClose.hide()
|
||||
|
||||
val buttonAction = View.OnClickListener {
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
}
|
||||
val buttonTitle = binding.root.getString(
|
||||
warning.buttonTextId ?: R.string.how_to_got_it_button,
|
||||
)
|
||||
binding.btnGotIt.setOnClickListener(buttonAction)
|
||||
binding.btnGotIt.text = buttonTitle
|
||||
}
|
||||
WarningMessage.Type.AppRating -> {
|
||||
binding.groupControlsTemporary.hide()
|
||||
binding.btnClose.show()
|
||||
binding.btnClose.setOnClickListener {
|
||||
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed))
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
}
|
||||
binding.btnReallyCool.setOnClickListener {
|
||||
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
|
||||
|
||||
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked))
|
||||
val reviewManager = ReviewManagerFactory.create(activity)
|
||||
val task = reviewManager.requestReviewFlow()
|
||||
task.addOnCompleteListener {
|
||||
if (!it.isSuccessful) {
|
||||
Timber.e(task.exception)
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
Timber.e(it)
|
||||
}
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
// TODO: [REDACTED_JIRA]
|
||||
object ChiaWarningDialog {
|
||||
|
||||
fun create(context: Context, dialog: SendAction.Dialog.ChiaWarningDialog): AlertDialog {
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.common_warning)
|
||||
setMessage(
|
||||
context.getString(
|
||||
R.string.common_utxo_validate_withdrawal_message_warning,
|
||||
dialog.blockchainName,
|
||||
dialog.maxOutputs,
|
||||
dialog.maxAmount.toPlainString(),
|
||||
),
|
||||
)
|
||||
setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
dialog.onOk()
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(SendAction.Dialog.Hide)
|
||||
}
|
||||
}
|
||||
.create()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
object KaspaWarningDialog {
|
||||
|
||||
fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog {
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.common_warning)
|
||||
setMessage(
|
||||
context.getString(
|
||||
R.string.common_utxo_validate_withdrawal_message_warning,
|
||||
Blockchain.Kaspa.fullName,
|
||||
dialog.maxOutputs,
|
||||
dialog.maxAmount.toPlainString(),
|
||||
),
|
||||
)
|
||||
setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
dialog.onOk()
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(SendAction.Dialog.Hide)
|
||||
}
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object RequestFeeErrorDialog {
|
||||
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.details_row_title_contact_to_support) { _, _ ->
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
|
||||
store.dispatch(
|
||||
GlobalAction.SendEmail(
|
||||
feedbackData = SendTransactionFailedEmail(errorMessage),
|
||||
scanResponse = dialog.scanResponse,
|
||||
),
|
||||
)
|
||||
}
|
||||
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
|
||||
setNeutralButton(R.string.common_cancel) { _, _ -> }
|
||||
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.sdk.extensions.localizedDescription
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object SendTransactionFailsDialog {
|
||||
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog {
|
||||
return create(context, dialog.error.localizedDescription(context), dialog.scanResponse)
|
||||
}
|
||||
|
||||
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog {
|
||||
val errorConverter = BlockchainSdkErrorConverter(context)
|
||||
return create(context, errorConverter.convert(dialog.error), dialog.scanResponse)
|
||||
}
|
||||
|
||||
private fun create(context: Context, errorMessage: String, scanResponse: ScanResponse): 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.details_row_title_contact_to_support) { _, _ ->
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
|
||||
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage), scanResponse))
|
||||
}
|
||||
setPositiveButton(R.string.common_cancel) { _, _ -> }
|
||||
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockchainSdkErrorConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<BlockchainSdkError, String> {
|
||||
|
||||
override fun convert(message: BlockchainSdkError): String {
|
||||
return when (message) {
|
||||
is BlockchainSdkError.CreateAccountUnderfunded -> {
|
||||
val resStringId = when (message.blockchain) {
|
||||
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.string.no_account_polkadot
|
||||
else -> R.string.send_error_no_target_account
|
||||
}
|
||||
val reserveValueString = message.minReserve.value?.stripZeroPlainString() ?: "0"
|
||||
val argument = "$reserveValueString ${message.minReserve.currencySymbol}"
|
||||
context.getString(resStringId, argument)
|
||||
}
|
||||
else -> message.customMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
class TezosWarningDialog(context: Context) : AlertDialog(context) {
|
||||
|
||||
companion object {
|
||||
fun create(context: Context, showDialogData: SendAction.Dialog.TezosWarningDialog): AlertDialog {
|
||||
val reduceAmount = showDialogData.reduceAmount.toPlainString()
|
||||
return Builder(context).apply {
|
||||
setTitle(R.string.common_warning)
|
||||
setMessage(context.getString(R.string.xtz_withdrawal_message_warning, reduceAmount))
|
||||
setNegativeButton(R.string.xtz_withdrawal_message_ignore) { _, _ ->
|
||||
showDialogData.sendAllCallback()
|
||||
}
|
||||
setPositiveButton(context.getString(R.string.xtz_withdrawal_message_reduce, reduceAmount)) { _, _ ->
|
||||
showDialogData.reduceCallback()
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(SendAction.Dialog.Hide)
|
||||
}
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.stateSubscribers
|
||||
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import org.rekotlin.StateType
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class FragmentStateSubscriber<S : StateType>(fragment: BaseStoreFragment) : StoreSubscriber<S> {
|
||||
private val weakFragment: WeakReference<BaseStoreFragment> = WeakReference(fragment)
|
||||
|
||||
abstract fun updateWithNewState(fg: BaseStoreFragment, state: S)
|
||||
|
||||
override fun newState(state: S) {
|
||||
val fg = weakFragment.get() ?: return
|
||||
|
||||
updateWithNewState(fg, state)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,530 +0,0 @@
|
|||
package com.tangem.tap.features.send.ui.stateSubscribers
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.text.bold
|
||||
import com.tangem.common.extensions.remove
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.getMessageString
|
||||
import com.tangem.tap.common.text.DecimalDigitsInputFilter
|
||||
import com.tangem.tap.domain.MultiMessageError
|
||||
import com.tangem.tap.domain.assembleErrors
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.send.redux.states.*
|
||||
import com.tangem.tap.features.send.ui.FeeUiHelper
|
||||
import com.tangem.tap.features.send.ui.SendFragment
|
||||
import com.tangem.tap.features.send.ui.SendViewModel
|
||||
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
|
||||
import com.tangem.tap.features.send.ui.dialogs.*
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
internal class SendStateSubscriber(
|
||||
fragment: BaseStoreFragment,
|
||||
) : FragmentStateSubscriber<SendState>(fragment) {
|
||||
|
||||
private var dialog: Dialog? = null
|
||||
private var sendViewModel: SendViewModel? = null
|
||||
fun initViewModel(viewModel: SendViewModel) {
|
||||
sendViewModel = viewModel
|
||||
}
|
||||
|
||||
override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) {
|
||||
fg.view ?: return
|
||||
if (fg !is SendFragment) return
|
||||
if (state.isSuccessSend) {
|
||||
sendViewModel?.updateCurrencyDelayed()
|
||||
return
|
||||
}
|
||||
|
||||
val lastChangedStates = state.lastChangedStates.toList()
|
||||
state.lastChangedStates.clear()
|
||||
(fg as? SendFragment)?.binding?.mainSendContainer?.beginDelayedTransition()
|
||||
lastChangedStates.forEach {
|
||||
when (it) {
|
||||
StateId.SEND_SCREEN -> handleSendScreen(fg, state)
|
||||
StateId.ADDRESS_PAY_ID -> handleAddressState(fg, state.addressState)
|
||||
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
|
||||
StateId.AMOUNT -> handleAmountState(fg, state.amountState)
|
||||
StateId.FEE -> handleFeeState(fg, state.feeState)
|
||||
StateId.RECEIPT -> handleReceiptState(fg, state.receiptState, state.feeState.progressState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Legacy")
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
|
||||
with(fg.binding.lSendAddress) {
|
||||
fun showView(view: View, info: Any?) {
|
||||
view.show(info != null)
|
||||
}
|
||||
showView(xlmMemoContainer, infoState.xlmMemo)
|
||||
showView(xrpDestinationTagContainer, infoState.xrpDestinationTag)
|
||||
showView(binanceMemoContainer, infoState.binanceMemo)
|
||||
showView(tonMemoContainer, infoState.tonMemoState)
|
||||
showView(cosmosMemoContainer, infoState.cosmosMemoState)
|
||||
showView(hederaMemoContainer, infoState.hederaMemoState)
|
||||
showView(algorandMemoContainer, infoState.algorandMemoState)
|
||||
|
||||
infoState.xlmMemo?.let {
|
||||
if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value)
|
||||
if (it.error != null) {
|
||||
if (it.error == TransactionExtraError.INVALID_XLM_MEMO) {
|
||||
tilXlmMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
}
|
||||
} else {
|
||||
tilXlmMemo.error = null
|
||||
}
|
||||
}
|
||||
infoState.xrpDestinationTag?.let {
|
||||
if (infoState.xrpDestinationTag.error != null) {
|
||||
if (infoState.xrpDestinationTag.error == TransactionExtraError.INVALID_DESTINATION_TAG) {
|
||||
tilDestinationTag.error =
|
||||
fg.getText(R.string.send_extras_error_invalid_destination_tag)
|
||||
}
|
||||
} else {
|
||||
tilDestinationTag.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etDestinationTag.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
infoState.binanceMemo?.let {
|
||||
if (infoState.binanceMemo.error != null) {
|
||||
if (infoState.binanceMemo.error == TransactionExtraError.INVALID_BINANCE_MEMO) {
|
||||
tilBinanceMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
}
|
||||
} else {
|
||||
tilBinanceMemo.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etBinanceMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
infoState.tonMemoState?.let {
|
||||
if (infoState.tonMemoState.error != null) {
|
||||
tilTonMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
} else {
|
||||
tilBinanceMemo.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etTonMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
infoState.cosmosMemoState?.let {
|
||||
if (infoState.cosmosMemoState.error != null) {
|
||||
tilCosmosMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
} else {
|
||||
tilBinanceMemo.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etCosmosMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
infoState.hederaMemoState?.let {
|
||||
if (infoState.hederaMemoState.error != null) {
|
||||
tilHederaMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
} else {
|
||||
tilHederaMemo.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etHederaMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
infoState.algorandMemoState?.let {
|
||||
if (infoState.algorandMemoState.error != null) {
|
||||
tilAlgorandMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
} else {
|
||||
tilAlgorandMemo.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etAlgorandMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private fun handleSendScreen(fg: SendFragment, state: SendState) = with(fg.binding) {
|
||||
when (state.dialog) {
|
||||
is SendAction.Dialog.TezosWarningDialog -> {
|
||||
if (dialog == null) {
|
||||
dialog = TezosWarningDialog.create(fg.requireContext(), state.dialog)
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
is SendAction.Dialog.KaspaWarningDialog -> {
|
||||
if (dialog == null) {
|
||||
dialog = KaspaWarningDialog.create(fg.requireContext(), state.dialog)
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
is SendAction.Dialog.ChiaWarningDialog -> {
|
||||
if (dialog == null) {
|
||||
dialog = ChiaWarningDialog.create(fg.requireContext(), state.dialog)
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
is SendAction.Dialog.SendTransactionFails.CardSdkError -> {
|
||||
if (dialog == null) {
|
||||
dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog)
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> {
|
||||
if (dialog == null) {
|
||||
dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog)
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
is SendAction.Dialog.RequestFeeError -> {
|
||||
if (dialog == null) {
|
||||
dialog = RequestFeeErrorDialog.create(fg.requireContext(), state.dialog)
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
}
|
||||
}
|
||||
|
||||
fg.sendBtn.changeState(state.sendButtonState.progressState)
|
||||
fg.sendBtn.mainView.isEnabled = state.sendButtonState.enabled
|
||||
|
||||
val rv = rvWarningMessages
|
||||
val adapter = rv.adapter as? WarningMessagesAdapter ?: return
|
||||
|
||||
adapter.submitList(state.sendWarningsList)
|
||||
rv.show(state.sendWarningsList.isNotEmpty())
|
||||
|
||||
toolbar.title = fg.getString(
|
||||
R.string.send_title_currency_format,
|
||||
state.amountState.mainCurrency.currencySymbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleAddressState(fg: SendFragment, state: AddressState) {
|
||||
with(fg.binding.lSendAddress) {
|
||||
fun parseError(context: Context, error: Error?): String? {
|
||||
val resId = when (error) {
|
||||
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address
|
||||
Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet
|
||||
else -> null
|
||||
}
|
||||
return if (resId == null) null else context.getString(resId, "", "")
|
||||
}
|
||||
|
||||
imvPaste.isEnabled = state.pasteIsEnabled
|
||||
|
||||
val et = etAddress
|
||||
val til = tilAddress
|
||||
val parsedError = parseError(til.context, state.error)
|
||||
|
||||
til.isEnabled = state.inputIsEnabled
|
||||
imvPaste.show(state.inputIsEnabled)
|
||||
imvQrCode.show(state.inputIsEnabled)
|
||||
flPaste.show(state.inputIsEnabled)
|
||||
flQrCode.show(state.inputIsEnabled)
|
||||
|
||||
til.hint = til.getString(R.string.send_destination_hint_address)
|
||||
til.error = parsedError
|
||||
til.isErrorEnabled = parsedError != null
|
||||
til.helperText = state.destinationWalletAddress
|
||||
til.isHelperTextEnabled = parsedError == null
|
||||
|
||||
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAmountState(fg: SendFragment, state: AmountState) = with(fg.binding.lSendAmount) {
|
||||
if (state.error != null) {
|
||||
val context = fg.requireContext()
|
||||
val message = when (state.error) {
|
||||
is MultiMessageError -> {
|
||||
val multiError = state.error as MultiMessageError
|
||||
val messageList = multiError.assembleErrors()
|
||||
.map { getMessageString(context, it.first, it.second) }
|
||||
multiError.builder(messageList)
|
||||
}
|
||||
else -> context.getString(state.error.messageResource)
|
||||
}
|
||||
tilAmountToSend.enableError(true, message)
|
||||
} else {
|
||||
tilAmountToSend.enableError(false)
|
||||
}
|
||||
|
||||
val filter = DecimalDigitsInputFilter(
|
||||
digitsBeforeDecimal = 40,
|
||||
digitsAfterDecimal = state.maxLengthOfAmount,
|
||||
decimalSeparator = state.decimalSeparator,
|
||||
)
|
||||
etAmountToSend.filters = arrayOf(filter)
|
||||
val amountToSend = state.viewAmountValue
|
||||
if (!amountToSend.isFromUserInput) etAmountToSend.update(amountToSend.value)
|
||||
|
||||
tvAmountCurrency.update(state.mainCurrency.currencySymbol)
|
||||
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)
|
||||
|
||||
val balanceText = when (state.mainCurrency.type) {
|
||||
MainCurrencyType.FIAT -> fg.getString(
|
||||
R.string.common_balance,
|
||||
"${state.viewBalanceValue} ${state.mainCurrency.currencySymbol}",
|
||||
).remove(":")
|
||||
MainCurrencyType.CRYPTO -> fg.getString(
|
||||
R.string.common_balance,
|
||||
"${state.mainCurrency.currencySymbol} ${state.viewBalanceValue}",
|
||||
)
|
||||
}
|
||||
|
||||
tvBalance.update(balanceText)
|
||||
|
||||
tilAmountToSend.isEnabled = state.inputIsEnabled
|
||||
|
||||
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
|
||||
tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
|
||||
val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary
|
||||
tvAmountCurrency.setTextColor(fg.getColor(textColor))
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun handleFeeState(fg: SendFragment, state: FeeState) {
|
||||
with(fg.binding.clNetworkFee) {
|
||||
fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible)
|
||||
flExpandCollapse.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f
|
||||
llFeeControlsContainer.show(state.controlsLayoutIsVisible)
|
||||
chipGroup.show(state.feeChipGroupIsVisible)
|
||||
|
||||
swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled
|
||||
if (swIncludeFee.isChecked != state.feeIsIncluded) {
|
||||
swIncludeFee.isChecked = state.feeIsIncluded
|
||||
}
|
||||
|
||||
val chipId = FeeUiHelper.toId(state.selectedFeeType)
|
||||
if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId)
|
||||
}
|
||||
with(fg.binding.clReceiptContainer) {
|
||||
when (state.progressState) {
|
||||
ProgressState.Loading -> {
|
||||
tvReceiptFeeValue.hide()
|
||||
pbReceiptFee.show()
|
||||
}
|
||||
ProgressState.Done -> {
|
||||
pbReceiptFee.hide()
|
||||
tvReceiptFeeValue.show()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "ComplexMethod")
|
||||
private fun handleReceiptState(fg: SendFragment, state: ReceiptState, feeProgressState: ProgressState) {
|
||||
with(fg.binding.clReceiptContainer) {
|
||||
val mainLayout = clReceiptContainer as ViewGroup
|
||||
val totalLayout = llTotalContainer.llTotal as ViewGroup
|
||||
val totalTokenLayout = llTotalContainer.flTotalTokenCrypto as ViewGroup
|
||||
|
||||
fun getString(id: Int, vararg formatStrings: String): String = mainLayout.getString(id, *formatStrings)
|
||||
|
||||
fun roughOrEmpty(value: String): String {
|
||||
return if (value == BigDecimalFormatter.EMPTY_BALANCE_SIGN) value else "$ROUGH_SIGN $value"
|
||||
}
|
||||
|
||||
when (feeProgressState) {
|
||||
ProgressState.Loading -> {
|
||||
tvReceiptFeeValue.hide()
|
||||
pbReceiptFee.show()
|
||||
}
|
||||
ProgressState.Done -> {
|
||||
pbReceiptFee.hide()
|
||||
tvReceiptFeeValue.show()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
when (state.visibleTypeOfReceipt) {
|
||||
ReceiptLayoutType.FIAT -> {
|
||||
val receipt = state.fiat ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
|
||||
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
|
||||
llTotalContainer.tvTotalValue.post {
|
||||
llTotalContainer.tvTotalValue.update(
|
||||
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
|
||||
)
|
||||
}
|
||||
|
||||
val willSent = getString(
|
||||
R.string.send_total_subtitle_format,
|
||||
"${receipt.willSentCrypto} ${receipt.symbols.crypto}",
|
||||
)
|
||||
llTotalContainer.tvWillBeSentValue.update(willSent)
|
||||
}
|
||||
ReceiptLayoutType.CRYPTO -> {
|
||||
val receipt = state.crypto ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
tvReceiptAmountValue.update("${receipt.amountCrypto} ${receipt.symbols.crypto}")
|
||||
tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.crypto}")
|
||||
llTotalContainer.tvTotalValue.post {
|
||||
llTotalContainer.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}")
|
||||
}
|
||||
|
||||
if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) {
|
||||
llTotalContainer.tvWillBeSentValue.hide()
|
||||
} else {
|
||||
llTotalContainer.tvWillBeSentValue.show()
|
||||
llTotalContainer.tvWillBeSentValue.update(
|
||||
getString(
|
||||
R.string.send_total_subtitle_fiat_format,
|
||||
"${receipt.willSentFiat} ${receipt.symbols.fiat}",
|
||||
"${receipt.feeFiat} ${receipt.symbols.fiat}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
ReceiptLayoutType.TOKEN_FIAT -> {
|
||||
val receipt = state.tokenFiat ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
|
||||
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
|
||||
llTotalContainer.tvTotalValue.post {
|
||||
llTotalContainer.tvTotalValue.update(
|
||||
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
|
||||
)
|
||||
}
|
||||
|
||||
val willSent = getString(
|
||||
R.string.send_total_subtitle_asset_format,
|
||||
"${receipt.symbols.token ?: ""} ${receipt.willSentToken}",
|
||||
"${receipt.symbols.crypto} ${receipt.willSentFeeCoin}",
|
||||
)
|
||||
llTotalContainer.tvWillBeSentValue.update(willSent)
|
||||
}
|
||||
ReceiptLayoutType.TOKEN_CRYPTO -> {
|
||||
val receipt = state.tokenCrypto ?: return
|
||||
|
||||
totalLayout.show(false)
|
||||
totalTokenLayout.show(true)
|
||||
|
||||
tvReceiptAmountValue.update("${receipt.amountToken} ${receipt.symbols.token}")
|
||||
tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.crypto}")
|
||||
|
||||
val willSent = SpannableStringBuilder()
|
||||
.bold {
|
||||
append(roughOrEmpty(receipt.totalFiat)).append(" ")
|
||||
append(receipt.symbols.fiat)
|
||||
}
|
||||
llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString())
|
||||
}
|
||||
ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN -> {
|
||||
val receipt = state.customTokenCrypto ?: return
|
||||
|
||||
totalLayout.show(false)
|
||||
totalTokenLayout.show(true)
|
||||
|
||||
tvReceiptAmountValue.update(
|
||||
"${receipt.amountToken} ${receipt.symbols.token ?: receipt.symbols.crypto}",
|
||||
)
|
||||
tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.fee}")
|
||||
|
||||
val willSent = SpannableStringBuilder()
|
||||
.bold {
|
||||
append(roughOrEmpty(receipt.totalFiat)).append(" ")
|
||||
append(receipt.symbols.fiat)
|
||||
}
|
||||
llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString())
|
||||
}
|
||||
ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT -> {
|
||||
val receipt = state.customTokenFiat ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
|
||||
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
|
||||
llTotalContainer.tvTotalValue.post {
|
||||
llTotalContainer.tvTotalValue.update(
|
||||
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
|
||||
)
|
||||
}
|
||||
|
||||
val willSent = getString(
|
||||
R.string.send_total_subtitle_asset_format,
|
||||
"${receipt.symbols.token ?: receipt.symbols.crypto} ${receipt.willSentToken}",
|
||||
"${receipt.symbols.fee} ${receipt.willSentFeeCoin}",
|
||||
)
|
||||
llTotalContainer.tvWillBeSentValue.update(willSent)
|
||||
}
|
||||
|
||||
ReceiptLayoutType.SAME_CURRENCY -> {
|
||||
val receipt = state.sameCurrencyCrypto ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
tvReceiptAmountValue.update(
|
||||
"${receipt.amountCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
|
||||
)
|
||||
tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}")
|
||||
llTotalContainer.tvTotalValue.post {
|
||||
llTotalContainer.tvTotalValue.update(
|
||||
"${receipt.totalCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
|
||||
)
|
||||
}
|
||||
|
||||
if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) {
|
||||
llTotalContainer.tvWillBeSentValue.hide()
|
||||
} else {
|
||||
llTotalContainer.tvWillBeSentValue.show()
|
||||
llTotalContainer.tvWillBeSentValue.update(
|
||||
getString(
|
||||
R.string.send_total_subtitle_fiat_format,
|
||||
"${receipt.willSentFiat} ${receipt.symbols.fiat}",
|
||||
"${receipt.feeFiat} ${receipt.symbols.fiat}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
ReceiptLayoutType.SAME_CURRENCY_FIAT -> {
|
||||
val receipt = state.sameCurrencyFiat ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
|
||||
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
|
||||
llTotalContainer.tvTotalValue.post {
|
||||
llTotalContainer.tvTotalValue.update(
|
||||
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
|
||||
)
|
||||
}
|
||||
|
||||
val willSent = getString(
|
||||
R.string.send_total_subtitle_format,
|
||||
"${receipt.willSentCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
|
||||
)
|
||||
llTotalContainer.tvWillBeSentValue.update(willSent)
|
||||
}
|
||||
ReceiptLayoutType.UNKNOWN, null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ROUGH_SIGN = "≈"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.tap.features.wallet.redux.middlewares
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
|
|
@ -17,11 +15,8 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
|||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -45,9 +40,6 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private val isSendRedesignedEnabled: Boolean
|
||||
get() = store.inject(getDependency = DaggerGraphState::sendFeatureToggles).isRedesignedSendEnabled
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
|
||||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
|
@ -62,20 +54,8 @@ object TradeCryptoMiddleware {
|
|||
cryptoCurrencyId = action.cryptoCurrencyId,
|
||||
yield = action.yield,
|
||||
)
|
||||
is TradeCryptoAction.SendToken -> {
|
||||
if (isSendRedesignedEnabled) {
|
||||
handleNewSendToken(action = action)
|
||||
} else {
|
||||
handleSendToken(action = action)
|
||||
}
|
||||
}
|
||||
is TradeCryptoAction.SendCoin -> {
|
||||
if (isSendRedesignedEnabled) {
|
||||
handleNewSendCoin(action = action)
|
||||
} else {
|
||||
handleSendCoin(action = action)
|
||||
}
|
||||
}
|
||||
is TradeCryptoAction.SendToken -> handleNewSendToken(action = action)
|
||||
is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,135 +157,6 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleSendToken(action: TradeCryptoAction.SendToken) {
|
||||
val currency = action.tokenCurrency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
||||
scope.launch {
|
||||
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
|
||||
.getOrCreateWalletManager(
|
||||
userWalletId = action.userWallet.walletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = currency.network.derivationPath.value,
|
||||
)
|
||||
|
||||
if (walletManager == null) {
|
||||
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
|
||||
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
|
||||
store.dispatchErrorNotification(error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val sendableAmount = walletManager.wallet.amounts.values.firstOrNull {
|
||||
val amountType = it.type
|
||||
amountType is AmountType.Token && amountType.token.contractAddress == currency.contractAddress
|
||||
}
|
||||
|
||||
store.dispatchOnMain(
|
||||
action = PrepareSendScreen(
|
||||
walletManager = walletManager,
|
||||
feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
|
||||
currency = currency,
|
||||
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
|
||||
coinRate = action.coinFiatRate,
|
||||
tokenAmount = sendableAmount,
|
||||
tokenRate = action.tokenFiatRate,
|
||||
feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate,
|
||||
feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0,
|
||||
),
|
||||
)
|
||||
|
||||
val txInfo = action.transactionInfo
|
||||
if (txInfo != null) {
|
||||
store.dispatchOnMain(
|
||||
SendAction.SendSpecificTransaction(
|
||||
sendAmount = txInfo.amount,
|
||||
destinationAddress = txInfo.destinationAddress,
|
||||
transactionId = txInfo.transactionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val route = AppRoute.Send(
|
||||
currency = currency,
|
||||
userWalletId = action.userWallet.walletId,
|
||||
)
|
||||
|
||||
store.dispatchNavigationAction { push(route) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendCoin(action: TradeCryptoAction.SendCoin) {
|
||||
if (action.transactionInfo?.tag != null) {
|
||||
// avoid open old send if memo exists
|
||||
return
|
||||
}
|
||||
val cryptoStatus = action.coinStatus
|
||||
val currency = cryptoStatus.currency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
||||
scope.launch {
|
||||
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
|
||||
.getOrCreateWalletManager(
|
||||
userWalletId = action.userWallet.walletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = currency.network.derivationPath.value,
|
||||
)
|
||||
|
||||
if (walletManager == null) {
|
||||
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
|
||||
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
|
||||
store.dispatchErrorNotification(error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin }
|
||||
when (currency) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol }
|
||||
|
||||
if (amountToSend == null) {
|
||||
val error = TapError.UnsupportedState(stateError = "Amount to send is null")
|
||||
FirebaseCrashlytics.getInstance()
|
||||
.recordException(IllegalStateException(error.stateError))
|
||||
store.dispatchErrorNotification(error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
store.dispatchOnMain(
|
||||
action = PrepareSendScreen(
|
||||
walletManager = walletManager,
|
||||
feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
|
||||
currency = currency,
|
||||
coinAmount = amountToSend,
|
||||
coinRate = cryptoStatus.value.fiatRate,
|
||||
feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate,
|
||||
feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
|
||||
}
|
||||
|
||||
val txInfo = action.transactionInfo
|
||||
if (txInfo != null) {
|
||||
store.dispatchOnMain(
|
||||
SendAction.SendSpecificTransaction(
|
||||
sendAmount = txInfo.amount,
|
||||
destinationAddress = txInfo.destinationAddress,
|
||||
transactionId = txInfo.transactionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val route = AppRoute.Send(
|
||||
currency = currency,
|
||||
userWalletId = action.userWallet.walletId,
|
||||
)
|
||||
store.dispatchNavigationAction { push(route) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
|
||||
handleNewSend(
|
||||
userWalletId = action.userWallet.walletId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue