Updated on 2026-08-14
This commit is contained in:
commit
e49ccb9a12
39 changed files with 929 additions and 433 deletions
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.legacy
|
|||
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -20,6 +21,11 @@ internal object LegacyMiddleware {
|
|||
GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup),
|
||||
)
|
||||
}
|
||||
is LegacyAction.SendEmailTransactionFailed -> {
|
||||
store.state.globalState.feedbackManager?.sendEmail(
|
||||
SendTransactionFailedEmail(action.errorMessage),
|
||||
)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
|||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,24 +19,21 @@ internal object TransactionDomainModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetFeeUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade, dispatchers)
|
||||
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSendTransactionUseCase(
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
transactionRepository: TransactionRepository,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
transactionRepository = transactionRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@
|
|||
<string name="common_transfer">Перевод</string>
|
||||
<string name="common_understand">Я понял</string>
|
||||
<string name="common_unreachable">Недоступно</string>
|
||||
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
|
||||
<string name="common_yes">Да</string>
|
||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
<string name="currency_subtitle_expanded">Доступные сети</string>
|
||||
|
|
@ -448,8 +449,9 @@
|
|||
<string name="send_amount_label">Сумма</string>
|
||||
<string name="send_amount_substract">Вычесть из суммы отправки</string>
|
||||
<string name="send_amount_substract_footer">Сумма к получению %s</string>
|
||||
<string name="send_alert_button_request_support">Поддержка</string>
|
||||
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
|
||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\Код:%2$s</string>
|
||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
|
||||
<string name="send_date_format">%1$s в %2$s</string>
|
||||
<string name="send_destination_hint_address">Адрес</string>
|
||||
<string name="send_destination_tag_field">Код назначения</string>
|
||||
|
|
@ -477,8 +479,10 @@
|
|||
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
|
||||
<string name="send_notification_exceed_fee_text">Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети.</string>
|
||||
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_fee_too_high_accept">Оставить %s XTZ</string>
|
||||
<string name="send_notification_fee_too_high_ignore">Отправить все</string>
|
||||
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
|
||||
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
|
||||
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
|
||||
|
|
@ -528,7 +532,6 @@
|
|||
<string name="swapping_approve_information_title">Подтвердить</string>
|
||||
<string name="swapping_error_wrapper">Ошибка: %s</string>
|
||||
<string name="swapping_from_title">Вы отправляете</string>
|
||||
<string name="swapping_generic_error">Произошла ошибка. Пожалуйста, попробуйте еще раз.</string>
|
||||
<string name="swapping_give_permission">Дать разрешение</string>
|
||||
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
|
||||
<string name="swapping_insufficient_funds">Недостаточно средств</string>
|
||||
|
|
|
|||
|
|
@ -327,7 +327,6 @@
|
|||
<string name="swapping_approve_information_text">批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。</string>
|
||||
<string name="swapping_approve_information_title">批准</string>
|
||||
<string name="swapping_error_wrapper">錯誤: %s</string>
|
||||
<string name="swapping_generic_error">有錯誤。請再試一遍</string>
|
||||
<string name="swapping_give_permission">賦予權限</string>
|
||||
<string name="swapping_high_price_impact_description">在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量</string>
|
||||
<string name="swapping_insufficient_funds">餘額不足</string>
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@
|
|||
<string name="common_transfer">Transfer</string>
|
||||
<string name="common_understand">I understand</string>
|
||||
<string name="common_unreachable">Unreachable</string>
|
||||
<string name="common_unknown_error">There was an error. Please try again.</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
|
|
@ -447,8 +448,9 @@
|
|||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_amount_substract">Subtract from send amount</string>
|
||||
<string name="send_amount_substract_footer">The recipient will receive %s</string>
|
||||
<string name="send_alert_button_request_support">Support</string>
|
||||
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode:%2$s</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
||||
<string name="send_confirm_label">Confirm</string>
|
||||
<string name="send_date_format">%1$s at %2$s</string>
|
||||
<string name="send_destination_hint_address">Address</string>
|
||||
|
|
@ -482,8 +484,10 @@
|
|||
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
|
||||
<string name="send_notification_exceed_fee_text">The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance.</string>
|
||||
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
|
||||
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Fee is increased</string>
|
||||
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
|
||||
<string name="send_notification_fee_too_high_accept">Reduce by %s XTZ</string>
|
||||
<string name="send_notification_fee_too_high_ignore">No, send all</string>
|
||||
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
|
||||
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
|
||||
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
|
||||
|
|
@ -519,6 +523,7 @@
|
|||
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
|
||||
<string name="send_validation_invalid_address">Invalid address</string>
|
||||
<string name="sent_transaction_sent_title">Transaction sent</string>
|
||||
<string name="send_wallet_balance_format">%s (%s)</string>
|
||||
<string name="shop_buy_now">Buy now</string>
|
||||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
|
|
@ -545,7 +550,6 @@
|
|||
<string name="swapping_approve_information_title">Approve</string>
|
||||
<string name="swapping_error_wrapper">Error: %s</string>
|
||||
<string name="swapping_from_title">You swap</string>
|
||||
<string name="swapping_generic_error">There was an error. Please try again.</string>
|
||||
<string name="swapping_give_permission">Give Permission</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_insufficient_funds">Insufficient funds</string>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
|||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionExtras
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
|
|
@ -44,6 +41,21 @@ internal class DefaultTransactionRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
) = withContext(coroutineDispatcherProvider.io) {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
(walletManager as TransactionSender).send(txData, signer)
|
||||
}
|
||||
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
if (memo == null) return null
|
||||
|
|
|
|||
|
|
@ -13,4 +13,9 @@ sealed interface LegacyAction : Action {
|
|||
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
|
||||
*/
|
||||
data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction
|
||||
|
||||
/**
|
||||
* Sending an email to support when sending transaction failed
|
||||
*/
|
||||
data class SendEmailTransactionFailed(val errorMessage: String) : LegacyAction
|
||||
}
|
||||
|
|
@ -13,9 +13,13 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.CommonSigner
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
|
|
@ -17,4 +19,11 @@ interface TransactionRepository {
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): TransactionData?
|
||||
|
||||
suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): SimpleResult
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.domain.transaction.error
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
sealed class SendTransactionError {
|
||||
|
||||
object DemoCardError : SendTransactionError()
|
||||
|
|
@ -8,9 +10,12 @@ sealed class SendTransactionError {
|
|||
|
||||
data class NetworkError(val message: String?) : SendTransactionError()
|
||||
|
||||
data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
|
||||
|
||||
object UserCancelledError : SendTransactionError()
|
||||
data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
|
||||
data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError()
|
||||
|
||||
data class UnknownError(val ex: Exception? = null) : SendTransactionError()
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -23,16 +17,15 @@ import java.math.BigDecimal
|
|||
*/
|
||||
class GetFeeUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
amount: BigDecimal,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<Either<GetFeeError, TransactionFee>> {
|
||||
return flow {
|
||||
try {
|
||||
) = either {
|
||||
catch(
|
||||
block = {
|
||||
val result = requireNotNull(
|
||||
walletManagersFacade.getFee(
|
||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
||||
|
|
@ -43,14 +36,15 @@ class GetFeeUseCase(
|
|||
) { "Fee is null" }
|
||||
|
||||
val maybeFee = when (result) {
|
||||
is Result.Success -> result.data.right()
|
||||
is Result.Failure -> GetFeeError.DataError(result.error).left()
|
||||
is Result.Success -> result.data
|
||||
is Result.Failure -> raise(GetFeeError.DataError(result.error))
|
||||
}
|
||||
emit(maybeFee)
|
||||
} catch (e: Exception) {
|
||||
emit(GetFeeError.DataError(e.cause).left())
|
||||
}
|
||||
}.flowOn(dispatcher.io)
|
||||
maybeFee
|
||||
},
|
||||
catch = {
|
||||
raise(GetFeeError.DataError(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
|
||||
|
|
|
|||
|
|
@ -8,19 +8,23 @@ import com.tangem.blockchain.common.TransactionData
|
|||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.ResultChecker
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.R
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||
|
||||
class SendTransactionUseCase(
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val transactionRepository: TransactionRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
txData: TransactionData,
|
||||
|
|
@ -37,7 +41,7 @@ class SendTransactionUseCase(
|
|||
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
|
||||
SendTransactionError.DemoCardError.left()
|
||||
} else {
|
||||
walletManagersFacade.sendTransaction(
|
||||
transactionRepository.sendTransaction(
|
||||
txData = txData,
|
||||
signer = signer,
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -64,29 +68,28 @@ class SendTransactionUseCase(
|
|||
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
||||
if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message)
|
||||
val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError()
|
||||
when (error) {
|
||||
return when (error) {
|
||||
is BlockchainSdkError.WrappedTangemError -> {
|
||||
val errorByCode = mapErrorByCode(error)
|
||||
if (errorByCode != null) {
|
||||
return errorByCode
|
||||
if (error.code == USER_CANCELLED_ERROR_CODE) {
|
||||
SendTransactionError.UserCancelledError
|
||||
} else {
|
||||
val tangemError = error.tangemError
|
||||
if (tangemError is TangemSdkError) {
|
||||
val resource = tangemError.localizedDescriptionRes()
|
||||
val resId = resource.resId ?: R.string.common_unknown_error
|
||||
val resArgs = resource.args.map { it.value }
|
||||
val textReference = resourceReference(resId, wrappedList(resArgs))
|
||||
SendTransactionError.TangemSdkError(tangemError.code, textReference)
|
||||
} else {
|
||||
SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage)
|
||||
}
|
||||
}
|
||||
val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError()
|
||||
if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError
|
||||
return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause)
|
||||
}
|
||||
else -> {
|
||||
return SendTransactionError.TangemSdkError(error.code, error.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? {
|
||||
return when (error.code) {
|
||||
USER_CANCELLED_ERROR_CODE -> {
|
||||
return SendTransactionError.UserCancelledError
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
SendTransactionError.BlockchainSdkError(
|
||||
code = error.code,
|
||||
message = error.customMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendAlertState {
|
||||
|
||||
abstract val title: TextReference?
|
||||
abstract val message: TextReference
|
||||
open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
open val onConfirmClick: (() -> Unit)? = null
|
||||
|
||||
data class GenericError(
|
||||
override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title),
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
override val message: TextReference = resourceReference(R.string.common_unknown_error)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.send_alert_button_request_support)
|
||||
}
|
||||
|
||||
data class TransactionError(
|
||||
val code: String,
|
||||
val cause: String?,
|
||||
val causeTextReference: TextReference? = null,
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
|
||||
override val message: TextReference = resourceReference(
|
||||
id = R.string.send_alert_transaction_failed_text,
|
||||
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
|
||||
)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.send_alert_button_request_support)
|
||||
}
|
||||
|
||||
data class DemoMode(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
|
||||
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendEvent {
|
||||
|
||||
data class ShowSnackBar(val text: TextReference) : SendEvent()
|
||||
|
||||
data class ShowAlert(val alert: SendAlertState) : SendEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
/**
|
||||
* Factory to produce event state for [SendUiState]
|
||||
*
|
||||
* @param currentStateProvider [Provider] of [SendUiState]
|
||||
* @param clickIntents [SendClickIntents]
|
||||
*/
|
||||
internal class SendEventStateFactory(
|
||||
val currentStateProvider: Provider<SendUiState>,
|
||||
val clickIntents: SendClickIntents,
|
||||
) {
|
||||
private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) }
|
||||
|
||||
fun onConsumeEventState(): SendUiState {
|
||||
return currentStateProvider().copy(event = consumedEvent())
|
||||
}
|
||||
|
||||
fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val event = error?.let {
|
||||
sendTransactionErrorConverter.convert(error)?.let {
|
||||
triggeredEvent<SendEvent>(SendEvent.ShowAlert(it), onConsume)
|
||||
}
|
||||
}
|
||||
return state.copy(
|
||||
event = event ?: consumedEvent(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.GenericError(
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) },
|
||||
),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,16 +57,28 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
|||
sealed class Warning(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
) : SendNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = buttonsState,
|
||||
),
|
||||
) {
|
||||
data class HighFeeError(val amount: String) : Warning(
|
||||
data class HighFeeError(
|
||||
val amount: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
val onDismissClick: () -> Unit,
|
||||
) : Warning(
|
||||
title = resourceReference(R.string.send_notification_high_fee_title),
|
||||
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)),
|
||||
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
|
||||
primaryText = resourceReference(R.string.send_notification_fee_too_high_accept, wrappedList(amount)),
|
||||
onPrimaryClick = onConfirmClick,
|
||||
secondaryText = resourceReference(R.string.send_notification_fee_too_high_ignore),
|
||||
onSecondaryClick = onDismissClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class ExistentialDeposit(val deposit: String) : Warning(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -21,6 +22,7 @@ internal class SendNotificationFactory(
|
|||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val clickIntents: SendClickIntents,
|
||||
) {
|
||||
|
||||
fun create(): Flow<ImmutableList<SendNotification>> = currentStateProvider().currentState
|
||||
|
|
@ -30,19 +32,33 @@ internal class SendNotificationFactory(
|
|||
val feeState = state.feeState ?: return@map persistentListOf()
|
||||
val recipientState = state.recipientState ?: return@map persistentListOf()
|
||||
val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val amountValue = state.amountState?.amountTextField?.value?.toBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
val sendAmount = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue
|
||||
buildList {
|
||||
// errors
|
||||
addExceedBalanceNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addInvalidAmountNotification(feeState.isSubtract, feeState.receivedAmountValue)
|
||||
addMinimumAmountErrorNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addExceedBalanceNotification(feeAmount, sendAmount)
|
||||
addInvalidAmountNotification(feeState.isSubtract, sendAmount)
|
||||
addMinimumAmountErrorNotification(feeAmount, sendAmount)
|
||||
addReserveAmountErrorNotification(recipientState.addressTextField.value)
|
||||
addTransactionLimitErrorNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addTransactionLimitErrorNotification(feeAmount, sendAmount)
|
||||
// warnings
|
||||
addExistentialWarningNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addHighFeeWarningNotification()
|
||||
addExistentialWarningNotification(feeAmount, sendAmount)
|
||||
addHighFeeWarningNotification(amountValue, state.sendState.ignoreAmountReduce)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun dismissHighFeeWarningState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState
|
||||
val updatedNotifications = sendState.notifications.filterNot { it is SendNotification.Warning.HighFeeError }
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
ignoreAmountReduce = true,
|
||||
notifications = updatedNotifications.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addExceedBalanceNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
|
|
@ -173,16 +189,30 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addHighFeeWarningNotification() {
|
||||
// TODO Move Blockchain check elsewhere
|
||||
if (cryptoCurrencyStatusProvider().currency.network.id.value == Blockchain.Tezos.id) {
|
||||
add(SendNotification.Warning.HighFeeError(TEZOS_FEE_THRESHOLD))
|
||||
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
|
||||
sendAmount: BigDecimal,
|
||||
ignoreAmountReduce: Boolean,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id
|
||||
if (!ignoreAmountReduce && sendAmount == balance && isTezos) {
|
||||
add(
|
||||
SendNotification.Warning.HighFeeError(
|
||||
amount = TEZOS_FEE_THRESHOLD.toPlainString(),
|
||||
onConfirmClick = {
|
||||
val reduceTo = sendAmount.minus(TEZOS_FEE_THRESHOLD).toPlainString()
|
||||
clickIntents.onAmountReduceClick(reduceTo)
|
||||
},
|
||||
onDismissClick = clickIntents::onAmountReduceIgnoreClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CARDANO_MINIMUM = "1"
|
||||
private const val DOGECOIN_MINIMUM = "0.01"
|
||||
private const val TEZOS_FEE_THRESHOLD = "0.01"
|
||||
private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01")
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,9 @@ package com.tangem.features.send.impl.presentation.state
|
|||
import androidx.paging.PagingData
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
|
@ -16,22 +15,19 @@ import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
|||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
|
|
@ -40,25 +36,11 @@ internal class SendStateFactory(
|
|||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) }
|
||||
private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) }
|
||||
private val customFeeFieldConverter by lazy {
|
||||
SendFeeCustomFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val feeNotificationFactory = FeeNotificationFactory(
|
||||
coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider,
|
||||
userWalletProvider = userWalletProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
private val amountStateConverter by lazy {
|
||||
SendAmountStateConverter(
|
||||
|
|
@ -92,6 +74,7 @@ internal class SendStateFactory(
|
|||
fun getInitialState(): SendUiState = SendUiState(
|
||||
clickIntents = clickIntents,
|
||||
currentState = MutableStateFlow(SendUiStateType.Amount),
|
||||
event = consumedEvent(),
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState {
|
||||
|
|
@ -218,144 +201,6 @@ internal class SendStateFactory(
|
|||
}
|
||||
//endregion
|
||||
|
||||
//region fee
|
||||
fun onFeeOnLoadingState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Loading
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
notifications = persistentListOf(),
|
||||
isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract)
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeSelectedState(feeType: FeeType): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract)
|
||||
|
||||
val updatedState = feeState.copy(
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(
|
||||
customValues = feeSelectorState.customValues.toMutableList().apply {
|
||||
set(index, feeSelectorState.customValues[index].copy(value = value))
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract)
|
||||
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onSubtractSelect(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, value)
|
||||
val updatedState = feeState.copy(
|
||||
isSubtract = value,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = if (value) {
|
||||
getFormattedValue(receivedAmount)
|
||||
} else {
|
||||
feeState.receivedAmount
|
||||
},
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun FeeSelectorState.isPrimaryButtonEnabled(): Boolean {
|
||||
return when (this) {
|
||||
is FeeSelectorState.Loading -> false
|
||||
is FeeSelectorState.Content -> {
|
||||
val customValue = customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = getFee().amount.value ?: BigDecimal.ZERO
|
||||
|
||||
val isNotEmptyCustom = !customValue.isNullOrZero() && selectedFee == FeeType.CUSTOM
|
||||
val isNotCustom = selectedFee != FeeType.CUSTOM
|
||||
fee < balance && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedValue(value: BigDecimal): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = value,
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region send
|
||||
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
|
|
@ -375,6 +220,7 @@ internal class SendStateFactory(
|
|||
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
|
||||
isSuccess = true,
|
||||
txUrl = txUrl,
|
||||
notifications = persistentListOf(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendTransactionAlertConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<SendTransactionError, SendAlertState?> {
|
||||
override fun convert(value: SendTransactionError): SendAlertState? {
|
||||
return when (value) {
|
||||
SendTransactionError.DemoCardError -> SendAlertState.DemoMode(
|
||||
onConfirmClick = { clickIntents.popBackStack() },
|
||||
)
|
||||
is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError(
|
||||
code = value.code.toString(),
|
||||
cause = null,
|
||||
causeTextReference = value.messageReference,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) },
|
||||
)
|
||||
is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError(
|
||||
code = value.code.toString(),
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
|
||||
)
|
||||
is SendTransactionError.DataError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.NetworkError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.UnknownError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.ex?.localizedMessage,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
|
|
@ -31,6 +32,7 @@ internal data class SendUiState(
|
|||
val sendState: SendStates.SendState = SendStates.SendState(),
|
||||
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val currentState: MutableStateFlow<SendUiStateType>,
|
||||
val event: StateEvent<SendEvent>,
|
||||
)
|
||||
|
||||
@Stable
|
||||
|
|
@ -52,6 +54,7 @@ internal sealed class SendStates {
|
|||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
val amountValue: BigDecimal,
|
||||
) : SendStates()
|
||||
|
||||
/** Recipient state */
|
||||
|
|
@ -72,6 +75,7 @@ internal sealed class SendStates {
|
|||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading,
|
||||
val isSubtract: Boolean = false,
|
||||
val isUserSubtracted: Boolean = false,
|
||||
val fee: Fee? = null,
|
||||
val receivedAmountValue: BigDecimal = BigDecimal.ZERO,
|
||||
val receivedAmount: String = "",
|
||||
|
|
@ -86,6 +90,7 @@ internal sealed class SendStates {
|
|||
val isSuccess: Boolean = false,
|
||||
val transactionDate: Long = 0L,
|
||||
val txUrl: String = "",
|
||||
val ignoreAmountReduce: Boolean = false,
|
||||
val notifications: ImmutableList<SendNotification> = persistentListOf(),
|
||||
) : SendStates()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldCo
|
|||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SendAmountStateConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
|
|
@ -37,6 +38,7 @@ internal class SendAmountStateConverter(
|
|||
amountTextField = sendAmountFieldConverter.convert(Unit),
|
||||
isFiatValue = false,
|
||||
isPrimaryButtonEnabled = false,
|
||||
amountValue = BigDecimal.ZERO,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
|
|
|
|||
|
|
@ -8,14 +8,10 @@ import java.math.BigDecimal
|
|||
/**
|
||||
* Calculate receiving amount when fee is subtracted from sending amount
|
||||
*/
|
||||
internal fun calculateReceiveAmount(uiState: SendUiState, feeAmount: Fee, isSubtract: Boolean): BigDecimal {
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO
|
||||
internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDecimal {
|
||||
val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO
|
||||
val fee = feeAmount.amount.value ?: return BigDecimal.ZERO
|
||||
return if (isSubtract) {
|
||||
amount.toBigDecimal().minus(fee)
|
||||
} else {
|
||||
amount.toBigDecimal()
|
||||
}
|
||||
return amountValue.minus(fee)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -9,7 +10,6 @@ import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
|||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -19,17 +19,28 @@ internal class FeeNotificationFactory(
|
|||
private val clickIntents: SendClickIntents,
|
||||
) {
|
||||
|
||||
operator fun invoke(feeState: SendStates.FeeState): ImmutableList<SendFeeNotification> {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return persistentListOf()
|
||||
val customFee = feeSelectorState.customValues
|
||||
val selectedFee = feeSelectorState.selectedFee
|
||||
|
||||
return buildList {
|
||||
addTooLowNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addFeeCoverageNotification()
|
||||
addExceedsBalanceNotification(feeSelectorState)
|
||||
operator fun invoke(feeState: SendStates.FeeState, amountValue: BigDecimal?): ImmutableList<SendFeeNotification> =
|
||||
buildList {
|
||||
when (val feeSelectorState = feeState.feeSelectorState) {
|
||||
FeeSelectorState.Loading -> Unit
|
||||
FeeSelectorState.Error -> {
|
||||
addFeeUnreachableNotification(feeSelectorState)
|
||||
}
|
||||
is FeeSelectorState.Content -> {
|
||||
val customFee = feeSelectorState.customValues
|
||||
val selectedFee = feeSelectorState.selectedFee
|
||||
addTooLowNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addFeeCoverageNotification(feeState, amountValue)
|
||||
addExceedsBalanceNotification(feeState.fee)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
|
||||
if (feeSelectorState is FeeSelectorState.Error) {
|
||||
add(SendFeeNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addTooLowNotification(
|
||||
|
|
@ -59,25 +70,24 @@ internal class FeeNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addFeeCoverageNotification() {
|
||||
// TODO add fee coverage condition [REDACTED_JIRA]
|
||||
add(SendFeeNotification.Warning.NetworkCoverage)
|
||||
private fun MutableList<SendFeeNotification>.addFeeCoverageNotification(
|
||||
feeState: SendStates.FeeState,
|
||||
amountValue: BigDecimal?,
|
||||
) {
|
||||
val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val value = amountValue ?: BigDecimal.ZERO
|
||||
if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) {
|
||||
add(SendFeeNotification.Warning.NetworkCoverage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(
|
||||
feeSelectorState: FeeSelectorState.Content,
|
||||
) {
|
||||
private fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(fee: Fee?) {
|
||||
val coinCryptoCurrency = coinCryptoCurrencyStatusProvider()
|
||||
val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO
|
||||
val choosableFee = feeSelectorState.fees as? TransactionFee.Choosable
|
||||
val fee = when (feeSelectorState.selectedFee) {
|
||||
FeeType.SLOW -> choosableFee?.minimum?.amount?.value
|
||||
FeeType.MARKET -> feeSelectorState.fees.normal.amount.value
|
||||
FeeType.FAST -> choosableFee?.priority?.amount?.value
|
||||
FeeType.CUSTOM -> feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
} ?: return
|
||||
val feeValue = fee?.amount?.value ?: BigDecimal.ZERO
|
||||
|
||||
if (fee > cryptoAmount) {
|
||||
if (feeValue > cryptoAmount) {
|
||||
add(
|
||||
SendFeeNotification.Error.ExceedsBalance(
|
||||
coinCryptoCurrency.currency.networkIconResId,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ internal sealed class FeeSelectorState {
|
|||
val selectedFee: FeeType = FeeType.MARKET,
|
||||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||
) : FeeSelectorState()
|
||||
|
||||
object Error : FeeSelectorState()
|
||||
}
|
||||
|
||||
enum class FeeType {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory to produce fee state for [SendUiState]
|
||||
*/
|
||||
internal class FeeStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
userWalletProvider: Provider<UserWallet>,
|
||||
) {
|
||||
private val customFeeFieldConverter by lazy {
|
||||
SendFeeCustomFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val feeNotificationFactory = FeeNotificationFactory(
|
||||
coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider,
|
||||
userWalletProvider = userWalletProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
fun onFeeOnLoadingState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = FeeSelectorState.Loading,
|
||||
notifications = persistentListOf(),
|
||||
isPrimaryButtonEnabled = feeState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isSubtract = checkAutoSubtract(state, fee, balance),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(
|
||||
feeState = updatedState,
|
||||
amountValue = state.amountState?.amountValue,
|
||||
),
|
||||
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnErrorState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val updatedState = state.feeState?.copy(
|
||||
feeSelectorState = FeeSelectorState.Error,
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState?.copy(
|
||||
notifications = feeNotificationFactory(
|
||||
feeState = updatedState,
|
||||
amountValue = state.amountState?.amountValue,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeSelectedState(feeType: FeeType): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
val updatedState = feeState.copy(
|
||||
fee = fee,
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isSubtract = checkAutoSubtract(state, fee, balance),
|
||||
)
|
||||
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(
|
||||
feeState = updatedState,
|
||||
amountValue = state.amountState?.amountValue,
|
||||
),
|
||||
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(
|
||||
customValues = feeSelectorState.customValues.toMutableList().apply {
|
||||
set(index, feeSelectorState.customValues[index].copy(value = value))
|
||||
}.toImmutableList(),
|
||||
)
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isSubtract = checkAutoSubtract(state, fee, balance),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(
|
||||
feeState = updatedState,
|
||||
amountValue = state.amountState?.amountValue,
|
||||
),
|
||||
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onSubtractSelect(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
val updatedState = feeState.copy(
|
||||
isSubtract = value,
|
||||
isUserSubtracted = true,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
fee = fee,
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(
|
||||
feeState = updatedState,
|
||||
amountValue = state.amountState?.amountValue,
|
||||
),
|
||||
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun SendStates.FeeState.isPrimaryButtonEnabled(): Boolean {
|
||||
return when (feeSelectorState) {
|
||||
is FeeSelectorState.Content -> {
|
||||
val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = feeSelectorState.getFee().amount.value ?: BigDecimal.ZERO
|
||||
|
||||
val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM
|
||||
val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom
|
||||
val isSubtractRequired = if (fee + receivedAmountValue >= balance) isSubtract else true
|
||||
val isBalanceEnough = fee + receivedAmountValue <= balance
|
||||
isBalanceEnough && isSubtractRequired && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean {
|
||||
val feeState = state.feeState ?: return false
|
||||
val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO
|
||||
val feeAmount = fee.amount.value ?: BigDecimal.ZERO
|
||||
return if (feeState.isUserSubtracted) {
|
||||
feeState.isSubtract
|
||||
} else {
|
||||
amountValue + feeAmount >= balance
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedValue(value: BigDecimal): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = value,
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Immutable
|
||||
sealed class SendFeeNotification(val config: NotificationConfig) {
|
||||
|
||||
sealed class Informational(
|
||||
|
|
@ -29,11 +27,13 @@ sealed class SendFeeNotification(val config: NotificationConfig) {
|
|||
sealed class Warning(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
) : SendFeeNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = buttonsState,
|
||||
),
|
||||
) {
|
||||
data class TooHigh(
|
||||
|
|
@ -47,6 +47,15 @@ sealed class SendFeeNotification(val config: NotificationConfig) {
|
|||
title = resourceReference(id = R.string.send_network_fee_warning_title),
|
||||
subtitle = resourceReference(id = R.string.send_network_fee_warning_content),
|
||||
)
|
||||
|
||||
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
|
||||
title = resourceReference(R.string.send_fee_unreachable_error_title),
|
||||
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.warning_button_refresh),
|
||||
onClick = onRefresh,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Error(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
|
|
@ -41,8 +42,9 @@ internal class SendAmountFieldChangeConverter(
|
|||
trimmedValue
|
||||
}
|
||||
|
||||
val isExceedBalance = cryptoValue.checkExceedBalance(amountState)
|
||||
val isMaxAmount = cryptoValue.checkMaxAmount(amountState)
|
||||
val checkValue = if (amountState.isFiatValue) fiatValue else cryptoValue
|
||||
val isExceedBalance = checkValue.checkExceedBalance(amountState)
|
||||
val isMaxAmount = checkValue.checkMaxAmount(amountState)
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = !isExceedBalance,
|
||||
|
|
@ -51,6 +53,7 @@ internal class SendAmountFieldChangeConverter(
|
|||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
),
|
||||
amountValue = cryptoValue.toBigDecimalOrDefault(),
|
||||
),
|
||||
feeState = feeState.copy(
|
||||
isSubtract = isMaxAmount,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendAlertState
|
||||
import com.tangem.features.send.impl.presentation.state.SendEvent
|
||||
|
||||
@Composable
|
||||
internal fun SendEventEffect(event: StateEvent<SendEvent>, snackbarHostState: SnackbarHostState) {
|
||||
val resources = LocalContext.current.resources
|
||||
var alertConfig by remember { mutableStateOf<SendAlertState?>(value = null) }
|
||||
|
||||
alertConfig?.let {
|
||||
SendAlert(state = it, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
||||
EventEffect(
|
||||
event = event,
|
||||
onTrigger = { value ->
|
||||
when (value) {
|
||||
is SendEvent.ShowSnackBar -> {
|
||||
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
|
||||
}
|
||||
is SendEvent.ShowAlert -> {
|
||||
alertConfig = value.alert
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) {
|
||||
val confirmButton: DialogButton
|
||||
val dismissButton: DialogButton?
|
||||
|
||||
val onActionClick = state.onConfirmClick
|
||||
if (onActionClick != null) {
|
||||
confirmButton = DialogButton(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = {
|
||||
onActionClick()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
dismissButton = DialogButton(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
} else {
|
||||
confirmButton = DialogButton(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
dismissButton = null
|
||||
}
|
||||
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
confirmButton = confirmButton,
|
||||
onDismissDialog = onDismiss,
|
||||
title = state.title?.resolveReference(),
|
||||
dismissButton = dismissButton,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -28,6 +30,7 @@ import com.tangem.features.send.impl.presentation.ui.send.SendContent
|
|||
internal fun SendScreen(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
val isSuccess = uiState.sendState.isSuccess
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
BackHandler { uiState.clickIntents.onBackClick() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -65,6 +68,11 @@ internal fun SendScreen(uiState: SendUiState) {
|
|||
)
|
||||
SendNavigationButtons(uiState)
|
||||
}
|
||||
|
||||
SendEventEffect(
|
||||
event = uiState.event,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.ui.fee
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -16,7 +15,6 @@ import com.tangem.core.ui.components.notifications.Notification
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -36,7 +34,6 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
|
|||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
item(
|
||||
key = FEE_SELECTOR_KEY,
|
||||
|
|
@ -46,13 +43,12 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
|
|||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
notifications(notifications)
|
||||
customFee(
|
||||
feeSendState = feeSendState,
|
||||
cryptoCurrencySymbol = state.cryptoCurrencyStatus.currency.symbol,
|
||||
)
|
||||
notifications(notifications)
|
||||
subtractButton(
|
||||
feeSendState = feeSendState,
|
||||
receivedAmount = state.receivedAmount,
|
||||
isSubtract = state.isSubtract,
|
||||
clickIntents = clickIntents,
|
||||
|
|
@ -69,8 +65,15 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotificat
|
|||
itemContent = {
|
||||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
modifier = modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.animateItemPlacement(),
|
||||
containerColor = when (it) {
|
||||
is SendFeeNotification.Error.ExceedsBalance,
|
||||
is SendFeeNotification.Warning.NetworkFeeUnreachable,
|
||||
-> TangemTheme.colors.background.primary
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (it) {
|
||||
is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
else -> null
|
||||
|
|
@ -102,6 +105,7 @@ internal fun LazyListScope.customFee(
|
|||
customValues = customValues,
|
||||
selectedFee = fee.selectedFee,
|
||||
symbol = cryptoCurrencySymbol,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -110,32 +114,19 @@ internal fun LazyListScope.customFee(
|
|||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.subtractButton(
|
||||
feeSendState: FeeSelectorState,
|
||||
receivedAmount: String,
|
||||
isSubtract: Boolean,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
(feeSendState as? FeeSelectorState.Content)?.let { state ->
|
||||
item {
|
||||
val selectedFeeValue = state.selectedFee
|
||||
val topPadding = if (selectedFeeValue != FeeType.CUSTOM) {
|
||||
TangemTheme.dimens.spacing8
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0
|
||||
}
|
||||
|
||||
SendSpeedSubtract(
|
||||
receivingAmount = receivedAmount,
|
||||
isSubtract = isSubtract,
|
||||
onSelectClick = clickIntents::onSubtractSelect,
|
||||
modifier = modifier
|
||||
.animateItemPlacement()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
item {
|
||||
SendSpeedSubtract(
|
||||
receivingAmount = receivedAmount,
|
||||
isSubtract = isSubtract,
|
||||
onSelectClick = clickIntents::onSubtractSelect,
|
||||
modifier = modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing12)
|
||||
.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.ui.fee
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -13,6 +14,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
|
@ -26,12 +28,19 @@ import com.tangem.core.ui.components.SpacerWMax
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
private val DEFAULT_FEE_OPTIONS = listOf(
|
||||
R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24,
|
||||
R.string.common_fee_selector_option_market to R.drawable.ic_bird_24,
|
||||
R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SendSpeedSelector(
|
||||
|
|
@ -50,10 +59,11 @@ internal fun SendSpeedSelector(
|
|||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
when (state) {
|
||||
FeeSelectorState.Error -> {
|
||||
SendSpeedSelectorItemError()
|
||||
}
|
||||
FeeSelectorState.Loading -> {
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
}
|
||||
is FeeSelectorState.Content -> {
|
||||
when (state.fees) {
|
||||
|
|
@ -84,7 +94,10 @@ internal fun SendSpeedSelector(
|
|||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) },
|
||||
showDivider = state.fees.normal is Fee.Ethereum,
|
||||
)
|
||||
if (state.fees.normal is Fee.Ethereum) {
|
||||
AnimatedVisibility(
|
||||
visible = state.fees.normal is Fee.Ethereum,
|
||||
label = "Custom fee appearance animation",
|
||||
) {
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_custom,
|
||||
iconRes = R.drawable.ic_edit_24,
|
||||
|
|
@ -114,34 +127,52 @@ internal fun SendSpeedSelector(
|
|||
|
||||
@Composable
|
||||
private fun SendSpeedSelectorItemLoading() {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size50,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
SpacerWMax()
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size90,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
repeat(DEFAULT_FEE_OPTIONS.size) {
|
||||
val (text, iconRes) = DEFAULT_FEE_OPTIONS[it]
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
SelectorTitleContent(
|
||||
titleRes = text,
|
||||
iconRes = iconRes,
|
||||
)
|
||||
SpacerWMax()
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size90,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSpeedSelectorItemError() {
|
||||
repeat(DEFAULT_FEE_OPTIONS.size) {
|
||||
val (text, iconRes) = DEFAULT_FEE_OPTIONS[it]
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
SelectorTitleContent(
|
||||
titleRes = text,
|
||||
iconRes = iconRes,
|
||||
)
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,27 +208,11 @@ private fun SendSpeedSelectorItem(
|
|||
.clickable { onSelect() },
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
SelectorTitleContent(
|
||||
titleRes = titleRes,
|
||||
iconRes = iconRes,
|
||||
iconTint = iconTint,
|
||||
textStyle = textStyle,
|
||||
)
|
||||
if (amount != null && symbol != null) {
|
||||
SelectorValueContent(
|
||||
|
|
@ -220,6 +235,37 @@ private fun SendSpeedSelectorItem(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectorTitleContent(
|
||||
@StringRes titleRes: Int,
|
||||
@DrawableRes iconRes: Int,
|
||||
iconTint: Color = TangemTheme.colors.icon.informative,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -197,7 +197,10 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendNotification
|
|||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
containerColor = when (it) {
|
||||
is SendNotification.Warning.HighFeeError -> TangemTheme.colors.background.action
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (it) {
|
||||
is SendNotification.Error -> TangemTheme.colors.icon.warning
|
||||
is SendNotification.Warning -> null
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface SendClickIntents {
|
||||
|
||||
fun popBackStack()
|
||||
|
|
@ -16,6 +17,8 @@ interface SendClickIntents {
|
|||
|
||||
fun onQrCodeScanClick()
|
||||
|
||||
fun onFailedTxEmailClick(errorMessage: String)
|
||||
|
||||
fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
|
||||
// region Amount
|
||||
|
|
@ -33,6 +36,8 @@ interface SendClickIntents {
|
|||
// endregion
|
||||
|
||||
// region Fee
|
||||
fun feeReload()
|
||||
|
||||
fun onFeeSelectorClick(feeType: FeeType)
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String)
|
||||
|
|
@ -50,5 +55,9 @@ interface SendClickIntents {
|
|||
fun showFee()
|
||||
|
||||
fun onExploreClick(txUrl: String)
|
||||
|
||||
fun onAmountReduceClick(reducedAmount: String)
|
||||
|
||||
fun onAmountReduceIgnoreClick()
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -8,8 +8,11 @@ import androidx.paging.PagingData
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
||||
|
|
@ -31,11 +34,8 @@ import com.tangem.features.send.api.navigation.SendRouter
|
|||
import com.tangem.features.send.impl.navigation.InnerSendRouter
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.*
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotificationFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.getFee
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -70,6 +70,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
|
@ -93,19 +94,36 @@ internal class SendViewModel @Inject constructor(
|
|||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
)
|
||||
|
||||
private val feeStateFactory by lazy {
|
||||
FeeStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
userWalletProvider = Provider { userWallet },
|
||||
)
|
||||
}
|
||||
|
||||
private val eventStateFactory = SendEventStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
)
|
||||
|
||||
private val sendNotificationFactory = SendNotificationFactory(
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
clickIntents = this,
|
||||
)
|
||||
|
||||
// todo convert to StateFlow
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
|
|
@ -122,7 +140,7 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
getFee()
|
||||
onFeeStateActive()
|
||||
}
|
||||
|
||||
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
|
||||
|
|
@ -139,7 +157,9 @@ internal class SendViewModel @Inject constructor(
|
|||
getCurrenciesStatusUpdates(owner, wallet)
|
||||
},
|
||||
ifLeft = {
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
return@launch
|
||||
},
|
||||
)
|
||||
|
|
@ -280,37 +300,11 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getFee() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState.currentState
|
||||
.filter { it == SendUiStateType.Fee }
|
||||
.onEach {
|
||||
val amountState = uiState.amountState ?: return@onEach
|
||||
val recipientState = uiState.recipientState ?: return@onEach
|
||||
val amount = amountState.amountTextField.value.toBigDecimal()
|
||||
|
||||
uiState = stateFactory.onFeeOnLoadingState()
|
||||
getFeeUseCase.invoke(
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeFee ->
|
||||
maybeFee.fold(
|
||||
ifRight = {
|
||||
uiState = stateFactory.onFeeOnLoadedState(it)
|
||||
},
|
||||
ifLeft = {
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}.launchIn(viewModelScope)
|
||||
}.saveIn(feeJobHolder)
|
||||
private fun onFeeStateActive() {
|
||||
uiState.currentState
|
||||
.filter { it == SendUiStateType.Fee }
|
||||
.onEach { loadFee() }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun updateNotifications() {
|
||||
|
|
@ -331,6 +325,10 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {
|
||||
reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage))
|
||||
}
|
||||
|
||||
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
// endregion
|
||||
|
|
@ -415,16 +413,41 @@ internal class SendViewModel @Inject constructor(
|
|||
// endregion
|
||||
|
||||
// region fee
|
||||
override fun feeReload() = loadFee()
|
||||
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
uiState = stateFactory.onFeeSelectedState(feeType)
|
||||
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState = stateFactory.onCustomFeeValueChange(index, value)
|
||||
uiState = feeStateFactory.onCustomFeeValueChange(index, value)
|
||||
}
|
||||
|
||||
override fun onSubtractSelect(value: Boolean) {
|
||||
uiState = stateFactory.onSubtractSelect(value)
|
||||
uiState = feeStateFactory.onSubtractSelect(value)
|
||||
}
|
||||
|
||||
private fun loadFee() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val amountState = uiState.amountState ?: return@launch
|
||||
val recipientState = uiState.recipientState ?: return@launch
|
||||
val amount = amountState.amountTextField.value.toBigDecimal()
|
||||
|
||||
uiState = feeStateFactory.onFeeOnLoadingState()
|
||||
getFeeUseCase.invoke(
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
).fold(
|
||||
ifRight = {
|
||||
uiState = feeStateFactory.onFeeOnLoadedState(it)
|
||||
},
|
||||
ifLeft = {
|
||||
uiState = feeStateFactory.onFeeOnErrorState()
|
||||
},
|
||||
)
|
||||
}.saveIn(feeJobHolder)
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -433,8 +456,8 @@ internal class SendViewModel @Inject constructor(
|
|||
val sendState = uiState.sendState
|
||||
if (sendState.isSuccess) popBackStack()
|
||||
|
||||
uiState = stateFactory.getSendingStateUpdate(true)
|
||||
viewModelScope.launch(dispatchers.io) { verifyAndSendTransaction() }
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
verifyAndSendTransaction()
|
||||
}
|
||||
|
||||
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
|
||||
|
|
@ -445,50 +468,66 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl)
|
||||
|
||||
private suspend fun verifyAndSendTransaction() {
|
||||
override fun onAmountReduceClick(reducedAmount: String) {
|
||||
uiState = stateFactory.getOnAmountValueChange(reducedAmount)
|
||||
uiState = sendNotificationFactory.dismissHighFeeWarningState()
|
||||
loadFee()
|
||||
}
|
||||
|
||||
override fun onAmountReduceIgnoreClick() {
|
||||
uiState = sendNotificationFactory.dismissHighFeeWarningState()
|
||||
}
|
||||
|
||||
private fun verifyAndSendTransaction() {
|
||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val fee = feeSelectorState.getFee()
|
||||
val amountValue = uiState.amountState?.amountValue ?: return
|
||||
|
||||
val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency)
|
||||
val amountToSend = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue
|
||||
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
// val transactionErrors = walletManagersFacade.validateTransaction(
|
||||
// amount = amountToSend,
|
||||
// fee = fee.amount,
|
||||
// userWalletId = userWalletId,
|
||||
// network = cryptoCurrency.network,
|
||||
// )
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
createTransactionUseCase(
|
||||
amount = amountToSend.convertToAmount(cryptoCurrency),
|
||||
fee = fee,
|
||||
memo = memo,
|
||||
destination = recipient,
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it)
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
error = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
ifRight = { txData ->
|
||||
sendTransaction(txData)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
createTransactionUseCase(
|
||||
amount = amountToSend,
|
||||
fee = fee,
|
||||
memo = memo,
|
||||
destination = recipient,
|
||||
userWalletId = userWalletId,
|
||||
private suspend fun sendTransaction(txData: TransactionData) {
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it)
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = { txData ->
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
uiState = stateFactory.getSendingStateUpdate(false)
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = {
|
||||
uiState = stateFactory.getTransactionSendState(txData)
|
||||
},
|
||||
ifLeft = { error ->
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = eventStateFactory.getSendTransactionErrorState(
|
||||
error = error,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = stateFactory.getTransactionSendState(txData)
|
||||
},
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
|||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
|
|
@ -118,13 +119,13 @@ class SwapDomainModule {
|
|||
@Singleton
|
||||
fun provideSendTransactionUseCase(
|
||||
@SwapScope isDemoCardUseCase: IsDemoCardUseCase,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
transactionRepository: TransactionRepository,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
transactionRepository = transactionRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -553,7 +553,7 @@ internal class StateBuilder(
|
|||
resourceReference(R.string.warning_express_refresh_required_title)
|
||||
},
|
||||
subtitle = if (dataError is DataError.UnknownError) {
|
||||
resourceReference(R.string.swapping_generic_error)
|
||||
resourceReference(R.string.common_unknown_error)
|
||||
} else {
|
||||
resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString()))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
|
|||
val message = if (state.alert.type == GenericWarningType.NETWORK) {
|
||||
stringResource(id = R.string.disclaimer_error_loading)
|
||||
} else {
|
||||
state.alert.message?.resolveReference() ?: stringResource(id = R.string.swapping_generic_error)
|
||||
state.alert.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error)
|
||||
}
|
||||
SimpleOkDialog(
|
||||
message = message,
|
||||
|
|
@ -338,7 +338,7 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
|
|||
} else {
|
||||
it.resolveReference()
|
||||
}
|
||||
} ?: stringResource(id = R.string.swapping_generic_error)
|
||||
} ?: stringResource(id = R.string.common_unknown_error)
|
||||
RefreshableWaringCard(
|
||||
title = stringResource(id = R.string.common_warning),
|
||||
description = message,
|
||||
|
|
|
|||
|
|
@ -85,9 +85,9 @@ spr-client = "3.6.2"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "release-app_5.5-462"
|
||||
tangemBlockchainSdk = "develop-463"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "release-app_5.5-321"
|
||||
tangemCardSdk = "develop-319"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
# endregion Tangem
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue