From d630852c2939f107db518acaff99343f6c5f0bd0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 23 May 2024 14:02:59 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 2 + .../tap/di/domain/TransactionDomainModule.kt | 18 ++++++ domain/legacy/build.gradle.kts | 1 + .../DefaultWalletManagersFacade.kt | 32 ++++++++++ .../walletmanager/WalletManagersFacade.kt | 13 ++++ .../utils/CryptoCurrencyTypeConverter.kt | 23 +++++++ .../SdkRequirementsConditionConverter.kt | 18 ++++++ domain/tokens/build.gradle.kts | 1 + .../tokens/model/warnings/HederaWarnings.kt | 18 ++++++ .../analytics/TokenScreenAnalyticsEvent.kt | 5 ++ .../tokens/GetCryptoCurrencyActionsUseCase.kt | 30 +++++++-- .../tokens/GetCurrencyWarningsUseCase.kt | 19 ++++++ .../model/ScenarioUnavailabilityReason.kt | 2 + domain/transaction/models/.gitignore | 1 + domain/transaction/models/build.gradle.kts | 4 ++ .../models/AssetRequirementsCondition.kt | 20 ++++++ .../transaction/error/AssociateAssetError.kt | 9 +++ .../usecase/AssociateAssetUseCase.kt | 63 +++++++++++++++++++ features/tokendetails/impl/build.gradle.kts | 1 + ...okenDetailsNotificationsAnalyticsSender.kt | 1 + .../components/TokenDetailsDialogConfig.kt | 17 +++++ .../components/TokenDetailsNotification.kt | 25 +++++++- .../TokenDetailsNotificationConverter.kt | 25 ++++++++ .../state/factory/TokenDetailsStateFactory.kt | 26 +++++++- .../viewmodels/TokenDetailsClickIntents.kt | 2 + .../viewmodels/TokenDetailsViewModel.kt | 36 ++++++++++- .../WalletCurrencyActionsClickIntents.kt | 3 + settings.gradle.kts | 1 + 28 files changed, 406 insertions(+), 10 deletions(-) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt create mode 100644 domain/transaction/models/.gitignore create mode 100644 domain/transaction/models/build.gradle.kts create mode 100644 domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 91c7d4aadb..0067fa4c3a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -185,6 +185,7 @@ internal object TokensDomainModule { @ViewModelScoped fun provideGetCryptoCurrencyActionsUseCase( rampStateManager: RampStateManager, + walletManagersFacade: WalletManagersFacade, marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -193,6 +194,7 @@ internal object TokensDomainModule { ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( rampManager = rampStateManager, + walletManagersFacade = walletManagersFacade, marketCryptoCurrencyRepository = marketCryptoCurrencyRepository, currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 0066c2ae37..bb6a62fc78 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -2,6 +2,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* @@ -40,6 +42,22 @@ internal object TransactionDomainModule { ) } + @Provides + @ViewModelScoped + fun provideAssociateAssetUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + walletManagersFacade: WalletManagersFacade, + currenciesRepository: CurrenciesRepository, + networksRepository: NetworksRepository, + ): AssociateAssetUseCase { + return AssociateAssetUseCase( + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + ) + } + @Provides @ViewModelScoped fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index f13641bdc1..f01a2e6a96 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) /** Tangem libraries */ diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 1f8a476da0..c63725cdc2 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -13,6 +13,7 @@ import com.tangem.blockchain.common.address.EstimationFeeAddressFactory import com.tangem.blockchain.common.pagination.Page import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.common.trustlines.AssetRequirementsManager import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -26,6 +27,7 @@ import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState @@ -57,6 +59,8 @@ class DefaultWalletManagersFacade( private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() } private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) } private val sdkPageConverter by lazy { SdkPageConverter() } + private val cryptoCurrencyTypeConverter by lazy { CryptoCurrencyTypeConverter() } + private val requirementsConditionConverter by lazy { SdkRequirementsConditionConverter() } private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory() } override suspend fun update( @@ -575,6 +579,34 @@ class DefaultWalletManagersFacade( ) } + override suspend fun getAssetRequirements( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): AssetRequirementsCondition? { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) + if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null + + val condition = walletManager.requirementsCondition(currencyType) ?: return null + return requirementsConditionConverter.convert(condition) + } + + override suspend fun associateAsset( + userWalletId: UserWalletId, + currency: CryptoCurrency, + signer: CommonSigner, + ): SimpleResult { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) + + if (walletManager !is AssetRequirementsManager) { + return SimpleResult.Failure( + BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"), + ) + } + return walletManager.fulfillRequirements(currencyType, signer) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 4636317765..5c80c0db3d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -13,6 +13,7 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState @@ -240,4 +241,16 @@ interface WalletManagersFacade { decimals: Int, id: String? = null, ): BigDecimal + + /** + * Get requirements for asset(currency) + * @return null if there's no requirement, otherwise [AssetRequirementsCondition]. + */ + suspend fun getAssetRequirements(userWalletId: UserWalletId, currency: CryptoCurrency): AssetRequirementsCondition? + + suspend fun associateAsset( + userWalletId: UserWalletId, + currency: CryptoCurrency, + signer: CommonSigner, + ): SimpleResult } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt new file mode 100644 index 0000000000..53d7f303c5 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.CryptoCurrencyType +import com.tangem.blockchain.common.Token +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.converter.Converter + +internal class CryptoCurrencyTypeConverter : Converter { + override fun convert(value: CryptoCurrency): CryptoCurrencyType { + return when (value) { + is CryptoCurrency.Coin -> CryptoCurrencyType.Coin + is CryptoCurrency.Token -> CryptoCurrencyType.Token( + info = Token( + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + id = value.id.rawCurrencyId, + ), + ) + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt new file mode 100644 index 0000000000..7c84120470 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.domain.transaction.models.AssetRequirementsCondition +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.trustlines.AssetRequirementsCondition as SdkRequirementsCondition + +internal class SdkRequirementsConditionConverter : Converter { + override fun convert(value: SdkRequirementsCondition): AssetRequirementsCondition { + return when (value) { + SdkRequirementsCondition.PaidTransaction -> AssetRequirementsCondition.PaidTransaction + is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee( + feeAmount = requireNotNull(value.feeAmount.value), + feeCurrencySymbol = value.feeAmount.currencySymbol, + decimals = value.feeAmount.decimals, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 501cf70dd8..de3ef835c5 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) implementation(projects.domain.settings) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt new file mode 100644 index 0000000000..e251aae9a0 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.tokens.model.warnings + +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +sealed class HederaWarnings : CryptoCurrencyWarning() { + + abstract val currency: CryptoCurrency + + data class AssociateWarning(override val currency: CryptoCurrency) : HederaWarnings() + + data class AssociateWarningWithFee( + override val currency: CryptoCurrency, + val fee: BigDecimal, + val feeCurrencySymbol: String, + val feeCurrencyDecimals: Int, + ) : HederaWarnings() +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt index ed01f84f51..171f60eb9e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt @@ -72,4 +72,9 @@ sealed class TokenScreenAnalyticsEvent( event = "Token Bought", params = mapOf("Token" to token), ) + + class Associate(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent( + event = "Button - Token Trustline", + params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index d0155c6c62..e521d035af 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.* @Suppress("LongParameterList") class GetCryptoCurrencyActionsUseCase( private val rampManager: RampStateManager, + private val walletManagersFacade: WalletManagersFacade, private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, @@ -30,7 +32,10 @@ class GetCryptoCurrencyActionsUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { + suspend operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -38,7 +43,7 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id - + val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency) return flow { val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) @@ -53,6 +58,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, coinStatus = maybeCoinStatus.getOrNull(), cryptoCurrencyStatus = cryptoCurrencyStatus, + needAssociateAsset = requirements != null, ) } @@ -64,6 +70,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): TokenActionsState { return TokenActionsState( walletId = userWallet.walletId, @@ -72,6 +79,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, coinStatus = coinStatus, cryptoCurrencyStatus = cryptoCurrencyStatus, + needAssociateAsset = needAssociateAsset, ), ) } @@ -85,13 +93,14 @@ class GetCryptoCurrencyActionsUseCase( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): List { val cryptoCurrency = cryptoCurrencyStatus.currency if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) } if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) { - return getActionsForUnreachableCurrency(cryptoCurrencyStatus) + return getActionsForUnreachableCurrency(cryptoCurrencyStatus, needAssociateAsset) } val activeList = mutableListOf() @@ -104,7 +113,12 @@ class GetCryptoCurrencyActionsUseCase( // receive if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None)) + val scenario = if (needAssociateAsset) { + ScenarioUnavailabilityReason.UnassociatedAsset + } else { + ScenarioUnavailabilityReason.None + } + activeList.add(TokenActionsState.ActionState.Receive(scenario)) } // send @@ -190,6 +204,7 @@ class GetCryptoCurrencyActionsUseCase( private fun getActionsForUnreachableCurrency( cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): List { val actionsList = mutableListOf() @@ -212,7 +227,12 @@ class GetCryptoCurrencyActionsUseCase( actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - actionsList.add(TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None)) + val scenario = if (needAssociateAsset) { + ScenarioUnavailabilityReason.UnassociatedAsset + } else { + ScenarioUnavailabilityReason.None + } + actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return actionsList diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 5036e8f341..ba46c3aca0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -6,8 +6,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.* +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.api.SwapRepository @@ -76,6 +78,7 @@ class GetCurrencyWarningsUseCase( getNetworkUnavailableWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus), getBeaconChainShutdownWarning(currency.network.id), + getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), ) }.flowOn(dispatchers.io) } @@ -267,6 +270,22 @@ class GetCurrencyWarningsUseCase( return if (BlockchainUtils.isBeaconChain(networkId.value)) CryptoCurrencyWarning.BeaconChainShutdown else null } + private suspend fun getAssetRequirementsWarning( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): CryptoCurrencyWarning? { + return when (val requirements = walletManagersFacade.getAssetRequirements(userWalletId, currency)) { + is AssetRequirementsCondition.PaidTransaction -> HederaWarnings.AssociateWarning(currency = currency) + is AssetRequirementsCondition.PaidTransactionWithFee -> HederaWarnings.AssociateWarningWithFee( + currency = currency, + fee = requirements.feeAmount, + feeCurrencySymbol = requirements.feeCurrencySymbol, + feeCurrencyDecimals = requirements.decimals, + ) + null -> null + } + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index e241178026..54db16bf92 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -21,6 +21,8 @@ sealed class ScenarioUnavailabilityReason { data object Unreachable : ScenarioUnavailabilityReason() + data object UnassociatedAsset : ScenarioUnavailabilityReason() + enum class WithdrawalScenario { SELL, SEND } diff --git a/domain/transaction/models/.gitignore b/domain/transaction/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/transaction/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/transaction/models/build.gradle.kts b/domain/transaction/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/transaction/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt new file mode 100644 index 0000000000..3503f354df --- /dev/null +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.transaction.models + +import java.math.BigDecimal + +sealed class AssetRequirementsCondition { + + /** + * The exact value of the fee for this type of condition is unknown. + */ + data object PaidTransaction : AssetRequirementsCondition() + + /** + * The exact value of the fee for this type of condition is stored in `feeAmount`. + */ + data class PaidTransactionWithFee( + val feeAmount: BigDecimal, + val feeCurrencySymbol: String, + val decimals: Int, + ) : AssetRequirementsCondition() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt new file mode 100644 index 0000000000..fc92dd8cea --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.transaction.error + +import com.tangem.domain.tokens.model.CryptoCurrency + +sealed class AssociateAssetError { + data class NotEnoughBalance(val feeCurrency: CryptoCurrency) : AssociateAssetError() + + data class DataError(val message: String?) : AssociateAssetError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt new file mode 100644 index 0000000000..3187c90352 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.transaction.error.AssociateAssetError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.isNullOrZero + +class AssociateAssetUseCase( + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Either { + return either { + val networkCoin = currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = currency.network.id, + derivationPath = currency.network.derivationPath, + ) + if (isBalanceZero(userWalletId, networkCoin)) { + raise(AssociateAssetError.NotEnoughBalance(networkCoin)) + } + val signer = cardSdkConfigRepository.getCommonSigner(cardId = null) + + catch( + block = { + when (val result = walletManagersFacade.associateAsset(userWalletId, currency, signer)) { + is SimpleResult.Failure -> raise(AssociateAssetError.DataError(result.error.message)) + SimpleResult.Success -> Unit + } + }, + catch = { error -> AssociateAssetError.DataError(error.message) }, + ) + } + } + + private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { + val networkStatus = networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = setOf(currency.network), + ).find { it.network == currency.network } + val networkCoinAmountStatus = (networkStatus?.value as? NetworkStatus.Verified) + ?.amounts + ?.get(currency.id) + return networkCoinAmountStatus is CryptoCurrencyAmountStatus.Loaded && + networkCoinAmountStatus.value.isNullOrZero() + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 0ec7c8726f..5c435284d7 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.transaction) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index b458c3f622..998dbf0aff 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -42,6 +42,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.SwapPromo, is TokenDetailsNotification.NetworkShutdown, + is TokenDetailsNotification.HederaAssociateWarning, -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt index f0bccdec33..dfa224df70 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt @@ -94,5 +94,22 @@ internal data class TokenDetailsDialogConfig( onClick = onConfirmClick, ) } + + data class ErrorDialogConfig( + val text: TextReference, + val onConfirmClick: () -> Unit, + ) : DialogContentConfig() { + + override val title = null + + override val message: TextReference = text + + override val cancelButtonConfig = null + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_ok), + onClick = onConfirmClick, + ) + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 853191b4a2..c6314d4e3f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -117,8 +117,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ), iconResId = currency.networkIconResId, - buttonsState = - NotificationConfig.ButtonsState.SecondaryButtonConfig( + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference( id = R.string.common_buy_currency, formatArgs = wrappedList( @@ -176,4 +175,26 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { title = title, subtitle = subtitle, ) + + data class HederaAssociateWarning( + private val currency: CryptoCurrency, + private val fee: String?, + private val feeCurrencySymbol: String?, + private val onAssociateClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.warning_hedera_missing_token_association_title), + subtitle = if (fee != null && feeCurrencySymbol != null) { + resourceReference( + id = R.string.warning_hedera_missing_token_association_message, + formatArgs = wrappedList(fee, feeCurrencySymbol), + ) + } else { + resourceReference(R.string.warning_hedera_missing_token_association_message_brief) + }, + iconResId = currency.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_hedera_missing_token_association_button_title), + onClick = onAssociateClick, + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 8f368bb1b3..33c5b85d67 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.* @@ -30,6 +31,13 @@ internal class TokenDetailsNotificationConverter( return newNotifications.toImmutableList() } + fun removeHederaAssociateWarning(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is HederaAssociateWarning } + return newNotifications.toImmutableList() + } + + @Suppress("LongMethod") private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification { return when (warning) { is CryptoCurrencyWarning.BalanceNotEnoughForFee -> NetworkFeeWithBuyButton( @@ -86,6 +94,23 @@ internal class TokenDetailsNotificationConverter( title = resourceReference(R.string.warning_beacon_chain_retirement_title), subtitle = resourceReference(R.string.warning_beacon_chain_retirement_content), ) + + is HederaWarnings.AssociateWarning -> HederaAssociateWarning( + currency = warning.currency, + fee = null, + feeCurrencySymbol = null, + onAssociateClick = clickIntents::onAssociateClick, + ) + is HederaWarnings.AssociateWarningWithFee -> HederaAssociateWarning( + currency = warning.currency, + fee = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = warning.fee, + cryptoCurrency = "", + decimals = warning.feeCurrencyDecimals, + ), + feeCurrencySymbol = warning.feeCurrencySymbol, + onAssociateClick = clickIntents::onAssociateClick, + ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index db67d88385..1cd62078a1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -10,7 +10,9 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver @@ -35,7 +37,6 @@ import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import java.lang.IllegalArgumentException @Suppress("TooManyFunctions", "LargeClass") internal class TokenDetailsStateFactory( @@ -187,6 +188,19 @@ internal class TokenDetailsStateFactory( ) } + fun getStateWithErrorDialog(text: TextReference): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ErrorDialogConfig( + text = text, + onConfirmClick = clickIntents::onDismissDialog, + ), + ), + ) + } + fun getRefreshingState(): TokenDetailsState { return refreshStateConverter.convert(true) } @@ -256,6 +270,11 @@ internal class TokenDetailsStateFactory( return state.copy(notifications = notificationConverter.removeRentInfo(state)) } + fun getStateWithRemovedHederaAssociateNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeHederaAssociateWarning(state)) + } + fun getStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( @@ -379,6 +398,9 @@ internal class TokenDetailsStateFactory( id = R.string.token_button_unavailability_generic_description, ) } + ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( + id = R.string.warning_receive_blocked_hedera_token_association_required_message, + ) ScenarioUnavailabilityReason.None -> { throw IllegalArgumentException("The unavailability reason must be other than None") } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 54c81902ca..751b8dcd92 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -53,4 +53,6 @@ interface TokenDetailsClickIntents { fun onGenerateExtendedKey() fun onCopyAddress(): TextReference? + + fun onAssociateClick() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f40778e2eb..814cc1bcdd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.extensions.wrappedList import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -38,6 +39,8 @@ import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.error.AssociateAssetError +import com.tangem.domain.transaction.usecase.AssociateAssetUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase @@ -99,6 +102,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val quotesRepository: QuotesRepository, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val isDemoCardUseCase: IsDemoCardUseCase, + private val associateAssetUseCase: AssociateAssetUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, private val hapticManager: HapticManager, @@ -216,7 +220,7 @@ internal class TokenDetailsViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun updateButtons(currencyStatus: CryptoCurrencyStatus) { + private suspend fun updateButtons(currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = currencyStatus, @@ -709,6 +713,36 @@ internal class TokenDetailsViewModel @Inject constructor( return resourceReference(R.string.wallet_notification_address_copied) } + override fun onAssociateClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.Associate( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + viewModelScope.launch(dispatchers.io) { + associateAssetUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifLeft = { e -> + when (e) { + is AssociateAssetError.NotEnoughBalance -> { + uiState = stateFactory.getStateWithErrorDialog( + resourceReference( + id = R.string.warning_hedera_token_association_not_enough_hbar_message, + formatArgs = wrappedList(e.feeCurrency.symbol), + ), + ) + } + is AssociateAssetError.DataError -> Timber.e(e.message) + } + }, + ifRight = { uiState = stateFactory.getStateWithRemovedHederaAssociateNotification() }, + ) + } + } + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index e472dd6eac..53f2f86e93 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -492,6 +492,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( id = R.string.token_button_unavailability_generic_description, ) } + ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( + id = R.string.warning_receive_blocked_hedera_token_association_required_message, + ) ScenarioUnavailabilityReason.None -> { throw IllegalArgumentException("The unavailability reason must be other than None") } diff --git a/settings.gradle.kts b/settings.gradle.kts index b714ecb4b4..eeeaa4c19a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -180,6 +180,7 @@ include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") +include(":domain:transaction:models") include(":domain:analytics") include(":domain:visa") include(":domain:onboarding")