From 20eb597613325c26ff00d7e35729ee92fb585d4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 20:29:25 +0500 Subject: [PATCH 01/90] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 +- .../datasource/api/stakekit/StakeKitApi.kt | 2 +- .../local/token/DefaultStakingBalanceStore.kt | 46 ++++++------ .../local/token/StakingBalanceStore.kt | 13 ++-- .../data/staking/DefaultStakingRepository.kt | 75 ++++++++++--------- .../converters/YieldBalanceListConverter.kt | 4 +- .../DefaultWalletManagersFacade.kt | 11 +-- .../walletmanager/WalletManagersFacade.kt | 6 +- .../FetchStakingYieldBalanceUseCase.kt | 6 +- .../staking/GetStakingYieldBalanceUseCase.kt | 6 +- .../staking/repositories/StakingRepository.kt | 15 ++-- .../tokens/FetchCardTokenListUseCase.kt | 10 ++- .../tokens/FetchCurrencyStatusUseCase.kt | 19 ++++- .../CurrenciesStatusesLceOperations.kt | 7 +- .../CurrenciesStatusesOperations.kt | 50 +++++-------- .../repository/MockStakingRepository.kt | 15 ++-- .../viewmodel/StakingViewModel.kt | 70 ++++++++++++++--- .../WalletCurrencyActionsClickIntents.kt | 17 ++--- 18 files changed, 217 insertions(+), 158 deletions(-) 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 24c222149b..6e5da90b47 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 @@ -158,8 +158,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): FetchCurrencyStatusUseCase { - return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository) + return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 2d5bd551ac..1c9b1ab3e4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -34,7 +34,7 @@ interface StakeKitApi { @POST("yields/balances") suspend fun getMultipleYieldBalances( @Body body: List, - ): ApiResponse> + ): ApiResponse> @POST("yields/{integrationId}/balances") suspend fun getSingleYieldBalance( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index c88970b238..a80f1b8e05 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -3,49 +3,53 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock internal class DefaultStakingBalanceStore( - private val dataStore: StringKeyDataStore>, + private val dataStore: StringKeyDataStore>, ) : StakingBalanceStore { - override fun get(): Flow> { - return dataStore.get(STAKING_BALANCE_KEY) + private val mutex = Mutex() + + override fun get(userWalletId: UserWalletId): Flow> { + return dataStore.get(userWalletId.stringValue) } - override suspend fun getSyncOrNull(): List? { - return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? { + return dataStore.getSyncOrNull(userWalletId.stringValue) } - override suspend fun store(items: List) { - return dataStore.store(STAKING_BALANCE_KEY, items) + override suspend fun store(userWalletId: UserWalletId, items: Set) { + mutex.withLock { + dataStore.store(userWalletId.stringValue, items) + } } - override fun get(integrationId: String): Flow> { - return dataStore.get(STAKING_BALANCE_KEY) + override fun get(userWalletId: UserWalletId, integrationId: String): Flow> { + return dataStore.get(userWalletId.stringValue) .map { balances -> balances.filter { it.integrationId == integrationId } .flatMap { it.balances } } } - override suspend fun getSyncOrNull(integrationId: String): List? { - return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? { + return dataStore.getSyncOrNull(userWalletId.stringValue) ?.firstOrNull { it.integrationId == integrationId }?.balances } - override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) { - val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY) - ?.toMutableList() - ?.addOrReplace(item) { item.integrationId == integrationId } - ?: listOf(item) + override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) { + mutex.withLock { + val balances = dataStore.getSyncOrNull(userWalletId.stringValue) + ?.addOrReplace(item) { it.integrationId == integrationId } + ?: setOf(item) - return dataStore.store(STAKING_BALANCE_KEY, balances) - } - - companion object { - private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY" + dataStore.store(userWalletId.stringValue, balances) + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index 0a9ea06c9e..1dc58ba38d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -2,19 +2,20 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow interface StakingBalanceStore { - fun get(): Flow> + fun get(userWalletId: UserWalletId): Flow> - suspend fun getSyncOrNull(): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? - suspend fun store(items: List) + suspend fun store(userWalletId: UserWalletId, items: Set) - fun get(integrationId: String): Flow> + fun get(userWalletId: UserWalletId, integrationId: String): Flow> - suspend fun getSyncOrNull(integrationId: String): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? - suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) + suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 9ce502abe3..541762c34d 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -45,7 +45,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -261,25 +260,27 @@ internal class DefaultStakingRepository( override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext - val cryptoCurrency = address.cryptoCurrency val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() + cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val requestBody = getBalanceRequestData(address.address, integrationId) + val requestBody = getBalanceRequestData(address, integrationId) val result = stakeKitApi.getSingleYieldBalance( integrationId = requestBody.integrationId, body = requestBody, ).getOrThrow() stakingBalanceStore.store( + userWalletId, requestBody.integrationId, YieldBalanceWrapperDTO( balances = result, @@ -292,15 +293,15 @@ internal class DefaultStakingRepository( override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalance.Empty) } else { launch(dispatchers.io) { - val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: error("Could not get integrationId") - stakingBalanceStore.get(integrationId) + stakingBalanceStore.get(userWalletId, integrationId) .collectLatest { send( yieldBalanceConverter.convert( @@ -316,7 +317,7 @@ internal class DefaultStakingRepository( withContext(dispatchers.io) { fetchSingleYieldBalance( userWalletId, - address, + cryptoCurrency, ) } } @@ -324,16 +325,18 @@ internal class DefaultStakingRepository( override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): YieldBalance = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) { YieldBalance.Empty } else { - fetchSingleYieldBalance(userWalletId, address) + fetchSingleYieldBalance(userWalletId, cryptoCurrency) - val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: error("Could not get integrationId") - val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error + val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) + ?: return@withContext YieldBalance.Error + yieldBalanceConverter.convert( YieldBalanceConverter.Data( balance = result, @@ -345,7 +348,7 @@ internal class DefaultStakingRepository( override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext @@ -357,23 +360,23 @@ internal class DefaultStakingRepository( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val result = stakeKitApi.getMultipleYieldBalances( - addresses - .mapNotNull { networkAddress -> - val cryptoCurrency = networkAddress.cryptoCurrency - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] + val availableCurrencies = cryptoCurrencies + .mapNotNull { currency -> + val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network) + val integrationId = integrationIdMap[currency.id.getIntegrationKey()] - if (integrationId != null) { - networkAddress.address to integrationId - } else { - null - } + if (integrationId != null && address != null) { + address to integrationId + } else { + null } - .distinct() - .map { getBalanceRequestData(it.first, it.second) }, - ).getOrThrow() + } + .distinct() + .map { getBalanceRequestData(it.first, it.second) } + .ifEmpty { return@invokeOnExpire } + val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() - stakingBalanceStore.store(result) + stakingBalanceStore.store(userWalletId, result) }, ) } finally { @@ -385,20 +388,20 @@ internal class DefaultStakingRepository( override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { launch(dispatchers.io) { - stakingBalanceStore.get() + stakingBalanceStore.get(userWalletId) .collectLatest { send(yieldBalanceListConverter.convert(it)) } } withContext(dispatchers.io) { fetchMultiYieldBalance( userWalletId, - addresses, + cryptoCurrencies, ) } } @@ -406,14 +409,14 @@ internal class DefaultStakingRepository( override fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow = lceFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { launch(dispatchers.io) { combine( - stakingBalanceStore.get(), + stakingBalanceStore.get(userWalletId), isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } }, ) { result, isFetching -> val balances = yieldBalanceListConverter.convert(result) @@ -422,7 +425,7 @@ internal class DefaultStakingRepository( } withContext(dispatchers.io) { catch( - block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) }, + block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) }, catch = { raise(it) }, ) } @@ -431,13 +434,13 @@ internal class DefaultStakingRepository( override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) { YieldBalanceList.Empty } else { - fetchMultiYieldBalance(userWalletId, addresses) - val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error + fetchMultiYieldBalance(userWalletId, cryptoCurrencies) + val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error yieldBalanceListConverter.convert(result) } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt index 0476368b30..007c73bfe1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.utils.converter.Converter -internal class YieldBalanceListConverter : Converter, YieldBalanceList> { +internal class YieldBalanceListConverter : Converter, YieldBalanceList> { internal val converter by lazy(LazyThreadSafetyMode.NONE) { YieldBalanceConverter() } - override fun convert(value: List): YieldBalanceList { + override fun convert(value: Set): YieldBalanceList { return if (value.isEmpty()) { YieldBalanceList.Empty } else { 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 68f4060640..7b187af646 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 @@ -376,15 +376,12 @@ class DefaultWalletManagersFacade( return walletManagersStore.getAllSync(userWalletId) } - @Deprecated( - "Use NetworkAddress from CryptoCurrencyStatus", - ReplaceWith("cryptoCurrencyStatus.value.networkAddress"), - ) - override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ - return getAddresses(userWalletId, network).sortedBy { it.type } + override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? { + return getAddresses(userWalletId, network) + .firstOrNull { it.type == AddressType.Default } + ?.value } - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
{ val manager = getOrCreateWalletManager( userWalletId = userWalletId, 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 4ecfbe3805..e55a359854 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 @@ -117,20 +117,18 @@ interface WalletManagersFacade { suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List /** - * Returns ordered list of addresses for selected wallet for given currency + * Returns default network address for selected wallet in given network * * @param userWalletId selected wallet id * @param network network of currency */ - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") - suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
+ suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? /** Returns list of all addresses for all currencies in selected wallet * * @param userWalletId selected wallet id * @param network required to create wallet manager */ - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
/** diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 333762d0e1..e09a9309c7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.raise.either import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( @@ -16,7 +16,7 @@ class FetchStakingYieldBalanceUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean = false, ): Either { return either { @@ -24,7 +24,7 @@ class FetchStakingYieldBalanceUseCase( block = { stakingRepository.fetchSingleYieldBalance( userWalletId = userWalletId, - address = address, + cryptoCurrency = cryptoCurrency, refresh = refresh, ) }, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt index 5c5528b931..16d17d97ad 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -20,11 +20,11 @@ class GetStakingYieldBalanceUseCase( operator fun invoke( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): EitherFlow { return stakingRepository.getSingleYieldBalanceFlow( userWalletId = userWalletId, - address = address, + cryptoCurrency = cryptoCurrency, ).map> { it.right() } .catch { emit(stakingErrorResolver.resolve(it).left()) } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 5b6604423e..8a48c154a9 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -15,7 +15,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -38,33 +37,33 @@ interface StakingRepository { suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean = false, ) - fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, address: CryptoCurrencyAddress): Flow + fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow - suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, address: CryptoCurrencyAddress): YieldBalance + suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean = false, ) fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index ca9266b2d1..e40ecd7e98 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -44,6 +44,7 @@ class FetchCardTokenListUseCase( val yieldBalances = async { fetchYieldBalances( userWalletId = userWalletId, + currencies = currencies, refresh = refresh, ) } @@ -77,10 +78,13 @@ class FetchCardTokenListUseCase( ) } - private suspend fun fetchYieldBalances(userWalletId: UserWalletId, refresh: Boolean) { - val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) + private suspend fun fetchYieldBalances( + userWalletId: UserWalletId, + currencies: List, + refresh: Boolean, + ) { catch( - block = { stakingRepository.fetchMultiYieldBalance(userWalletId, networkAddresses, refresh) }, + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, catch = { /* Ignore error */ }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 8fbd29d4f5..7b631669e7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -29,6 +30,7 @@ class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, + private val stakingRepository: StakingRepository, ) { /** @@ -80,8 +82,11 @@ class FetchCurrencyStatusUseCase( val fetchQuote = async { fetchQuote(currency.id, refresh) } + val fetchStakingBalance = async { + fetchStakingBalance(userWalletId, currency, refresh) + } - awaitAll(fetchStatus, fetchQuote) + awaitAll(fetchStatus, fetchQuote, fetchStakingBalance) } private suspend fun Raise.getCurrency( @@ -122,4 +127,16 @@ class FetchCurrencyStatusUseCase( raise(CurrencyStatusError.DataError(it)) } } + + private suspend fun Raise.fetchStakingBalance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + refresh: Boolean, + ) { + catch( + block = { stakingRepository.fetchSingleYieldBalance(userWalletId, cryptoCurrency, refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 18f07bea3e..59461356b9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -69,11 +69,10 @@ internal class CurrenciesStatusesLceOperations( val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - val addresses = networksRepository.getNetworkAddresses(userWalletId) combine( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), - getYieldBalances(userWalletId, addresses), + getYieldBalances(userWalletId, nonEmptyCurrencies), ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> val statuses = createCurrenciesStatuses( currencies = nonEmptyCurrencies, @@ -196,11 +195,11 @@ internal class CurrenciesStatusesLceOperations( private fun getYieldBalances( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow { return stakingRepository.getMultiYieldBalanceLce( userWalletId = userWalletId, - addresses = addresses, + cryptoCurrencies = cryptoCurrencies, ).map { maybeBalances -> maybeBalances.mapError { TokenListError.DataError(it) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index e82bad56f2..55ba05a3d3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -11,7 +11,6 @@ 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.wallets.models.UserWalletId -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* // FIXME: Refactor - [REDACTED_JIRA] @@ -35,7 +34,7 @@ internal class CurrenciesStatusesOperations( val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() val networkStatuses = networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() - val yieldBalances = getYieldBalancesSync() + val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies) return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances) }, @@ -147,7 +146,7 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - getYieldBalances(), + getYieldBalances(nonEmptyCurrencies), ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances) } @@ -385,25 +384,23 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } - @OptIn(ExperimentalCoroutinesApi::class) - private fun getYieldBalances(): EitherFlow { - return networksRepository.getNetworkAddressesFlow(userWalletId).flatMapLatest { addresses -> - stakingRepository.getMultiYieldBalanceFlow( - userWalletId = userWalletId, - addresses = addresses, - ).map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } - } + private fun getYieldBalances(cryptoCurrencies: List): Flow> { + return stakingRepository.getMultiYieldBalanceFlow( + userWalletId = userWalletId, + cryptoCurrencies = cryptoCurrencies, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } } - private suspend fun getYieldBalancesSync(): Either { + private suspend fun getYieldBalancesSync( + cryptoCurrencies: List, + ): Either { return catch( block = { - val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) stakingRepository.getMultiYieldBalanceSync( userWalletId, - networkAddresses, + cryptoCurrencies, ).right() }, catch = { @@ -417,10 +414,9 @@ internal class CurrenciesStatusesOperations( ): Either { return catch( block = { - val address = networksRepository.getNetworkAddress(userWalletId, cryptoCurrency) stakingRepository.getSingleYieldBalanceSync( userWalletId, - address, + cryptoCurrency, ).right() }, catch = { @@ -429,19 +425,13 @@ internal class CurrenciesStatusesOperations( ) } - @OptIn(ExperimentalCoroutinesApi::class) private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow { - return networksRepository.getNetworkAddressFlow( - userWalletId, - cryptoCurrency, - ).flatMapLatest { address -> - stakingRepository.getSingleYieldBalanceFlow( - userWalletId = userWalletId, - address = address, - ).map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } - } + return stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } } private fun getIds( diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 33c6654e89..a19fea75b9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -16,7 +16,6 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -118,7 +117,7 @@ class MockStakingRepository : StakingRepository { override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean, ) { /* no-op */ @@ -126,19 +125,19 @@ class MockStakingRepository : StakingRepository { override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { send(YieldBalance.Error) } override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): YieldBalance = YieldBalance.Error override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean, ) { /* no-op */ @@ -146,7 +145,7 @@ class MockStakingRepository : StakingRepository { override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow = channelFlow { send( YieldBalanceList.Data( @@ -157,7 +156,7 @@ class MockStakingRepository : StakingRepository { override fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow = lceFlow { send( YieldBalanceList.Data( @@ -168,7 +167,7 @@ class MockStakingRepository : StakingRepository { override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList = YieldBalanceList.Data( balances = listOf(YieldBalance.Error), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 33f5b8df76..09221fb03d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -25,16 +25,19 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -54,8 +57,8 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -80,12 +83,17 @@ internal class StakingViewModel @Inject constructor( private val submitHashUseCase: SubmitHashUseCase, private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val getAllowanceUseCase: GetAllowanceUseCase, private val getFeeUseCase: GetFeeUseCase, private val isApproveNeededUseCase: IsApproveNeededUseCase, private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, + @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -575,7 +583,7 @@ internal class StakingViewModel @Inject constructor( }, ifRight = { txHash -> submitHash(transactionId, txHash) - updateStakeBalance() + scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( txHash = txHash, networkId = cryptoCurrencyStatus.currency.network.id, @@ -608,14 +616,55 @@ internal class StakingViewModel @Inject constructor( } } - private fun updateStakeBalance() { - viewModelScope.launch { - stakingYieldBalanceUseCase( + private fun scheduleUpdates() { + coroutineScope.launch { + listOf( + // we should update network to find pending tx after 1 sec + async { + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network)) + }, + // we should update tx history and network for new balances + async { + updateStakeBalance() + }, + async { + updateTxHistory() + }, + async { + updateNetworkStatuses() + }, + ).awaitAll() + } + } + + private suspend fun updateNetworkStatuses() { + updateDelayedNetworkStatusUseCase( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + delayMillis = BALANCE_UPDATE_DELAY, + refresh = true, + ) + } + + private suspend fun updateStakeBalance() { + stakingYieldBalanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + refresh = true, + ) + } + + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWalletId, + currency = cryptoCurrencyStatus.currency, + ) + + txHistoryItemsCountEither.onRight { + getTxHistoryItemsUseCase( userWalletId = userWalletId, - address = CryptoCurrencyAddress( - cryptoCurrencyStatus.currency, - cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), - ), + currency = cryptoCurrencyStatus.currency, refresh = true, ) } @@ -630,5 +679,6 @@ internal class StakingViewModel @Inject constructor( private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking" const val ALLOWANCE_UPDATE_DELAY = 10_000L + const val BALANCE_UPDATE_DELAY = 11_000L } } \ No newline at end of file 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 98590b9a89..cb24122b08 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 @@ -223,19 +223,16 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) viewModelScope.launch(dispatchers.main) { - walletManagersFacade.getAddress( + walletManagersFacade.getDefaultAddress( userWalletId = stateHolder.getSelectedWalletId(), network = cryptoCurrencyStatus.currency.network, - ) - .find { it.type == AddressType.Default } - ?.value - ?.let { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + )?.let { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) - walletEventSender.send( - event = WalletEvent.CopyAddress(address = it), - ) - } + walletEventSender.send( + event = WalletEvent.CopyAddress(address = it), + ) + } } } From 1715bcd79c0ff814041a89c1ce313fd9a695d6bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 15:05:00 +0300 Subject: [PATCH 02/90] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 4 +- .../java/com/tangem/tap/TangemApplication.kt | 8 +- .../domain/tasks/product/DerivationsFinder.kt | 11 ++- .../datasource/di/UserTokensStoreModule.kt | 34 ------- .../token/AppPreferencesUserTokensStore.kt | 63 ------------- .../datasource/local/token/UserTokensStore.kt | 46 ---------- .../local/token/UserTokensStoreMigration.kt | 56 ------------ .../token/UserTokensStoreMigrationRunner.kt | 50 ----------- .../tangem/data/tokens/di/TokensDataModule.kt | 9 +- .../repository/DefaultCurrenciesRepository.kt | 90 +++++++++++++------ .../repository/DefaultNetworksRepository.kt | 18 ++-- 11 files changed, 96 insertions(+), 293 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ff4810cfc6..21ab576e7f 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -10,7 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -76,7 +76,7 @@ interface ApplicationEntryPoint { fun getBalanceHidingRepository(): BalanceHidingRepository - fun getUserTokensStore(): UserTokensStore + fun getAppPreferencesStore(): AppPreferencesStore fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 42280203ef..ef8490029a 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -22,7 +22,7 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -126,8 +126,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val balanceHidingRepository: BalanceHidingRepository get() = entryPoint.getBalanceHidingRepository() - private val userTokensStore: UserTokensStore - get() = entryPoint.getUserTokensStore() + private val appPreferencesStore: AppPreferencesStore + get() = entryPoint.getAppPreferencesStore() val getAppThemeModeUseCase: GetAppThemeModeUseCase get() = entryPoint.getGetAppThemeModeUseCase() @@ -228,7 +228,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { } derivationsFinder = DerivationsFinder( - newTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, dispatchers = AppCoroutineDispatcherProvider(), ) appStateHolder.mainStore = store diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index d1f7b8a3d6..6fcad66476 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -5,7 +5,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.models.scan.CardDTO @@ -22,7 +25,7 @@ internal data class BlockchainToDerive( // FIXME: May be move to DI, currently unnecessary internal class DerivationsFinder( - private val newTokensStore: UserTokensStore, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -64,7 +67,9 @@ internal class DerivationsFinder( } private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - val responseTokens = newTokensStore.getSyncOrNull(userWalletId) + val responseTokens = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?.tokens ?: return hashSetOf() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt deleted file mode 100644 index b2997661fb..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.token.AppPreferencesUserTokensStore -import com.tangem.datasource.local.token.UserTokensStore -import com.tangem.datasource.local.token.UserTokensStoreMigrationRunner -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object UserTokensStoreModule { - - @Provides - @Singleton - fun provideUserTokensStore( - appPreferencesStore: AppPreferencesStore, - userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, - ): UserTokensStore { - return AppPreferencesUserTokensStore( - appPreferencesStore = appPreferencesStore, - userTokensStoreMigrationRunner = userTokensStoreMigrationRunner, - userWalletsStore = userWalletsStore, - dispatchers = dispatchers, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt deleted file mode 100644 index 80d70825bf..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* - -/** - * Implementation of [UserTokensStore] that based on [appPreferencesStore] - * - * @property appPreferencesStore application preference store - * -[REDACTED_AUTHOR] - */ -internal class AppPreferencesUserTokensStore( - private val appPreferencesStore: AppPreferencesStore, - private val userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, - private val userWalletsStore: UserWalletsStore, - private val dispatchers: CoroutineDispatcherProvider, -) : UserTokensStore { - - init { - runUserTokensMigrations() - } - - override fun get(key: UserWalletId): Flow { - return appPreferencesStore - .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) - .filterNotNull() - } - - override suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? { - return appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), - ) - } - - override suspend fun store(key: UserWalletId, value: UserTokensResponse) { - appPreferencesStore.storeObject( - key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), - value = value, - ) - } - - // TODO: delete in 5.15 (Mobile Sprint 161) [REDACTED_JIRA] - private fun runUserTokensMigrations() { - userWalletsStore.userWallets - .filter { it.isNotEmpty() } - .take(1) - .onEach { userWallets -> - userTokensStoreMigrationRunner.run(ids = userWallets.map { it.walletId.stringValue }) - } - .flowOn(dispatchers.io) - .launchIn(CoroutineScope(dispatchers.io)) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt deleted file mode 100644 index f3f12f026b..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -@Deprecated( - message = "Use AppPreferencesStore", - replaceWith = ReplaceWith( - expression = "AppPreferencesStore", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, -) -interface UserTokensStore { - - @Deprecated( - message = "Use getObject", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.getObject(userWalletId)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - fun get(key: UserWalletId): Flow - - @Deprecated( - message = "Use getObjectSyncOrNull", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.getObjectSyncOrNull(userWalletId)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? - - @Deprecated( - message = "Use storeObject", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.storeObject(userWalletId, response)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - suspend fun store(key: UserWalletId, value: UserTokensResponse) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt deleted file mode 100644 index dae09c0616..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.datasource.local.token - -import androidx.datastore.core.DataMigration -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject - -/** - * Migration of saving [UserTokensResponse] from file to [AppPreferencesStore] - * - * @param userWalletId user wallet id - * @param moshi moshi - * @property fileReader file reader - * -[REDACTED_AUTHOR] - */ -internal class UserTokensStoreMigration( - userWalletId: String, - moshi: Moshi, - private val fileReader: FileReader, -) : DataMigration { - - private val legacyFileName = "user_tokens_$userWalletId" - private val keyName = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId) - - @OptIn(ExperimentalStdlibApi::class) - private val adapter = moshi.adapter() - - override suspend fun shouldMigrate(currentData: AppPreferencesStore): Boolean = true - - override suspend fun migrate(currentData: AppPreferencesStore): AppPreferencesStore { - val currentKey = currentData.getObjectSyncOrNull(key = keyName) - - if (currentKey != null) return currentData - - val value = runCatching { - val json = fileReader.readFile(legacyFileName) - adapter.fromJson(json) - }.getOrNull() - - if (value != null) { - currentData.storeObject(key = keyName, value = value) - } - - return currentData - } - - override suspend fun cleanUp() { - fileReader.removeFile(legacyFileName) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt deleted file mode 100644 index eafb84aa79..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.datasource.local.token - -import com.squareup.moshi.Moshi -import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Runner that launch migrations of saving user tokens store - * - * @property appPreferencesStore application preference store - * @property fileReader file reader - * @property moshi moshi - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -@Singleton -class UserTokensStoreMigrationRunner @Inject constructor( - private val appPreferencesStore: AppPreferencesStore, - private val fileReader: FileReader, - @NetworkMoshi private val moshi: Moshi, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend fun run(ids: List) { - ids.forEach { id -> - coroutineScope { run(id) } - } - } - - private suspend fun run(id: String) { - withContext(dispatchers.io) { - val migration = UserTokensStoreMigration( - userWalletId = id, - moshi = moshi, - fileReader = fileReader, - ) - - migration.migrate(appPreferencesStore) - - migration.cleanUp() - } - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 59ddeeb01d..c37a3bc87f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -8,7 +8,6 @@ import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -28,7 +27,7 @@ internal object TokensDataModule { fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, - userTokensStore: UserTokensStore, + appPreferencesStore: AppPreferencesStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, expressAssetsStore: ExpressAssetsStore, @@ -38,7 +37,7 @@ internal object TokensDataModule { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, tangemExpressApi = tangemExpressApi, - userTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, expressAssetsStore = expressAssetsStore, @@ -71,7 +70,7 @@ internal object TokensDataModule { networksStatusesStore: NetworksStatusesStore, walletManagersFacade: WalletManagersFacade, userWalletsStore: UserWalletsStore, - userTokensStore: UserTokensStore, + appPreferencesStore: AppPreferencesStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): NetworksRepository { @@ -79,7 +78,7 @@ internal object TokensDataModule { networksStatusesStore = networksStatusesStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, - userTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ec3720fe1b..d1bc4dc436 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -18,8 +18,12 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation @@ -46,11 +50,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, - private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, private val expressAssetsStore: ExpressAssetsStore, private val cacheRegistry: CacheRegistry, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -86,7 +90,7 @@ internal class DefaultCurrenciesRepository( override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) { return withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, ) @@ -141,7 +145,7 @@ internal class DefaultCurrenciesRepository( override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, ) @@ -163,7 +167,7 @@ internal class DefaultCurrenciesRepository( override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) { return withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" }, ) @@ -276,9 +280,12 @@ internal class DefaultCurrenciesRepository( fetchTokensIfCacheExpired(userWallet, refresh) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWallet.walletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) } @@ -290,9 +297,12 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val response = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrency( currencyId = id, @@ -312,9 +322,12 @@ internal class DefaultCurrenciesRepository( fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) val blockchain = Blockchain.fromId(networkId.value) val blockchainNetworkId = blockchain.toNetworkId() val coinId = blockchain.toCoinId() @@ -335,7 +348,7 @@ internal class DefaultCurrenciesRepository( ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) launch(dispatchers.io) { - userTokensStore.get(userWalletId) + getSavedUserTokensResponse(userWalletId) .map { it.group == UserTokensResponse.GroupType.NETWORK } .collect(::send) } @@ -347,7 +360,7 @@ internal class DefaultCurrenciesRepository( ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) launch(dispatchers.io) { - userTokensStore.get(userWalletId) + getSavedUserTokensResponse(userWalletId) .map { it.sort == UserTokensResponse.SortType.BALANCE } .collect(::send) } @@ -461,9 +474,14 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) fetchTokensIfCacheExpired(userWallet, refresh = false) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), + ), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) return storedTokens.tokens.any { it.contractAddress != null && @@ -473,7 +491,7 @@ internal class DefaultCurrenciesRepository( } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { - return userTokensStore.get(userWallet.walletId).map { storedTokens -> + return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( response = storedTokens, scanResponse = userWallet.scanResponse, @@ -517,17 +535,26 @@ internal class DefaultCurrenciesRepository( .let { customTokensMerger.mergeIfPresented(userWalletId, response) } .let(userTokensBackwardCompatibility::applyCompatibilityAndGetUpdated) - userTokensStore.store(userWallet.walletId, compatibleUserTokensResponse) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWallet.walletId.stringValue), + value = compatibleUserTokensResponse, + ) + fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse) } private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean { - return demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(userWallet.walletId) == null + val response = getSavedUserTokensResponseSync(key = userWallet.walletId) + + return demoConfig.isDemoCardId(userWallet.cardId) && response == null } private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) - userTokensStore.store(userWalletId, compatibleUserTokensResponse) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) pushTokens(userWalletId, response) } @@ -561,8 +588,9 @@ internal class DefaultCurrenciesRepository( private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { val userWalletId = userWallet.walletId - val response = userTokensStore.getSyncOrNull(userWalletId) - ?: createDefaultUserTokensResponse(userWallet) + val response = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?: createDefaultUserTokensResponse(userWallet) if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId") @@ -622,4 +650,16 @@ internal class DefaultCurrenciesRepository( } private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" + + private fun getSavedUserTokensResponse(key: UserWalletId): Flow { + return appPreferencesStore + .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) + .filterNotNull() + } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index de6ad111ab..3762b4ddee 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -8,8 +8,11 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.network.NetworksStatusesStore -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.LceFlow @@ -33,7 +36,7 @@ internal class DefaultNetworksRepository( private val networksStatusesStore: NetworksStatusesStore, private val walletManagersFacade: WalletManagersFacade, private val userWalletsStore: UserWalletsStore, - private val userTokensStore: UserTokensStore, + private val appPreferencesStore: AppPreferencesStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : NetworksRepository { @@ -300,9 +303,14 @@ internal class DefaultNetworksRepository( } return if (userWallet.isMultiCurrency) { - val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val response = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() } else { From 2c29708284a5c2f4e9f82567a199182207d234d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 17:44:09 +0300 Subject: [PATCH 03/90] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../common/ui/userwallet/UserWalletItem.kt | 198 ++++++++++++ .../ui/userwallet/state/UserWalletItemUM.kt | 22 ++ core/res/src/main/res/values-de/strings.xml | 4 +- core/res/src/main/res/values-ja/strings.xml | 4 +- core/res/src/main/res/values-ru/strings.xml | 2 - .../src/main/res/values-uk-rUA/strings.xml | 4 +- core/ui/build.gradle.kts | 2 + .../core/ui/coil/RotationTransformation.kt | 22 ++ .../com/tangem/core/ui/components/Shimmers.kt | 9 +- .../block/information/InformationBlock.kt | 4 +- .../core/ui/components/buttons/SmallButton.kt | 23 +- .../core/ui/components/rows/ArrowRow.kt | 92 ++++-- .../core/ui/components/rows/BlockchainRow.kt | 7 +- .../tangem/core/ui/res/TangemThemePreview.kt | 4 + .../drawable/img_card_wallet_2_gray_22_36.xml | 13 + features/details/impl/build.gradle.kts | 1 + .../preview/PreviewUserWalletListComponent.kt | 7 +- .../details/entity/UserWalletListUM.kt | 17 +- .../details/model/UserWalletListModel.kt | 4 +- .../details/ui/UserWalletListBlock.kt | 106 +------ .../details/utils/UserWalletMappers.kt | 6 +- .../details/utils/UserWalletsFetcher.kt | 6 +- .../managetokens/ui/ManageTokensScreen.kt | 1 + features/markets/impl/build.gradle.kts | 2 + .../impl/model/MarketsTokenDetailsModel.kt | 2 + .../api/MarketsPortfolioComponent.kt | 17 + .../impl/DefaultMarketsPortfolioComponent.kt | 39 +++ .../portfolio/impl/di/ComponentModule.kt | 20 ++ .../markets/portfolio/impl/di/ModelModule.kt | 20 ++ .../impl/model/MarketsPortfolioModel.kt | 20 ++ .../impl/ui/AddToPortfolioBottomSheet.kt | 240 ++++++++++++++ .../markets/portfolio/impl/ui/MyPortfolio.kt | 174 +++++++++++ .../portfolio/impl/ui/PortfolioItem.kt | 292 ++++++++++++++++++ .../impl/ui/PortfolioQuickActions.kt | 214 +++++++++++++ .../impl/ui/TokenActionsBottomSheet.kt | 90 ++++++ .../PreviewAddToPortfolioBSContentProvider.kt | 60 ++++ .../preview/PreviewMyPortfolioUMProvider.kt | 50 +++ .../ui/state/AddToPortfolioBSContentUM.kt | 10 + .../portfolio/impl/ui/state/MyPortfolioUM.kt | 29 ++ .../impl/ui/state/PortfolioTokenUM.kt | 34 ++ .../portfolio/impl/ui/state/QuickActionUM.kt | 30 ++ .../impl/ui/state/SelectNetworkUM.kt | 13 + .../impl/ui/state/TokenActionsBSContent.kt | 28 ++ .../tangem/feature/swap/ui/StateBuilder.kt | 8 +- 45 files changed, 1776 insertions(+), 175 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt create mode 100644 core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index e57cd07bd1..40a779c416 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.coil) /** Deps */ implementation(deps.kotlin.immutable.collections) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt new file mode 100644 index 0000000000..baf96fd3cf --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -0,0 +1,198 @@ +package com.tangem.common.ui.userwallet + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.common.ui.R +import com.tangem.core.ui.coil.RotationTransformation +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun UserWalletItem(state: UserWalletItemUM, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + onClick = state.onClick, + enabled = state.isEnabled, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size68) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + CardImage(imageUrl = state.imageUrl) + NameAndInfo( + modifier = Modifier.weight(1f), + name = state.name, + information = state.information, + ) + + when (state.endIcon) { + UserWalletItemUM.EndIcon.None -> {} + UserWalletItemUM.EndIcon.Arrow -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + UserWalletItemUM.EndIcon.Checkmark -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) { + Column( + modifier = modifier.heightIn(min = TangemTheme.dimens.size40), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Text( + text = name.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + AnimatedContent( + targetState = information.resolveReference(), + label = "User wallet information", + ) { information -> + Text( + text = information, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) { + val imageModifier = modifier + .width(TangemTheme.dimens.size24) + .height(TangemTheme.dimens.size36) + .clip(TangemTheme.shapes.roundedCornersSmall) + + SubcomposeAsyncImage( + modifier = imageModifier, + model = ImageRequest.Builder(LocalContext.current) + .transformations(RotationTransformation(angle = 90f)) + .size( + width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, + height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, + ) + .data(imageUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { + RectangleShimmer( + modifier = imageModifier, + radius = TangemTheme.dimens.size2, + ) + }, + error = { + Image( + modifier = imageModifier, + imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36), + contentDescription = null, + ) + }, + contentDescription = null, + ) +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + val list = persistentListOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_1".encodeToByteArray()), + name = stringReference("My Wallet"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_2".encodeToByteArray()), + name = stringReference("Old wallet"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = true, + onClick = {}, + endIcon = UserWalletItemUM.EndIcon.Arrow, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = false, + endIcon = UserWalletItemUM.EndIcon.Checkmark, + onClick = {}, + ), + ) + + Column { + list.fastForEach { userWalletItemUM -> + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + state = userWalletItemUM, + ) + } + } + } +} + +private fun getInformation(cardCount: Int, totalBalance: String): TextReference { + val t1 = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + val divider = stringReference(value = " • ") + val t2 = stringReference(totalBalance) + + return TextReference.Combined(wrappedList(t1, divider, t2)) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt new file mode 100644 index 0000000000..f8e361a6f2 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -0,0 +1,22 @@ +package com.tangem.common.ui.userwallet.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.wallets.models.UserWalletId +import javax.annotation.concurrent.Immutable + +@Immutable +data class UserWalletItemUM( + val id: UserWalletId, + val name: TextReference, + val information: TextReference, + val imageUrl: String, + val isEnabled: Boolean, + val endIcon: EndIcon = EndIcon.None, + val onClick: () -> Unit, +) { + enum class EndIcon { + None, + Arrow, + Checkmark, + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 1e9ed93914..21258cdee1 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -835,6 +835,8 @@ Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. + Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. + Genehmigung in Arbeit Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. @@ -852,8 +854,6 @@ Netzwerk erfordert eine Mindesteinzahlung Der Swap wird nach Abschluss der Transaktion %s verfügbar sein. Du hast aktive Transaktion - Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. - Genehmigung in Arbeit Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt. Du hast keine %s Coins in deiner Liste Keine Token zum Tauschen verfügbar diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e5c43474a1..046d0156bc 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -823,6 +823,8 @@ ウォレット設定 Tangem %sを使用するか、カードをスキャンしてウォレットにアクセスしてください + スワップの承認は現在進行中で、まもなく完了する予定です。 + 承認が進行中 カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 @@ -840,8 +842,6 @@ ネットワークには最低残高が必要です スワップは、%s の取引完了後に利用可能となります。 アクティブな取引があります - スワップの承認は現在進行中で、まもなく完了する予定です。 - 承認が進行中 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 あなたのリストには、交換可能な %s トークンがありません。 スワップ可能なトークンがありません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e89bb61d95..fe433aaed9 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -859,8 +859,6 @@ Для работы с сетью необходим депозит Обмен будет доступен после завершения %s транзакции У вас есть активная транзакция - Разрешение обмена в процессе и будет скоро завершено - Разрешение в процессе Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. У вас в списке нет монет доступных для обмена с %s Нет доступных для обмена токенов diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index d352a7be3c..62d3baa6e6 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -844,6 +844,8 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця + Затвердження обміну триває і незабаром буде завершено + Затвердження в процесі Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. @@ -861,8 +863,6 @@ Для роботи з мережею вимагається депозит Обмін буде доступний після завершення %s транзакції У вас є активна транзакція - Затвердження обміну триває і незабаром буде завершено - Затвердження в процесі Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s. У вашому списку немає доступних монет для обміну %s Немає доступних токенів для обміну diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index cb8f7f15ee..4758911a25 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.ide.common.resources.generateLocaleList + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) diff --git a/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt new file mode 100644 index 0000000000..6a8019af1e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt @@ -0,0 +1,22 @@ +package com.tangem.core.ui.coil + +import android.graphics.Bitmap +import android.graphics.Matrix +import coil.size.Size +import coil.transform.Transformation + +class RotationTransformation(private val angle: Float) : Transformation { + + override val cacheKey: String = "rotate:$angle" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val matrix = Matrix().apply { + val centerX = input.width / 2f + val centerY = input.height / 2f + + postRotate(angle, centerX, centerY) + } + + return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 5d09690136..c0f97514dc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -101,7 +102,11 @@ fun TextShimmer( * Height and min width will be set automatically */ @Composable -fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) { +fun SmallButtonShimmer( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), + withIcon: Boolean = false, +) { PrimarySmallButton( config = SmallButtonConfig( text = stringReference("B"), @@ -113,7 +118,7 @@ fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) }, ), modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .clip(shape) .shimmer(LocalTangemShimmer.current), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 900aafe401..fb2230889c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -29,6 +30,7 @@ class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope fun InformationBlock( title: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, + contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12, action: (@Composable BoxScope.() -> Unit)? = null, content: (@Composable InformationBlockContentScope.() -> Unit)? = null, ) { @@ -72,7 +74,7 @@ fun InformationBlock( if (content != null) { Box( modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing12) + .padding(horizontal = contentHorizontalPadding) .fillMaxWidth(), ) { val scope = InformationBlockContentScope(scope = this) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt index e02ae97109..195fae0923 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -33,6 +33,7 @@ data class SmallButtonConfig( val text: TextReference, val onClick: () -> Unit, val icon: TangemButtonIconPosition = TangemButtonIconPosition.None, + val enabled: Boolean = true, ) /** @@ -57,6 +58,7 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie SmallButton(config = config, isPrimary = false, modifier = modifier) } +@Suppress("LongMethod") @Composable private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) { val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16) @@ -77,7 +79,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: color = backgroundColor, shape = shape, ) - .clickable(enabled = true, onClick = config.onClick) + .clickable(enabled = config.enabled, onClick = config.onClick) .padding( paddingValues = when (config.icon) { is TangemButtonIconPosition.None -> PaddingValues( @@ -100,7 +102,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: iconPosition = config.icon, text = { val textColor by animateColorAsState( - targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, + targetValue = when { + !config.enabled -> TangemTheme.colors.text.disabled + isPrimary -> TangemTheme.colors.text.primary2 + else -> TangemTheme.colors.text.primary1 + }, label = "Update text color", ) @@ -116,7 +122,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Icon( modifier = Modifier.size(TangemTheme.dimens.size16), painter = painterResource(id = iconResId), - tint = TangemTheme.colors.icon.secondary, + tint = if (config.enabled) { + TangemTheme.colors.icon.secondary + } else { + TangemTheme.colors.icon.inactive + }, contentDescription = null, ) }, @@ -174,5 +184,12 @@ private fun ButtonsSample() { icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), ), ) + SecondarySmallButton( + config = config.copy( + text = TextReference.Str(value = "Add token"), + icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), + enabled = false, + ), + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt index 0e594fbd91..67ef1ac47d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.* @@ -68,6 +69,7 @@ private class ChildArrowScope( @Composable fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr val figureWidth = TangemTheme.dimens.size40 val strokeColor = TangemTheme.colors.stroke.secondary @@ -86,18 +88,31 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { ) val arrowHeadRectDp = DpRect( origin = DpOffset( - x = figureWidth - arrowHeadSize.width, + x = if (isLtr) { + figureWidth - arrowHeadSize.width + } else { + 0.dp + }, y = figureRectDp.size.center.y - arrowHeadSize.center.y, ), size = arrowHeadSize, ) - val curvedArrowRectDp = DpRect( - top = figureRectDp.top, - left = TangemTheme.dimens.size18, - right = figureRectDp.right - arrowHeadRectDp.width, - bottom = figureRectDp.size.center.y, - ) + val curvedArrowRectDp = if (isLtr) { + DpRect( + top = figureRectDp.top, + left = TangemTheme.dimens.size18, + right = figureRectDp.right - arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + } else { + DpRect( + top = figureRectDp.top, + left = arrowHeadRectDp.width, + right = TangemTheme.dimens.size18 + arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + } Canvas( modifier = Modifier @@ -114,20 +129,26 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { drawScope = this, ) - scope.drawCurveArrow() - scope.drawArrowHead() + scope.drawCurveArrow(isLtr) + scope.drawArrowHead(isLtr) if (!isLastChild) { - scope.drawArrowLine() + scope.drawArrowLine(isLtr) } } } -private fun ChildArrowScope.drawArrowHead() { +private fun ChildArrowScope.drawArrowHead(isLtr: Boolean) { val arrowHeadPath = Path().apply { - moveTo(arrowHeadRect.centerRight) - lineTo(arrowHeadRect.topLeft) - lineTo(arrowHeadRect.bottomLeft) + if (isLtr) { + moveTo(arrowHeadRect.centerRight) + lineTo(arrowHeadRect.topLeft) + lineTo(arrowHeadRect.bottomLeft) + } else { + moveTo(arrowHeadRect.centerLeft) + lineTo(arrowHeadRect.topRight) + lineTo(arrowHeadRect.bottomRight) + } close() } val paint = Paint().apply { @@ -143,13 +164,21 @@ private fun ChildArrowScope.drawArrowHead() { } } -private fun ChildArrowScope.drawCurveArrow() { +private fun ChildArrowScope.drawCurveArrow(isLtr: Boolean) { val curveArrowPath = Path().apply { - moveTo(curvedArrowRect.topLeft) - quadraticBezierTo( - control = curvedArrowRect.bottomLeft, - end = curvedArrowRect.bottomRight, - ) + if (isLtr) { + moveTo(curvedArrowRect.topLeft) + quadraticBezierTo( + control = curvedArrowRect.bottomLeft, + end = curvedArrowRect.bottomRight, + ) + } else { + moveTo(curvedArrowRect.topRight) + quadraticBezierTo( + control = curvedArrowRect.bottomRight, + end = curvedArrowRect.bottomLeft, + ) + } } drawPath( path = curveArrowPath, @@ -158,11 +187,20 @@ private fun ChildArrowScope.drawCurveArrow() { ) } -private fun ChildArrowScope.drawArrowLine() { - drawLine( - color = strokeColor, - start = curvedArrowRect.topLeft, - end = Offset(curvedArrowRect.left, figureRect.bottom), - strokeWidth = arrowStrokeWidth, - ) +private fun ChildArrowScope.drawArrowLine(isLtr: Boolean) { + if (isLtr) { + drawLine( + color = strokeColor, + start = curvedArrowRect.topLeft, + end = Offset(curvedArrowRect.left, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) + } else { + drawLine( + color = strokeColor, + start = curvedArrowRect.topRight, + end = Offset(curvedArrowRect.right, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index 4f8edf75c4..035be68d2e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -29,8 +29,9 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni modifier = modifier .heightIn(min = TangemTheme.dimens.size52) .padding( - vertical = TangemTheme.dimens.spacing8, - horizontal = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing8, ), icon = { RowIcon( @@ -125,7 +126,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid BlockchainRow( model = state, action = { - TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true) + TangemSwitch(onCheckedChange = { }, checked = true) }, ) }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index 0347ca4893..dd15fd2212 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection import com.tangem.core.ui.windowsize.rememberWindowSizePreview @Composable @@ -14,12 +16,14 @@ fun TangemThemePreview( typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, alwaysShowBottomSheets: Boolean = true, + rtl: Boolean = false, content: @Composable () -> Unit, ) { val isDarkTheme = isDark ?: isSystemInDarkTheme() CompositionLocalProvider( LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets, + LocalLayoutDirection provides if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr, ) { BoxWithConstraints { TangemTheme( diff --git a/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml b/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml new file mode 100644 index 0000000000..977d693e60 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 82ca657a96..ff405d0d36 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics.models) implementation(projects.common.routing) + implementation(projects.common.ui) /* Project - Domain */ implementation(projects.domain.models) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 92b6f46de3..e2a4774961 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -17,7 +18,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { private val previewState = UserWalletListUM( userWallets = persistentListOf( - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_1".encodeToByteArray()), name = stringReference("My Wallet"), information = getInformation(3, "4 496,75 $"), @@ -25,7 +26,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { isEnabled = true, onClick = {}, ), - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_2".encodeToByteArray()), name = stringReference("Old wallet"), information = getInformation(3, "4 496,75 $"), @@ -33,7 +34,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { isEnabled = true, onClick = {}, ), - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_3".encodeToByteArray()), name = stringReference("Multi Card"), information = getInformation(3, "4 496,75 $"), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index 1569f48efb..a8ef5eb141 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -1,25 +1,14 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.ImmutableList @Immutable internal data class UserWalletListUM( - val userWallets: ImmutableList, + val userWallets: ImmutableList, val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, -) { - - @Immutable - data class UserWalletUM( - val id: UserWalletId, - val name: TextReference, - val information: TextReference, - val imageUrl: String, - val isEnabled: Boolean, - val onClick: () -> Unit, - ) -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 12f18cc1f4..a36e126509 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -1,12 +1,12 @@ package com.tangem.features.details.model +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.details.utils.UserWalletsFetcher @@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor( } private fun updateState( - userWallets: ImmutableList, + userWallets: ImmutableList, shouldSaveUserWallets: Boolean, isWalletSavingInProgress: Boolean, ) = state.update { value -> diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index 794ef632c3..b09124ca04 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -1,7 +1,6 @@ package com.tangem.features.details.ui import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -10,32 +9,25 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R -import com.tangem.features.details.ui.coil.RotationTransformation @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { BlockCard( modifier = modifier, ) { - state.userWallets.forEach { model -> - key(model.id) { + state.userWallets.forEach { state -> + key(state.id) { UserWalletItem( modifier = Modifier.fillMaxWidth(), - model = model, + state = state, ) } } @@ -47,96 +39,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M } } -@Composable -private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier, - onClick = model.onClick, - enabled = model.isEnabled, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size68) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Image(imageUrl = model.imageUrl) - NameAndInfo( - name = model.name, - information = model.information, - ) - } - } -} - -@Composable -private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) { - Column( - modifier = modifier.heightIn(min = TangemTheme.dimens.size40), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceEvenly, - ) { - Text( - text = name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - AnimatedContent( - targetState = information.resolveReference(), - label = "User wallet information", - ) { information -> - Text( - text = information, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -private fun Image(imageUrl: String, modifier: Modifier = Modifier) { - val imageModifier = modifier - .width(TangemTheme.dimens.size24) - .height(TangemTheme.dimens.size36) - .clip(TangemTheme.shapes.roundedCornersSmall) - - SubcomposeAsyncImage( - modifier = imageModifier, - model = ImageRequest.Builder(LocalContext.current) - .transformations(RotationTransformation(angle = 90f)) - .size( - width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, - height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, - ) - .data(imageUrl) - .crossfade(enable = true) - .allowHardware(enable = false) - .build(), - loading = { - RectangleShimmer( - modifier = imageModifier, - radius = TangemTheme.dimens.size2, - ) - }, - error = { - Image( - modifier = imageModifier, - painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36), - contentDescription = null, - ) - }, - contentDescription = null, - ) -} - @Composable private fun AddWalletButton( text: TextReference, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index a9782fc1d9..1a3efa0cf4 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -1,5 +1,6 @@ package com.tangem.features.details.utils +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -7,7 +8,6 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.ImmutableList @@ -19,7 +19,7 @@ internal fun List.toUiModels( balances: Map = emptyMap(), isLoading: Boolean = true, isBalancesHidden: Boolean = false, -): ImmutableList = this.map { model -> +): ImmutableList = this.map { model -> val balance = balances[model.walletId] model.toUiModel( @@ -37,7 +37,7 @@ private fun UserWallet.toUiModel( isLoading: Boolean, isBalanceHidden: Boolean, onClick: () -> Unit, -): UserWalletUM = UserWalletUM( +): UserWalletItemUM = UserWalletItemUM( id = walletId, name = stringReference(name), information = getInfo( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index e141c4034e..cd5013c005 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.utils import arrow.core.Either import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender @@ -22,7 +23,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -40,7 +40,7 @@ internal class UserWalletsFetcher @Inject constructor( ) { @OptIn(ExperimentalCoroutinesApi::class) - val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> + val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) combine( @@ -72,7 +72,7 @@ internal class UserWalletsFetcher @Inject constructor( maybeAppCurrency: Either, maybeBalances: Lce>, balanceHidingSettings: BalanceHidingSettings, - ): Lce> = lce { + ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, block = { maybeBalances.bindOrNull().orEmpty() }, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 9f9584fac9..3008c84d30 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -286,6 +286,7 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod isLastItem = index == currentItems.lastIndex, content = { BlockchainRow( + modifier = Modifier.padding(end = TangemTheme.dimens.spacing8), model = with(network) { BlockchainRowUM( name = name, diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 7d8b7d0667..2e1f37fd3e 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.domain.markets) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.wallets.models) /* Compose */ implementation(deps.compose.coil) @@ -46,6 +47,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) + /* Common */ implementation(projects.common.ui) implementation(projects.common.uiCharts) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 2a40b0c83e..0f2d2d1465 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.ui.charts.state.* +import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener @@ -43,6 +44,7 @@ import javax.inject.Inject @Suppress("LargeClass", "LongParameterList") @Stable +@ComponentScoped internal class MarketsTokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt new file mode 100644 index 0000000000..c4217765dd --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.markets.portfolio.api + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import kotlinx.serialization.Serializable + +@Stable +interface MarketsPortfolioComponent : ComposableContentComponent { + + @Serializable + data class Params( + val tokenId: String, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt new file mode 100644 index 0000000000..9b732e6e42 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.markets.portfolio.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel +import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: MarketsPortfolioComponent.Params, +) : AppComponentContext by context, MarketsPortfolioComponent { + + private val model: MarketsPortfolioModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + MyPortfolio( + modifier = modifier, + state = MyPortfolioUM.Loading, + ) + } + + @AssistedFactory + interface Factory : MarketsPortfolioComponent.Factory { + override fun create( + context: AppComponentContext, + params: MarketsPortfolioComponent.Params, + ): DefaultMarketsPortfolioComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..d011fbf799 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.di + +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindMarketsPortfolioComponent( + factory: DefaultMarketsPortfolioComponent.Factory, + ): MarketsPortfolioComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt new file mode 100644 index 0000000000..38ea8f8688 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(MarketsPortfolioModel::class) + fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt new file mode 100644 index 0000000000..75baa3fd50 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class MarketsPortfolioModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params = paramsContainer.require() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt new file mode 100644 index 0000000000..8a5a2ae45f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -0,0 +1,240 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerW6 +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.components.rows.ArrowRow +import com.tangem.core.ui.components.rows.BlockchainRow +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM + +@Composable +internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + titleText = resourceReference(R.string.markets_add_to_portfolio_button), + ) { + Content( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = TangemTheme.dimens.spacing16), + state = config.content as AddToPortfolioBSContentUM, + ) + } +} + +@Composable +private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + UserWalletItem(state.selectedWallet) + + NetworkSelection( + modifier = Modifier.fillMaxWidth(), + state = state.selectNetworkUM, + ) + + AnimatedVisibility( + visible = state.isScanCardNotificationVisible, + modifier = Modifier.fillMaxWidth(), + ) { + ScanWalletWarning(modifier = Modifier.fillMaxWidth()) + } + + PrimaryButtonIconEnd( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.common_continue), + iconResId = R.drawable.ic_tangem_24, + onClick = {}, + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.markets_select_network), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing14), + verticalAlignment = Alignment.CenterVertically, + ) { + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + url = state.iconUrl, + alpha = 1f, + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + SpacerW12() + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .alignByBaseline(), + text = state.tokenName, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + SpacerW6() + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .alignByBaseline(), + text = state.tokenCurrencySymbol, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } + + state.networks.fastForEachIndexed { index, network -> + ArrowRow( + isLastItem = index == state.networks.lastIndex, + content = { + BlockchainRow( + modifier = Modifier.padding( + end = TangemTheme.dimens.spacing4, + ), + model = with(network) { + BlockchainRowUM( + name = name, + type = type, + iconResId = iconResId, + isMainNetwork = isMainNetwork, + isSelected = isSelected, + ) + }, + action = { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = { + state.onNetworkSwitchClick(network, it) + }, + ) + }, + ) + }, + ) + } + } + } +} + +@Composable +private fun ScanWalletWarning(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background( + color = TangemTheme.colors.button.disabled, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), + ) { + Icon( + modifier = Modifier.requiredSize(TangemTheme.dimens.size20), + imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + contentDescription = null, + ) + Text( + text = stringResource(R.string.markets_generate_addresses_notification), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview { + AddToPortfolioBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + content = content, + onDismissRequest = {}, + ), + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun PreviewContent( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview { + Content( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth(), + state = content, + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun PreviewContentRtl( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview(rtl = true) { + Content( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth(), + state = content, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt new file mode 100644 index 0000000000..30cbcb453f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -0,0 +1,174 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM + +@Composable +internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + contentHorizontalPadding = 0.dp, + title = { + Text( + text = stringResource(R.string.markets_common_my_portfolio), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + action = { + if (state !is MyPortfolioUM.Tokens) return@InformationBlock + + when (state.buttonState) { + MyPortfolioUM.Tokens.AddButtonState.Loading -> { + SmallButtonShimmer( + modifier = Modifier.size(width = 63.dp, height = TangemTheme.dimens.size18), + shape = RoundedCornerShape(TangemTheme.dimens.radius3), + ) + } + else -> { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.markets_add_token), + icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), + onClick = state.onAddClick, + enabled = state.buttonState == MyPortfolioUM.Tokens.AddButtonState.Available, + ), + ) + } + } + }, + ) { + when (state) { + is MyPortfolioUM.Tokens -> TokenList(state = state) + is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state) + MyPortfolioUM.Loading -> LoadingPlaceholder() + MyPortfolioUM.Unavailable -> UnavailableContent() + } + } +} + +@Composable +private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) { + Column(modifier) { + state.tokens.fastForEachIndexed { index, token -> + PortfolioItem( + state = token, + lastInList = index == state.tokens.size - 1, + ) + } + } +} + +@Composable +private fun UnavailableContent(modifier: Modifier = Modifier) { + Text( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Text( + text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.markets_add_to_portfolio_button), + onClick = state.onAddClick, + ) + } +} + +@Composable +private fun LoadingPlaceholder(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.7f), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing8), + ) { + MyPortfolio(state) + } + } +} + +@Preview +@Composable +private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { + TangemThemePreview(rtl = true) { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing8), + ) { + MyPortfolio(state) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt new file mode 100644 index 0000000000..3a98293707 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -0,0 +1,292 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.utils.StringsSigns + +// TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement) +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { + val hapticManager = LocalHapticManager.current + + Column(modifier) { + Row( + modifier = Modifier + .combinedClickable( + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + state.onClick() + }, + onLongClick = { + hapticManager.perform(TangemHapticEffect.View.LongPress) + state.onLongTap() + }, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .padding( + vertical = TangemTheme.dimens.spacing15, + horizontal = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Content(state) + } + + PortfolioQuickActions( + modifier = Modifier.padding( + bottom = if (lastInList) { + TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing24 + }, + ), + isVisible = state.isQuickActionsShown, + onActionClick = state.onQuickActionClick, + ) + } +} + +@Composable +private fun RowScope.Content(state: PortfolioTokenUM) { + // TODO add custom token + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + url = state.iconUrl, + alpha = 1f, // TODO add disabled state + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + + Column( + modifier = Modifier.align(Alignment.CenterVertically), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + when (state.balanceContent) { + is PortfolioTokenUM.BalanceContent.Disabled -> { + Disabled( + state = state, + disabledText = state.balanceContent.text.resolveReference(), + ) + } + PortfolioTokenUM.BalanceContent.Loading -> { + Loading(state = state) + } + is PortfolioTokenUM.BalanceContent.TokenBalance -> { + TokenBalance( + state = state, + content = state.balanceContent, + ) + } + } + } +} + +@Composable +private fun ColumnScope.TokenBalance(state: PortfolioTokenUM, content: PortfolioTokenUM.BalanceContent.TokenBalance) { + val balance = if (content.hidden) { + StringsSigns.STARS + } else { + content.balance + } + val tokenAmount = if (content.hidden) { + StringsSigns.STARS + } else { + content.tokenAmount + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + modifier = Modifier.alignByBaseline(), + text = state.title, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.alignByBaseline(), + text = balance, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = state.subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = tokenAmount, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun ColumnScope.Loading(state: PortfolioTokenUM) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + modifier = Modifier.alignByBaseline(), + text = state.title, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size40) + .alignByBaseline(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = state.subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size40) + .alignByBaseline(), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } +} + +@Composable +private fun Disabled(state: PortfolioTokenUM, disabledText: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + Modifier.weight(1f), + ) { + Text( + text = state.title, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + + Text( + text = disabledText, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + var quickActionsShown by remember { mutableStateOf(false) } + var quickActionsShown2 by remember { mutableStateOf(false) } + val sampleToken = PreviewMyPortfolioUMProvider().sampleToken + + Box( + Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + Column { + PortfolioItem( + state = sampleToken + .copy( + onClick = { + if (quickActionsShown2) { + quickActionsShown2 = false + } + quickActionsShown = quickActionsShown.not() + }, + isQuickActionsShown = quickActionsShown, + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy( + onClick = { + if (quickActionsShown) { + quickActionsShown = false + } + quickActionsShown2 = quickActionsShown2.not() + }, + isQuickActionsShown = quickActionsShown2, + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy( + balanceContent = ( + sampleToken.balanceContent + as PortfolioTokenUM.BalanceContent.TokenBalance + ) + .copy(hidden = true), + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy( + balanceContent = PortfolioTokenUM.BalanceContent.Disabled( + stringReference("No Address"), + ), + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy(balanceContent = PortfolioTokenUM.BalanceContent.Loading), + lastInList = true, + ) + } + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt new file mode 100644 index 0000000000..1af8cde8ce --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt @@ -0,0 +1,214 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM + +@Composable +internal fun PortfolioQuickActions( + isVisible: Boolean, + onActionClick: (QuickActionUM) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isVisible, + enter = expandVertically(expandFrom = Alignment.Top), + exit = shrinkVertically(shrinkTowards = Alignment.Top), + ) { + Column(modifier = modifier) { + LineSeparator() + QuickActionItem( + state = QuickActionUM.Buy, + onClick = { onActionClick(QuickActionUM.Buy) }, + ) + LineSeparator() + QuickActionItem( + state = QuickActionUM.Exchange, + onClick = { onActionClick(QuickActionUM.Exchange) }, + ) + LineSeparator() + QuickActionItem( + state = QuickActionUM.Receive, + onClick = { onActionClick(QuickActionUM.Receive) }, + ) + } + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) { + val lineColor = TangemTheme.colors.stroke.primary + val strokeWidth = TangemTheme.dimens.size1 + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val verticalPadding = TangemTheme.dimens.spacing2 + val startPadding = TangemTheme.dimens.spacing28 + + val height = TangemTheme.dimens.size16 + verticalPadding * 2 + + Canvas( + modifier = modifier + .animateEnterExit( + enter = expandVertically( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + ), + expandFrom = Alignment.Top, + ) + fadeIn(), + exit = shrinkVertically( + spring( + stiffness = Spring.StiffnessLow, + ), + shrinkTowards = Alignment.Top, + ) + fadeOut(), + ) + .fillMaxWidth() + .height(height), + ) { + val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx() + + drawLine( + color = lineColor, + start = Offset(x, verticalPadding.toPx()), + end = Offset(x, size.height - verticalPadding.toPx()), + strokeWidth = strokeWidth.toPx(), + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AnimatedVisibilityScope.QuickActionItem( + state: QuickActionUM, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticManager = LocalHapticManager.current + + Row( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersMedium) + .clickable { + hapticManager.perform(TangemHapticEffect.View.SegmentTick) + onClick() + } + .padding( + vertical = TangemTheme.dimens.spacing2, + horizontal = TangemTheme.dimens.spacing12, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), + ) { + Box( + Modifier + .animateEnterExit( + enter = scaleIn(), + exit = scaleOut(), + ) + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ) + .size(TangemTheme.dimens.size32), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(TangemTheme.dimens.size16), + imageVector = ImageVector.vectorResource(id = state.icon), + contentDescription = null, + tint = TangemTheme.colors.button.primary, + ) + } + Column( + modifier = Modifier + .animateEnterExit( + enter = fadeIn(), + exit = fadeOut(), + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.description.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + var isVisible by remember { mutableStateOf(true) } + + Column( + modifier = Modifier + .fillMaxWidth() + .height(680.dp), + ) { + Button( + onClick = { isVisible = !isVisible }, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) { + Text(text = "Toggle") + } + SpacerH4() + Box( + modifier = Modifier.background(color = TangemTheme.colors.background.action), + ) { + PortfolioQuickActions( + isVisible = isVisible, + onActionClick = {}, + ) + } + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewRtl() { + TangemThemePreview(rtl = true) { + Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { + PortfolioQuickActions( + isVisible = true, + onActionClick = {}, + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt new file mode 100644 index 0000000000..cfc6c814d1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -0,0 +1,90 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.inputrow.InputRowChecked +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContent +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + title = { content -> + TangemBottomSheetTitle(content.title) + }, + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: TokenActionsBSContent) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + content.actions.forEachIndexed { index, action -> + val cornersToRound = when (index) { + 0 -> CornersToRound.TOP_2 + content.actions.lastIndex -> CornersToRound.BOTTOM_2 + else -> CornersToRound.ZERO + } + + DividerContainer( + modifier = Modifier + .clip(cornersToRound.getShape()) + .background(TangemTheme.colors.background.action) + .clickable { content.onActionClick(action) }, + showDivider = index != content.actions.lastIndex, + ) { + InputRowChecked( + text = action.text, + checked = false, + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 640) +@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview( + alwaysShowBottomSheets = true, + ) { + Box(Modifier.background(TangemTheme.colors.background.secondary)) { + TokenActionsBottomSheet( + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = TokenActionsBSContent( + title = "Wallet 1", + actions = TokenActionsBSContent.Action.entries.toImmutableList(), + onActionClick = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt new file mode 100644 index 0000000000..2b33545fd3 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -0,0 +1,60 @@ +package com.tangem.features.markets.portfolio.impl.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + AddToPortfolioBSContentUM( + selectedWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ), + selectNetworkUM = SelectNetworkUM( + tokenId = "etherium", + tokenName = "Etherium", + tokenCurrencySymbol = "ETH", + networks = persistentListOf( + BlockchainRowUM( + name = "Etherium", + type = "MAIN", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = true, + isSelected = true, + ), + BlockchainRowUM( + name = "Etherium 2", + type = "TEST", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = false, + ), + BlockchainRowUM( + name = "Etherium 3", + type = "TEST", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = false, + ), + ), + onNetworkSwitchClick = { _, _ -> }, + iconUrl = null, + ), + isScanCardNotificationVisible = true, + ), + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt new file mode 100644 index 0000000000..b607386001 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -0,0 +1,50 @@ +package com.tangem.features.markets.portfolio.impl.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken, sampleToken), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Available, + onAddClick = {}, + ), + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable, + onAddClick = {}, + ), + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading, + onAddClick = {}, + ), + MyPortfolioUM.AddFirstToken( + onAddClick = {}, + ), + MyPortfolioUM.Loading, + MyPortfolioUM.Unavailable, + ) + + val sampleToken = PortfolioTokenUM( + id = "", + networkId = "", + iconUrl = "", + balanceContent = PortfolioTokenUM.BalanceContent.TokenBalance( + balance = "486,65 \$", + tokenAmount = "733,71097 MATIC", + hidden = false, + ), + title = "My wallet", + subtitle = "XRP Ledger token", + onClick = {}, + onLongTap = {}, + isQuickActionsShown = false, + onQuickActionClick = {}, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt new file mode 100644 index 0000000000..ad733c39cf --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class AddToPortfolioBSContentUM( + val selectedWallet: UserWalletItemUM, + val selectNetworkUM: SelectNetworkUM, + val isScanCardNotificationVisible: Boolean, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt new file mode 100644 index 0000000000..7fa4c11a4a --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class MyPortfolioUM { + + data class Tokens( + val tokens: ImmutableList, + val buttonState: AddButtonState, + val onAddClick: () -> Unit, + ) : MyPortfolioUM() { + + enum class AddButtonState { + Loading, + Available, + Unavailable, + } + } + + data class AddFirstToken( + val onAddClick: () -> Unit, + ) : MyPortfolioUM() + + data object Loading : MyPortfolioUM() + + data object Unavailable : MyPortfolioUM() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt new file mode 100644 index 0000000000..f7db3362b2 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt @@ -0,0 +1,34 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +internal data class PortfolioTokenUM( + val id: String, + val networkId: String, + val iconUrl: String, + val title: String, + val subtitle: String, + val balanceContent: BalanceContent, + val onClick: () -> Unit, + val onLongTap: () -> Unit, + val isQuickActionsShown: Boolean, + val onQuickActionClick: (QuickActionUM) -> Unit, +) { + + // TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement) + @Immutable + sealed class BalanceContent { + data class TokenBalance( // TODO Add stacking ([REDACTED_TASK_KEY] [Markets] Add staking info to portfolio token item) + val balance: String, + val tokenAmount: String, + val hidden: Boolean, + ) : BalanceContent() + + data class Disabled( + val text: TextReference, + ) : BalanceContent() + + data object Loading : BalanceContent() + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt new file mode 100644 index 0000000000..b079b5ce50 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -0,0 +1,30 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R + +@Immutable +internal enum class QuickActionUM( + val title: TextReference, + val description: TextReference, + @DrawableRes val icon: Int, +) { + Buy( + title = resourceReference(R.string.common_buy), + description = resourceReference(R.string.buy_token_description), + icon = R.drawable.ic_plus_24, + ), + Exchange( + title = resourceReference(R.string.common_exchange), + description = resourceReference(R.string.exсhange_token_description), + icon = R.drawable.ic_exchange_vertical_24, + ), + Receive( + title = resourceReference(R.string.common_receive), + description = resourceReference(R.string.receive_token_description), + icon = R.drawable.ic_arrow_down_24, + ), +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt new file mode 100644 index 0000000000..90830679ca --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt @@ -0,0 +1,13 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import kotlinx.collections.immutable.ImmutableList + +internal data class SelectNetworkUM( + val tokenId: String, + val iconUrl: String?, + val tokenName: String, + val tokenCurrencySymbol: String, + val networks: ImmutableList, + val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt new file mode 100644 index 0000000000..2252b38f70 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt @@ -0,0 +1,28 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.ImmutableList + +internal data class TokenActionsBSContent( + val title: String, + val actions: ImmutableList, + val onActionClick: (Action) -> Unit, +) : TangemBottomSheetConfigContent { + + @Immutable + enum class Action( + val text: TextReference, + ) { + CopyAddress(text = resourceReference(R.string.common_copy_address)), + Receive(text = resourceReference(R.string.common_receive)), + Sell(text = resourceReference(R.string.common_sell)), + Buy(text = resourceReference(R.string.common_buy)), + Send(text = resourceReference(R.string.common_send)), + Exchange(text = resourceReference(R.string.common_exchange)), + Stake(text = resourceReference(R.string.common_stake)), + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 517c795db6..3d80ba6721 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -363,8 +363,8 @@ internal class StateBuilder( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), + title = stringReference("//TODO"), + description = stringReference("//TODO"), ), ) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { @@ -1009,8 +1009,8 @@ internal class StateBuilder( warnings.add( 0, SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), + title = stringReference("//TODO"), + description = stringReference("//TODO"), ), ) return uiState.copy( From e6416ab4c6003af89a657d9ebb3d8fe7b4870305 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 20:03:55 +0300 Subject: [PATCH 04/90] Updated on 2026-08-14 --- .../DefaultMarketsTokenDetailsComponent.kt | 26 +++++++++++++++++++ .../impl/model/MarketsTokenDetailsModel.kt | 10 +++++++ .../impl/model/state/TokenNetworksState.kt | 12 +++++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 5 ++++ .../ui/components/TokenMarketDetailsBody.kt | 25 +++++++++++++----- .../api/MarketsPortfolioComponent.kt | 9 ++++--- .../impl/DefaultMarketsPortfolioComponent.kt | 4 +++ .../impl/model/MarketsPortfolioModel.kt | 10 +++++++ 8 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index 402356487a..74451ad79c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -6,24 +6,47 @@ import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel +import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch @Stable internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: MarketsTokenDetailsComponent.Params, @Assisted private val onBack: () -> Unit, + portfolioComponentFactory: MarketsPortfolioComponent.Factory, ) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { private val model: MarketsTokenDetailsModel = getOrCreateModel(params) + private val portfolioComponent = portfolioComponentFactory.create( + context = child("my_portfolio"), + params = MarketsPortfolioComponent.Params(params.token.id), + ) + + init { + componentScope.launch { + model.networksState.collectLatest { + when (it) { + is TokenNetworksState.NetworksAvailable -> portfolioComponent.setTokenNetworks(it.networks) + TokenNetworksState.NoNetworksAvailable -> portfolioComponent.setNoNetworksAvailable() + else -> {} + } + } + } + } + @Composable override fun BottomSheetContent( bottomSheetState: State, @@ -48,6 +71,9 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( state = state, onBackClick = onBack, onHeaderSizeChange = onHeaderSizeChange, + portfolioBlock = { modifier -> + portfolioComponent.Content(modifier) + }, modifier = modifier, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 0f2d2d1465..0e731a5227 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -26,6 +26,7 @@ import com.tangem.features.markets.details.impl.model.formatter.* import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval +import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R @@ -119,6 +120,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) val isVisibleOnScreen = MutableStateFlow(false) + val networksState = MutableStateFlow(TokenNetworksState.Loading) val state = MutableStateFlow( MarketsTokenDetailsUM( @@ -294,6 +296,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } + val networks = result.networks + + networksState.value = if (networks.isNullOrEmpty()) { + TokenNetworksState.NoNetworksAvailable + } else { + TokenNetworksState.NetworksAvailable(networks) + } + chartDataProducer.runTransaction { updateLook { it.copy( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt new file mode 100644 index 0000000000..dbe01ddcdc --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets.details.impl.model.state + +import com.tangem.domain.markets.TokenMarketInfo + +internal sealed class TokenNetworksState { + + data object Loading : TokenNetworksState() + + data object NoNetworksAvailable : TokenNetworksState() + + data class NetworksAvailable(val networks: List) : TokenNetworksState() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index ecfc715abd..001c81cf27 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -51,6 +51,7 @@ internal fun MarketsTokenDetailsContent( state: MarketsTokenDetailsUM, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + portfolioBlock: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, ) { Content( @@ -58,6 +59,7 @@ internal fun MarketsTokenDetailsContent( state = state, onBackClick = onBackClick, onHeaderSizeChange = onHeaderSizeChange, + portfolioBlock = portfolioBlock, ) InfoBottomSheet(config = state.infoBottomSheet) @@ -69,6 +71,7 @@ private fun Content( state: MarketsTokenDetailsUM, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + portfolioBlock: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, ) { val backgroundColor = LocalMainBottomSheetColor.current.value @@ -129,6 +132,7 @@ private fun Content( tokenMarketDetailsBody( state = state.body, + portfolioBlock = portfolioBlock, ) } } @@ -288,6 +292,7 @@ private fun Preview() { ), onHeaderSizeChange = {}, onBackClick = {}, + portfolioBlock = {}, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 40cc2fc2f5..620e182622 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -11,16 +11,31 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData -internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) { +internal fun LazyListScope.tokenMarketDetailsBody( + state: MarketsTokenDetailsUM.Body, + portfolioBlock: @Composable (Modifier) -> Unit, +) { when (state) { MarketsTokenDetailsUM.Body.Loading -> { - loading() + item("description-loading") { + DescriptionPlaceholder(modifier = Modifier.blockPaddings()) + } + + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } + + loadingInfoBlocks() } is MarketsTokenDetailsUM.Body.Content -> { if (state.description != null) { description(state.description) } + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } + infoBlocksList(state.infoBlocks) } is MarketsTokenDetailsUM.Body.Error -> { @@ -106,11 +121,7 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati } } -private fun LazyListScope.loading() { - item("description-loading") { - DescriptionPlaceholder(modifier = Modifier.blockPaddings()) - } - +private fun LazyListScope.loadingInfoBlocks() { item("insights-loading") { InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt index c4217765dd..e23fee1426 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt @@ -3,15 +3,18 @@ package com.tangem.features.markets.portfolio.api import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.markets.TokenMarketInfo import kotlinx.serialization.Serializable @Stable interface MarketsPortfolioComponent : ComposableContentComponent { @Serializable - data class Params( - val tokenId: String, - ) + data class Params(val tokenId: String) + + fun setTokenNetworks(networks: List) + + fun setNoNetworksAvailable() interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt index 9b732e6e42..be951a14a5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio @@ -21,6 +22,9 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( private val model: MarketsPortfolioModel = getOrCreateModel(params) + override fun setTokenNetworks(networks: List) = model.setTokenNetworks(networks) + override fun setNoNetworksAvailable() = model.setNoNetworksAvailable() + @Composable override fun Content(modifier: Modifier) { MyPortfolio( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 75baa3fd50..ba1f20d5c9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject @@ -17,4 +18,13 @@ internal class MarketsPortfolioModel @Inject constructor( @Suppress("UnusedPrivateMember") private val params = paramsContainer.require() + + @Suppress("UnusedPrivateMember") + fun setTokenNetworks(networks: List) { + // TODO [REDACTED_TASK_KEY] + } + + fun setNoNetworksAvailable() { + // TODO [REDACTED_TASK_KEY] + } } \ No newline at end of file From cba5086e704db93fbf7128778219e96648b117ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 18:09:38 +0400 Subject: [PATCH 05/90] Updated on 2026-08-14 --- .../ui/cardsettings/CardSettingsViewModel.kt | 9 ++--- .../feedback/converters/CardInfoConverter.kt | 6 ++-- ...sponseExtensions.kt => ScanResponseExt.kt} | 36 +++++++++++++++++-- .../domain/common/util/UserWalletExt.kt | 17 +++++++++ .../details/utils/UserWalletMappers.kt | 12 ++----- .../wallet/domain/UserWalletExt.kt | 18 ---------- .../domain/WalletAdditionalInfoFactory.kt | 1 + .../wallet/domain/WalletImageResolver.kt | 1 + .../SetBalancesAndLimitsTransformer.kt | 2 +- .../SetTokenListErrorTransformer.kt | 2 +- .../UpdateWalletCardsCountTransformer.kt | 2 +- .../MultiWalletCardStateConverter.kt | 2 +- .../SingleWalletCardStateConverter.kt | 2 +- .../viewmodels/WalletsUpdateActionResolver.kt | 2 +- 14 files changed, 65 insertions(+), 47 deletions(-) rename domain/legacy/src/main/java/com/tangem/domain/common/util/{ScanResponseExtensions.kt => ScanResponseExt.kt} (73%) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 7f972b948d..4abe4b37cc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder @@ -173,13 +174,7 @@ internal class CardSettingsViewModel @Inject constructor( userWalletId = userWalletId, cardId = card.cardId, isActiveBackupStatus = card.backupStatus?.isActive == true, - backupCardsCount = when (val status = card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked, - CardDTO.BackupStatus.NoBackup, - null, - -> 0 - }, + backupCardsCount = scanResponse.getBackupCardsCount() ?: 0, ), ) } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt index cb2081979e..c1855597a2 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -1,6 +1,7 @@ package com.tangem.data.feedback.converters import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -20,10 +21,7 @@ internal object CardInfoConverter : Converter { CardInfo( userWalletId = createUserWalletId(scanResponse = value), cardId = card.cardId, - cardsCount = when (val status = value.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount.toString() - else -> "0" - }, + cardsCount = value.getBackupCardsCount()?.toString() ?: "0", firmwareVersion = card.firmwareVersion.stringValue, cardBlockchain = walletData?.blockchain, signedHashesList = card.wallets.map { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt similarity index 73% rename from domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt rename to domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt index 1c3519f606..94727f02cb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt @@ -12,6 +12,7 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.configs.Wallet2CardConfig +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet @@ -32,8 +33,6 @@ val UserWallet.cardTypesResolver: CardTypesResolver get() = scanResponse.cardTypesResolver fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null -fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed -fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean { return hasDerivation(blockchain, DerivationPath(rawDerivationPath)) @@ -66,4 +65,37 @@ private fun ScanResponse.hasDerivation(curve: EllipticCurve, derivationPath: Der val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false val extendedPublicKey = extendedPublicKeysMap[derivationPath] return extendedPublicKey != null +} + +/** + * Get total cards count in wallets set for this [ScanResponse] card + * + * @return null if wallet is not multi-currency or total cards count + */ +fun ScanResponse.getCardsCount(): Int? { + if (!cardTypesResolver.isMultiwalletAllowed()) return null + + return when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + 1 + is CardDTO.BackupStatus.NoBackup, + is CardDTO.BackupStatus.CardLinked, + null, // Multi-currency wallet without backup function. Example, 4.12 + -> 1 + } +} + +/** + * Get backup cards count for this [ScanResponse] card + * + * @return null if wallet is not multi-currency or total cards count + */ +fun ScanResponse.getBackupCardsCount(): Int? { + return if (cardTypesResolver.isMultiwalletAllowed()) { + when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + else -> 0 + } + } else { + null + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt new file mode 100644 index 0000000000..0b5ef6f2fd --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.common.util + +import com.tangem.domain.wallets.models.UserWallet + +/** + * Get total cards count in wallets set for a card that was saved in [UserWallet] + * + * @return null if wallet is not multi-currency or total cards count + */ +fun UserWallet.getCardsCount(): Int? = scanResponse.getCardsCount() + +/** + * Get backup cards count for a card that was saved in [UserWallet] + * + * @return null if wallet is not multi-currency or total cards count + */ +fun UserWallet.getBackupCardsCount(): Int? = scanResponse.getBackupCardsCount() \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index 1a3efa0cf4..f51b383fbd 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -59,7 +59,7 @@ private fun UserWallet.getInfo( ): TextReference { val dividerRef = stringReference(value = " • ") - val cardCount = getCardCount() + val cardCount = getCardsCount() ?: 1 val cardCountRef = TextReference.PluralRes( id = R.plurals.card_label_card_count, count = cardCount, @@ -99,12 +99,4 @@ private fun getBalanceInfo( } else { combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) } -} - -private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount.inc() - is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc() - is CardDTO.BackupStatus.NoBackup, - null, - -> 1 } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt deleted file mode 100644 index a78ded4bf0..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.wallets.models.UserWallet - -fun UserWallet.getCardsCount(): Int? { - return if (isMultiCurrency) { - when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount + 1 - is CardDTO.BackupStatus.NoBackup, - is CardDTO.BackupStatus.CardLinked, - -> 1 - null -> 1 // Multi-currency wallet without backup function. Example, 4.12 - } - } else { - null - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 1f34e460f2..3de82a88d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 722f7de8f5..2bd5ddb371 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain import androidx.annotation.DrawableRes import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.demo.DemoConfig import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index d42d27f165..d4f7d3db34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -4,9 +4,9 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index fd70fb3cfa..a13bd46297 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 1994185f73..59531e1ece 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import timber.log.Timber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt index 6a914a5c2a..0ff9942daf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index 775957b478..8ea62cf3fe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 26d3d0c945..835a480b48 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -1,10 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import arrow.core.getOrElse +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState From 826eaece1333475f32121364f8e6da2791d174fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:55:10 +0400 Subject: [PATCH 06/90] Updated on 2026-08-14 --- .../components/SettingsAlertDialog.kt | 6 +-- .../components/SettingsSelectorDialog.kt | 4 +- .../details/ui/resetcard/ResetCardScreen.kt | 6 +-- .../EnrollBiometricsDialogConent.kt | 8 +-- .../welcome/ui/components/WarningDialog.kt | 8 +-- .../GiveTxPermisssionBottomSheet.kt | 2 +- .../com/tangem/core/ui/components/Dialogs.kt | 52 ++++++++++--------- .../impl/presentation/ui/SendEventEffect.kt | 12 ++--- .../feature/swap/ui/SwapScreenContent.kt | 2 +- .../ui/components/TokenDetailsDialogs.kt | 6 +-- .../model/WalletSettingsModel.kt | 6 +-- .../walletsettings/ui/RenameWalletDialog.kt | 10 ++-- .../presentation/wallet/ui/WalletAlert.kt | 20 +++---- 13 files changed, 73 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index b7b5b15da2..06e802b5e0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory @@ -20,12 +20,12 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) { title = dialog.title.resolveReference(), message = dialog.description.resolveReference(), isDismissable = false, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = dialog.confirmText.resolveReference(), warning = true, onClick = dialog.onConfirm, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = dialog.onDismiss, ), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt index 41b568b7ed..59af4ac24b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.SelectorDialog import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview @@ -21,7 +21,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { title = dialog.title.resolveReference(), selectedItemIndex = dialog.selectedItemIndex, items = dialog.items.map { it.resolveReference() }.toImmutableList(), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_cancel), onClick = dialog.onDismiss, ), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 1b52a50fd1..6f35968117 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -190,11 +190,11 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) { BasicDialog( title = stringResource(dialog.titleResId), message = stringResource(dialog.messageResId), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = dialog.onDismiss, ), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.card_settings_action_sheet_reset), warning = true, onClick = dialog.onConfirmClick, @@ -208,7 +208,7 @@ private fun CompletedResetDialog(dialog: ResetCardDialog) { BasicDialog( title = stringResource(id = dialog.titleResId), message = stringResource(id = dialog.messageResId), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = dialog.onConfirmClick, ), diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt index 115bd8d273..1f3b36e57d 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt @@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @Composable @@ -19,11 +19,11 @@ fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) { BasicDialog( title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title), message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_enable), onClick = dialog.onEnroll, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( onClick = dialog.onCancel, ), onDismissDialog = dialog.onCancel, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt index a6315d5b1e..57f226a9d7 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.wallet.R @@ -27,7 +27,7 @@ internal fun WarningDialog(warning: WarningModel?) { }, ), onDismissDialog = warning.onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), @@ -38,7 +38,7 @@ internal fun WarningDialog(warning: WarningModel?) { title = stringResource(id = R.string.common_attention), message = stringResource(id = R.string.key_invalidated_warning_description), onDismissDialog = warning.onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), @@ -50,7 +50,7 @@ internal fun WarningDialog(warning: WarningModel?) { message = stringResource(id = R.string.biometric_unavailable_warning), onDismissDialog = warning.onDismiss, isDismissable = false, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 73c62d311c..f10c86f973 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -59,7 +59,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { BasicDialog( message = content.data.dialogText.resolveReference(), title = stringResource(id = R.string.common_approve), - confirmButton = DialogButton { isPermissionAlertShow = false }, + confirmButton = DialogButtonUM { isPermissionAlertShow = false }, onDismissDialog = {}, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 97a3b692cc..e741b087fe 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -49,10 +49,10 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun BasicDialog( message: String, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -72,7 +72,7 @@ fun BasicDialog( fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) { TangemDialog( type = DialogType.Message(message), - confirmButton = DialogButton(onClick = onDismissDialog), + confirmButton = DialogButtonUM(onClick = onDismissDialog), onDismissDialog = onDismissDialog, title = null, dismissButton = null, @@ -97,12 +97,12 @@ fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) { @Composable fun TextInputDialog( fieldValue: TextFieldValue, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, onValueChange: (TextFieldValue) -> Unit, - textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, + textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() }, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -125,12 +125,12 @@ fun TextInputDialog( @Composable fun TextInputDialog( fieldValue: String, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, onValueChange: (String) -> Unit, - textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, + textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() }, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -154,7 +154,7 @@ fun TextInputDialog( fun SelectorDialog( selectedItemIndex: Int, items: ImmutableList, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onSelect: (index: Int) -> Unit, onDismissDialog: () -> Unit, title: String? = null, @@ -180,7 +180,7 @@ fun SelectorDialog( * @param enabled If false button will be disabled * @param onClick Button click callback */ -data class DialogButton( +data class DialogButtonUM( val title: String? = null, val warning: Boolean = false, val enabled: Boolean = true, @@ -190,7 +190,7 @@ data class DialogButton( /** * Additional params for dialog text field */ -data class AdditionalTextInputDialogParams( +data class AdditionalTextInputDialogUM( val label: String? = null, val placeholder: String? = null, val caption: String? = null, @@ -203,10 +203,10 @@ data class AdditionalTextInputDialogParams( @Composable private fun TangemDialog( type: DialogType, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, properties: DialogProperties = DialogProperties(), ) { Dialog(properties = properties, onDismissRequest = onDismissDialog) { @@ -304,7 +304,11 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { } @Composable -private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) { +private fun DialogButtons( + confirmButton: DialogButtonUM, + dismissButton: DialogButtonUM?, + modifier: Modifier = Modifier, +) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy( @@ -413,13 +417,13 @@ private sealed class DialogType { data class TextInput( val value: TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, - val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), + val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(), ) : DialogType() data class SimpleTextInput( val value: String, val onValueChange: (String) -> Unit, - val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), + val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(), ) : DialogType() data class Selector( @@ -445,8 +449,8 @@ private fun BasicDialogPreview() { message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " + "password to work with the app", title = "Attention", - confirmButton = DialogButton {}, - dismissButton = DialogButton {}, + confirmButton = DialogButtonUM {}, + dismissButton = DialogButtonUM {}, onDismissDialog = {}, ) } @@ -478,8 +482,8 @@ private fun WarningBasicDialogSample(modifier: Modifier = Modifier) { message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " + "password to work with the app", title = "Attention", - confirmButton = DialogButton(warning = true) {}, - dismissButton = DialogButton {}, + confirmButton = DialogButtonUM(warning = true) {}, + dismissButton = DialogButtonUM {}, onDismissDialog = {}, ) } @@ -502,10 +506,10 @@ private fun TextInputDialogSample(modifier: Modifier = Modifier) { TextInputDialog( fieldValue = TextFieldValue(text = ""), title = "Rename Wallet", - confirmButton = DialogButton {}, + confirmButton = DialogButtonUM {}, onDismissDialog = {}, onValueChange = {}, - textFieldParams = AdditionalTextInputDialogParams( + textFieldParams = AdditionalTextInputDialogUM( label = "Wallet name", ), ) @@ -530,7 +534,7 @@ private fun SelectorDialogPreview(@PreviewParameter(SelctorDialogParamsProvider: title = param.title, items = param.items, selectedItemIndex = param.selectedItemIndex, - confirmButton = DialogButton(title = "Cancel", onClick = {}), + confirmButton = DialogButtonUM(title = "Cancel", onClick = {}), onSelect = {}, onDismissDialog = {}, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt index 53378ca7f7..dee53dccc8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController 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.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference @@ -45,24 +45,24 @@ internal fun SendEventEffect(event: StateEvent, snackbarHostState: Sn @Composable internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = { onActionClick() onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = onDismiss, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index f3004d1c73..ea5ce81e01 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -110,7 +110,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi BasicDialog( title = state.alert.title?.resolveReference(), message = message, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = state.alert.onClick, ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt index ed54c9459b..5a4f590fe7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -2,7 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components import androidx.compose.runtime.Composable import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig @@ -19,7 +19,7 @@ internal fun TokenDetailsDialogs(state: TokenDetailsState) { private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { BasicDialog( message = config.content.message.resolveReference(), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = config.content.confirmButtonConfig.text.resolveReference(), warning = config.content.confirmButtonConfig.warning, onClick = config.content.confirmButtonConfig.onClick, @@ -27,7 +27,7 @@ private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { onDismissDialog = config.onDismissRequest, title = config.content.title?.resolveReference(), dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> - DialogButton( + DialogButtonUM( title = cancelButtonConfig.text.resolveReference(), warning = cancelButtonConfig.warning, onClick = cancelButtonConfig.onClick, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index a9e5b42932..fcb721457e 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -13,7 +13,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage @@ -93,7 +93,7 @@ internal class WalletSettingsModel @Inject constructor( BasicDialog( message = stringResource(R.string.user_wallet_list_delete_prompt), onDismissDialog = onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_delete), warning = true, onClick = { @@ -101,7 +101,7 @@ internal class WalletSettingsModel @Inject constructor( onDismiss() }, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(R.string.common_cancel), onClick = onDismiss, ), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index 5ff04fe2e7..8cbce3fc72 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -6,8 +6,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.AdditionalTextInputDialogParams -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.AdditionalTextInputDialogUM +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.walletsettings.component.preview.PreviewRenameWalletComponent @@ -21,18 +21,18 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { TextInputDialog( title = stringResource(id = R.string.user_wallet_list_rename_popup_title), fieldValue = value, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), enabled = model.isConfirmEnabled, onClick = model.onConfirm, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ), onDismissDialog = onDismiss, onValueChange = model.updateValue, - textFieldParams = AdditionalTextInputDialogParams( + textFieldParams = AdditionalTextInputDialogUM( label = stringResource(id = R.string.user_wallet_list_rename_popup_placeholder), ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index 48e959ad54..ca9ee260f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.compose.runtime.* import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.AdditionalTextInputDialogParams +import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.impl.R @@ -21,12 +21,12 @@ internal fun WalletAlert(state: WalletAlertState, onDismiss: () -> Unit) { @Composable private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), warning = state.isWarningConfirmButton, onClick = { @@ -34,12 +34,12 @@ private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), warning = state.isWarningConfirmButton, onClick = onDismiss, @@ -62,7 +62,7 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U TextInputDialog( fieldValue = value, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), enabled = value.text.isNotEmpty() && value.text != state.text && @@ -75,8 +75,8 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U onDismissDialog = onDismiss, onValueChange = { value = it }, title = state.title.resolveReference(), - dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), - textFieldParams = AdditionalTextInputDialogParams( + dismissButton = DialogButtonUM(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), + textFieldParams = AdditionalTextInputDialogUM( label = state.label.resolveReference(), isError = state.errorTextProvider(value.text) != null, caption = state.errorTextProvider(value.text)?.resolveReference(), From a83754481784c70c3e6f00341ef334813674c23a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:56:34 +0400 Subject: [PATCH 07/90] Updated on 2026-08-14 --- .../currency/icon/CurrencyIconState.kt | 28 +++++++++++++++++-- .../ui/src/main/res/drawable/ic_tether_24.xml | 14 ++++++++++ .../ui/src/main/res/drawable/ic_tether_28.xml | 17 ----------- .../feature/referral/ui/ReferralScreen.kt | 4 +-- 4 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_tether_24.xml delete mode 100644 core/ui/src/main/res/drawable/ic_tether_28.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 85e49532fc..f19975ab9e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -38,7 +38,7 @@ sealed class CurrencyIconState { * Represents a token icon. * * @property url The URL where the token icon can be fetched from. May be `null` if not found. - * @property topBadgeIconResId The drawable resource ID for the network badge. + * @property topBadgeIconResId The drawable resource ID for the network badge. May be `null`. * @property isGrayscale Specifies whether to show the icon in grayscale. * @property showCustomBadge Specifies whether to show the custom token badge. * @property fallbackTint The color to be used for tinting the fallback icon. @@ -46,7 +46,7 @@ sealed class CurrencyIconState { */ data class TokenIcon( val url: String?, - @DrawableRes override val topBadgeIconResId: Int, + @DrawableRes override val topBadgeIconResId: Int?, override val isGrayscale: Boolean, override val showCustomBadge: Boolean, val fallbackTint: Color, @@ -81,4 +81,28 @@ sealed class CurrencyIconState { override val showCustomBadge: Boolean = false override val topBadgeIconResId: Int? = null } + + fun copySealed( + isGrayscale: Boolean = this.isGrayscale, + showCustomBadge: Boolean = this.showCustomBadge, + topBadgeIconResId: Int? = this.topBadgeIconResId, + ): CurrencyIconState = when (this) { + is CoinIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + ) + is CustomTokenIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + topBadgeIconResId = topBadgeIconResId ?: this.topBadgeIconResId, + ) + is TokenIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + topBadgeIconResId = topBadgeIconResId, + ) + is Loading, + is Locked, + -> this + } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_tether_24.xml b/core/ui/src/main/res/drawable/ic_tether_24.xml new file mode 100644 index 0000000000..3c53fbab8e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tether_24.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_tether_28.xml b/core/ui/src/main/res/drawable/ic_tether_28.xml deleted file mode 100644 index e3f94b5dbb..0000000000 --- a/core/ui/src/main/res/drawable/ic_tether_28.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 7d6614a7aa..9fe33b7bd6 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -195,7 +195,7 @@ private fun ReferralInfo( } is ReferralInfoState.Loading -> { - LoadingCondition(iconResId = R.drawable.ic_tether_28) + LoadingCondition(iconResId = R.drawable.ic_tether_24) SpacerH32() LoadingCondition(iconResId = R.drawable.ic_discount_28) } @@ -211,7 +211,7 @@ private fun Conditions(state: ReferralInfoContentState) { @Composable private fun ConditionForYou(state: ReferralInfoContentState) { - Condition(iconResId = R.drawable.ic_tether_28) { + Condition(iconResId = R.drawable.ic_tether_24) { when (state) { is ReferralInfoState.ParticipantContent -> InfoForYou( award = state.award, From 09f69d5c32ed5be28b6b86c4dae82f6ec1637225 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:58:25 +0400 Subject: [PATCH 08/90] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/home/HomeFragment.kt | 2 +- .../java/com/tangem/tap/features/home/HomeViewModel.kt | 2 +- .../java/com/tangem/tap/routing/utils/ChildFactory.kt | 8 +------- .../main/kotlin/com/tangem/common/routing/AppRoute.kt | 4 ++-- .../managetokens/component/ManageTokensComponent.kt | 4 ++-- features/wallet-settings/impl/build.gradle.kts | 1 + .../preview/PreviewWalletSettingsComponent.kt | 4 ++++ .../feature/walletsettings/utils/ItemsBuilder.kt | 10 ++++++++++ .../wallet/presentation/router/DefaultWalletRouter.kt | 4 ++-- .../wallet/presentation/router/InnerWalletRouter.kt | 2 +- .../viewmodels/intents/WalletContentClickIntents.kt | 2 +- 11 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 508c8a2f4e..292b7b1bf6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -98,7 +98,7 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber { viewModel.onSearchClick() } else { Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } store.dispatch(TokensAction.SetArgs.ReadAccess) } }, diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt index c545f2f83b..0299b8832d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt @@ -61,7 +61,7 @@ internal class HomeViewModel @Inject constructor( analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) store.dispatch(TokensAction.SetArgs.ReadAccess) - store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } } private fun scanCard() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index af1bebc286..6815e3f6c9 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -126,13 +126,7 @@ internal class ChildFactory @Inject constructor( if (manageTokensToggles.isFeatureEnabled) { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = ManageTokensComponent.Params( - mode = if (route.readOnlyContent) { - ManageTokensComponent.Mode.READ_ONLY - } else { - ManageTokensComponent.Mode.MANAGE - }, - ), + params = ManageTokensComponent.Params(route.userWalletId), componentFactory = manageTokensComponentFactory, ) } else { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 6c03778967..4f53e068c8 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -177,8 +177,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( - val readOnlyContent: Boolean, - ) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams { + val userWalletId: UserWalletId? = null, + ) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) } diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index a38a96267c..c327e0ac05 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -2,11 +2,11 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface ManageTokensComponent : ComposableContentComponent { - data class Params(val mode: Mode) + data class Params(val userWalletId: UserWalletId?) - enum class Mode { READ_ONLY, MANAGE, } interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 14885a740b..e472817d13 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Project - API */ implementation(projects.features.walletSettings.api) + implementation(projects.features.manageTokens.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 4dba365556..4c4d0e3f25 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -8,6 +8,7 @@ import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.ui.WalletSettingsScreen import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.features.managetokens.ManageTokensToggles internal class PreviewWalletSettingsComponent : WalletSettingsComponent { @@ -15,6 +16,9 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { popBack = {}, items = ItemsBuilder( router = DummyRouter(), + manageTokensToggles = object : ManageTokensToggles { + override val isFeatureEnabled: Boolean = true + }, ).buildItems( userWalletId = UserWalletId("011"), userWalletName = "My Wallet", diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 941f1c53f2..4a7d294e77 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R +import com.tangem.features.managetokens.ManageTokensToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -17,6 +18,7 @@ import javax.inject.Inject @ComponentScoped internal class ItemsBuilder @Inject constructor( private val router: Router, + private val manageTokensToggles: ManageTokensToggles, ) { @Suppress("LongParameterList") @@ -50,6 +52,14 @@ internal class ItemsBuilder @Inject constructor( id = "card", description = resourceReference(R.string.settings_card_settings_footer), blocks = buildList { + if (manageTokensToggles.isFeatureEnabled) { + BlockUM( + text = resourceReference(R.string.add_tokens_title), + iconRes = R.drawable.ic_tether_24, + onClick = { router.push(AppRoute.ManageTokens(userWalletId)) }, + ).let(::add) + } + if (isLinkMoreCardsAvailable) { BlockUM( text = resourceReference(R.string.details_row_title_create_backup), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index a70acf53cd..d91ca3fb45 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -139,8 +139,8 @@ internal class DefaultWalletRouter( return router.stack.lastOrNull() is AppRoute.Wallet } - override fun openManageTokensScreen() { - router.push(AppRoute.ManageTokens(readOnlyContent = false)) + override fun openManageTokensScreen(userWalletId: UserWalletId) { + router.push(AppRoute.ManageTokens(userWalletId = userWalletId)) } override fun openScanFailedDialog(onTryAgain: () -> Unit) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index ee4229c970..afb3ce979a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -54,7 +54,7 @@ internal interface InnerWalletRouter : WalletRouter { fun isWalletLastScreen(): Boolean /** Open manage tokens screen */ - fun openManageTokensScreen() + fun openManageTokensScreen(userWalletId: UserWalletId) /** Open scan failed dialog */ fun openScanFailedDialog(onTryAgain: () -> Unit) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 5c192d9a44..6b0d74852e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -90,7 +90,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onManageTokensClick() { analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens) reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) - router.openManageTokensScreen() + router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } override fun onOrganizeTokensClick() { From e14e82a284aab4d16cb4c5d0f17d2ef4f203e370 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:59:32 +0400 Subject: [PATCH 09/90] Updated on 2026-08-14 --- .../ui/components/list/InfiniteListHandler.kt | 26 +++++++++++++++++++ .../ui/components/MarketsListLazyColumn.kt | 23 +--------------- 2 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt new file mode 100644 index 0000000000..9d5c548b93 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt @@ -0,0 +1,26 @@ +package com.tangem.core.ui.components.list + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* + +@Composable +fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) { + val loadMore by remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + + lastVisibleItemIndex > totalItemsNumber - buffer + } + } + + val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } + var emitted by remember(totalItemsCount) { mutableStateOf(false) } + + LaunchedEffect(loadMore) { + if (loadMore && !emitted) { + emitted = onLoadMore() + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 0da161d62a..2674b90648 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme @@ -196,26 +197,4 @@ private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { state.visibleIdsChanged(visibleItems) } } -} - -@Composable -fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) { - val loadMore by remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - - lastVisibleItemIndex > totalItemsNumber - buffer - } - } - - val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } - var emitted by remember(totalItemsCount) { mutableStateOf(false) } - - LaunchedEffect(loadMore) { - if (loadMore && !emitted) { - emitted = onLoadMore() - } - } } \ No newline at end of file From 5ef69ea4b65ae1c3fc954ed69cb06471f039f95b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 18:00:50 +0400 Subject: [PATCH 10/90] Updated on 2026-08-14 --- features/manage-tokens/impl/build.gradle.kts | 4 +- .../preview/PreviewManageTokensComponent.kt | 55 +++++++++---------- .../managetokens/entity/CurrencyItemUM.kt | 21 ++++--- .../managetokens/entity/ManageTokensUM.kt | 31 +++++++++-- .../ui/CustomTokenNetworkSelectorContent.kt | 2 +- 5 files changed, 70 insertions(+), 43 deletions(-) diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 1305e90261..681f347d02 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -22,8 +22,10 @@ dependencies { implementation(projects.core.featuretoggles) /* Project - Domain */ - implementation(projects.domain.wallets.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /* AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 980afcb895..89103d7f03 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -9,11 +9,14 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.* +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.ManageTokensTopBarUM +import com.tangem.features.managetokens.entity.ManageTokensUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.ManageTokensScreen import kotlinx.collections.immutable.mutate @@ -47,8 +50,10 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { onActiveChange = ::toggleSearchBar, ), hasChanges = false, - isLoading = false, - onSaveClick = {}, + isInitialBatchLoading = false, + isNextBatchLoading = true, + loadMore = { false }, + saveChanges = {}, ), ) @@ -58,7 +63,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { initItems() } else { items.filter { currency -> - currency.model.name.contains(query, ignoreCase = true) + currency.name.contains(query, ignoreCase = true) }.toPersistentList() } @@ -96,34 +101,28 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { }.toPersistentList() private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( - id = index.toString(), - model = ChainRowUM( - name = "Custom token $index", - type = "CT$index", - icon = CurrencyIconState.CustomTokenIcon( - tint = Color.White, - background = Color.Black, - topBadgeIconResId = R.drawable.img_eth_22, - isGrayscale = false, - showCustomBadge = true, - ), - showCustom = true, + id = ManagedCryptoCurrency.ID(index.toString()), + name = "Custom token $index", + symbol = "CT$index", + icon = CurrencyIconState.CustomTokenIcon( + tint = Color.White, + background = Color.Black, + topBadgeIconResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = true, ), onRemoveClick = {}, ) private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( - id = index.toString(), - model = ChainRowUM( - name = "Currency $index", - type = "C$index", - icon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_btc_22, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, + id = ManagedCryptoCurrency.ID(index.toString()), + name = "Currency $index", + symbol = "C$index", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + showCustomBadge = false, ), networks = if (index == 2) { CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt index 0cfc505545..eec4f853bc 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt @@ -1,18 +1,23 @@ package com.tangem.features.managetokens.entity import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class CurrencyItemUM { - abstract val id: String - abstract val model: ChainRowUM + abstract val id: ManagedCryptoCurrency.ID + abstract val name: String + abstract val symbol: String + abstract val icon: CurrencyIconState data class Basic( - override val id: String, - override val model: ChainRowUM, + override val id: ManagedCryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val icon: CurrencyIconState, val networks: NetworksUM, val onExpandClick: () -> Unit, ) : CurrencyItemUM() { @@ -29,8 +34,10 @@ internal sealed class CurrencyItemUM { } data class Custom( - override val id: String, - override val model: ChainRowUM, + override val id: ManagedCryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val icon: CurrencyIconState, val onRemoveClick: () -> Unit, ) : CurrencyItemUM() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt index d53e7e5e4d..bd8e4f27bf 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt @@ -8,26 +8,32 @@ import kotlinx.collections.immutable.ImmutableList internal sealed class ManageTokensUM { abstract val popBack: () -> Unit - abstract val isLoading: Boolean + abstract val isInitialBatchLoading: Boolean + abstract val isNextBatchLoading: Boolean abstract val items: ImmutableList abstract val topBar: ManageTokensTopBarUM abstract val search: SearchBarUM + abstract val loadMore: () -> Boolean data class ReadContent( override val popBack: () -> Unit, - override val isLoading: Boolean, + override val isInitialBatchLoading: Boolean, + override val isNextBatchLoading: Boolean, override val items: ImmutableList, override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, + override val loadMore: () -> Boolean, ) : ManageTokensUM() data class ManageContent( override val popBack: () -> Unit, - override val isLoading: Boolean, + override val isInitialBatchLoading: Boolean, + override val isNextBatchLoading: Boolean, override val items: ImmutableList, override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, - val onSaveClick: () -> Unit, + override val loadMore: () -> Boolean, + val saveChanges: () -> Unit, val hasChanges: Boolean, ) : ManageTokensUM() @@ -35,10 +41,23 @@ internal sealed class ManageTokensUM { search: SearchBarUM = this.search, items: ImmutableList = this.items, hasChanges: Boolean = this is ManageContent && this.hasChanges, + isInitialBatchLoading: Boolean = this.isInitialBatchLoading, + isNextBatchLoading: Boolean = this.isNextBatchLoading, ): ManageTokensUM { return when (this) { - is ManageContent -> copy(search = search, items = items, hasChanges = hasChanges) - is ReadContent -> copy(search = search, items = items) + is ManageContent -> copy( + search = search, + items = items, + hasChanges = hasChanges, + isInitialBatchLoading = isInitialBatchLoading, + isNextBatchLoading = isNextBatchLoading, + ) + is ReadContent -> copy( + search = search, + items = items, + isInitialBatchLoading = isInitialBatchLoading, + isNextBatchLoading = isNextBatchLoading, + ) } } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt index 57abdb15b5..2363fff9c9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt @@ -102,7 +102,7 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) icon = CurrencyIconState.CoinIcon( url = null, fallbackResId = model.iconResId, - isGrayscale = false, + isGrayscale = !model.isSelected, showCustomBadge = false, ), showCustom = false, From 68842dcadc0fdc2cf689bb2610c616a47f0e4ce0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 18:02:42 +0400 Subject: [PATCH 11/90] Updated on 2026-08-14 --- .../impl/DefaultManageTokensComponent.kt | 3 + .../managetokens/ui/ManageTokensScreen.kt | 212 +++++++++++------- .../ui/dialog/HasLinkedTokensWarning.kt | 30 +++ .../ui/dialog/HideTokenWarning.kt | 29 +++ 4 files changed, 197 insertions(+), 77 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index e89139cf5f..7fc4940bea 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.managetokens.component.impl +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -24,6 +25,8 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + BackHandler(onBack = state.popBack) + ManageTokensScreen( modifier = modifier, state = state, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 3008c84d30..00a647c307 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -1,7 +1,6 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration -import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi @@ -10,6 +9,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon @@ -20,6 +20,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -33,14 +39,17 @@ import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM @@ -51,18 +60,30 @@ import kotlinx.collections.immutable.ImmutableList private const val CHEVRON_ROTATION_EXPANDED = 180f private const val CHEVRON_ROTATION_COLLAPSED = 0f +private const val LOAD_ITEMS_BUFFER = 10 @Composable internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { - BackHandler(onBack = state.popBack) + val keyboardController = LocalSoftwareKeyboardController.current + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + keyboardController?.hide() + + return super.onPreScroll(available, source) + } + } + } Scaffold( - modifier = modifier, + modifier = modifier.nestedScroll(nestedScrollConnection), containerColor = TangemTheme.colors.background.primary, + contentWindowInsets = WindowInsetsZero, topBar = { ManageTokensTopBar( modifier = Modifier.statusBarsPadding(), topBar = state.topBar, + search = state.search, ) }, content = { innerPadding -> @@ -70,10 +91,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi modifier = Modifier .padding(innerPadding) .fillMaxSize(), - search = state.search, - items = state.items, - isLoading = state.isLoading, - hasChanges = state is ManageTokensUM.ManageContent && state.hasChanges, + state = state, ) }, floatingActionButtonPosition = FabPosition.Center, @@ -81,10 +99,11 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi if (state is ManageTokensUM.ManageContent) { SaveChangesButton( modifier = Modifier + .navigationBarsPadding() .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), isVisible = state.hasChanges, - onClick = state.onSaveClick, + onClick = state.saveChanges, ) } }, @@ -92,16 +111,26 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi } @Composable -private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, modifier: Modifier = Modifier) { - TangemTopAppBar( - modifier = modifier, - title = topBar.title.resolveReference(), - startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick), - endButton = when (topBar) { - is ManageTokensTopBarUM.ManageContent -> topBar.endButton - is ManageTokensTopBarUM.ReadContent -> null - }, - ) +private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + TangemTopAppBar( + title = topBar.title.resolveReference(), + startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick), + endButton = when (topBar) { + is ManageTokensTopBarUM.ManageContent -> topBar.endButton + is ManageTokensTopBarUM.ReadContent -> null + }, + ) + SearchBar( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing12) + .padding(horizontal = TangemTheme.dimens.spacing16), + state = search, + ) + } } @Composable @@ -122,80 +151,59 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: } @Composable -private fun LoadingContent() { - Box( - modifier = Modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.primary), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(color = TangemTheme.colors.icon.accent) - } -} - -@Composable -private fun Content( - search: SearchBarUM, - items: ImmutableList, - isLoading: Boolean, - hasChanges: Boolean, - modifier: Modifier = Modifier, -) { +private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { Box(modifier = modifier) { Currencies( modifier = Modifier.fillMaxSize(), - items = items, - search = search, + items = state.items, + showLoadingItem = state.isNextBatchLoading, + onLoadMore = state.loadMore, + isEditable = state is ManageTokensUM.ManageContent, ) - AnimatedVisibility( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth(), - visible = hasChanges, - label = "bottom_fade_visibility", - ) { - BottomFade() - } + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) } - Crossfade(targetState = isLoading, label = "ManageTokensLoadingContent") { - if (it) { - LoadingContent() + Crossfade(targetState = state.isInitialBatchLoading, label = "ManageTokensLoadingContent") { isVisible -> + if (isVisible) { + ProgressIndicator( + modifier = Modifier.fillMaxSize(), + ) } } } @OptIn(ExperimentalFoundationApi::class) @Composable -private fun Currencies(items: ImmutableList, search: SearchBarUM, modifier: Modifier = Modifier) { +private fun Currencies( + items: ImmutableList, + showLoadingItem: Boolean, + isEditable: Boolean, + onLoadMore: () -> Boolean, + modifier: Modifier = Modifier, +) { + val bottomBarHeight = with(LocalDensity.current) { + WindowInsets.systemBars.getBottom(density = this).toDp() + } + val listState = rememberLazyListState() + LazyColumn( modifier = modifier, + state = listState, + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing76 + bottomBarHeight, + ), ) { - stickyHeader(key = "search") { - Column( - modifier = Modifier - .background(TangemTheme.colors.background.primary) - .padding( - top = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, - ) - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) { - SearchBar(state = search) - } - } - items( items = items, - key = CurrencyItemUM::id, + key = { it.id.value }, ) { item -> when (item) { is CurrencyItemUM.Basic -> { BasicCurrencyItem( modifier = Modifier.fillMaxWidth(), item = item, + isEditable = isEditable, ) } is CurrencyItemUM.Custom -> { @@ -206,6 +214,34 @@ private fun Currencies(items: ImmutableList, search: SearchBarUM } } } + + item(key = "loading_indicator") { + AnimatedVisibility( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + visible = showLoadingItem, + label = "loading_indicator_visibility", + ) { + ProgressIndicator() + } + } + } + + InfiniteListHandler( + listState = listState, + buffer = LOAD_ITEMS_BUFFER, + onLoadMore = onLoadMore, + ) +} + +@Composable +private fun ProgressIndicator(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background(color = TangemTheme.colors.background.primary), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = TangemTheme.colors.icon.informative) } } @@ -213,7 +249,14 @@ private fun Currencies(items: ImmutableList, search: SearchBarUM private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = Modifier) { ChainRow( modifier = modifier, - model = item.model, + model = with(item) { + ChainRowUM( + name = name, + type = symbol, + icon = icon, + showCustom = true, + ) + }, action = { SecondarySmallButton( config = SmallButtonConfig( @@ -226,13 +269,20 @@ private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = } @Composable -private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = Modifier) { +private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, isEditable: Boolean, modifier: Modifier = Modifier) { val isExpanded = item.networks is NetworksUM.Expanded Column(modifier = modifier) { ChainRow( modifier = Modifier.clickable(onClick = item.onExpandClick), - model = item.model, + model = with(item) { + ChainRowUM( + name = name, + type = symbol, + icon = icon, + showCustom = false, + ) + }, action = { val rotation by animateFloatAsState( targetValue = if (isExpanded) { @@ -260,13 +310,19 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M end = TangemTheme.dimens.spacing8, ), networks = item.networks, - currencyId = item.id, + currencyId = item.id.value, + isEditable = isEditable, ) } } @Composable -private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) { +private fun NetworksList( + networks: NetworksUM, + currencyId: String, + isEditable: Boolean, + modifier: Modifier = Modifier, +) { AnimatedVisibility( modifier = modifier, visible = networks is NetworksUM.Expanded, @@ -297,10 +353,12 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod ) }, action = { - TangemSwitch( - checked = network.isSelected, - onCheckedChange = network.onSelectedStateChange, - ) + if (isEditable) { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = network.onSelectedStateChange, + ) + } }, ) }, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt new file mode 100644 index 0000000000..848d654e08 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt @@ -0,0 +1,30 @@ +package com.tangem.features.managetokens.ui.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun HasLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network, onDismiss: () -> Unit) { + BasicDialog( + title = stringResource( + R.string.token_details_unable_hide_alert_title, + currency.name, + ), + message = stringResource( + R.string.token_details_unable_hide_alert_message, + currency.name, + currency.symbol, + network.name, + ), + confirmButton = DialogButtonUM( + title = stringResource(R.string.common_ok), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt new file mode 100644 index 0000000000..46b9cc6a39 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt @@ -0,0 +1,29 @@ +package com.tangem.features.managetokens.ui.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun HideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit, onDismiss: () -> Unit) { + BasicDialog( + title = stringResource( + R.string.token_details_hide_alert_title, + currency.name, + ), + message = stringResource(R.string.token_details_hide_alert_message), + confirmButton = DialogButtonUM( + title = stringResource(R.string.token_details_hide_alert_hide), + warning = true, + onClick = onConfirm, + ), + dismissButton = DialogButtonUM( + title = stringResource(R.string.common_cancel), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file From 5b4f0214d28684d5a8712945920499611dea1e84 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 18:03:03 +0400 Subject: [PATCH 12/90] Updated on 2026-08-14 --- .../managetokens/model/ManageTokensModel.kt | 299 +++++++++--------- .../utils/list/ChangedCurrenciesManager.kt | 46 +++ .../utils/list/ManageTokensListManager.kt | 206 ++++++++++++ .../utils/list/ManageTokensListState.kt | 43 +++ .../utils/list/ManageTokensUiManager.kt | 200 ++++++++++++ .../utils/mapper/CurrencyItemMapper.kt | 77 +++++ .../utils/mapper/CurrencyNetworksMapper.kt | 46 +++ .../utils/ui/CurrencyItemOperations.kt | 66 ++++ .../utils/ui/CurrencyNetworkOperations.kt | 21 ++ 9 files changed, 848 insertions(+), 156 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 6141538abd..2d2ce96fe0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -1,53 +1,78 @@ package com.tangem.features.managetokens.model -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.tokens.model.Network +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.* +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.ManageTokensTopBarUM +import com.tangem.features.managetokens.entity.ManageTokensUM import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.list.ChangedCurrencies +import com.tangem.features.managetokens.utils.list.ManageTokensListManager +import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.mutate -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @ComponentScoped internal class ManageTokensModel @Inject constructor( - paramsContainer: ParamsContainer, - private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val manageTokensListManager: ManageTokensListManager, + private val messageSender: UiMessageSender, + paramsContainer: ParamsContainer, ) : Model() { private val params: ManageTokensComponent.Params = paramsContainer.require() - private val changedItemsIds: MutableSet = mutableSetOf() - private var items = initItems() - val state: MutableStateFlow = MutableStateFlow(value = getInitialState(mode = params.mode)) + val state: MutableStateFlow = MutableStateFlow(getInitialState(params.userWalletId)) - private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM { - return when (mode) { - ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel() - ManageTokensComponent.Mode.MANAGE -> createManageContentModel() + init { + manageTokensListManager.uiItems + .onEach { items -> updateItems(items) } + .launchIn(modelScope) + + manageTokensListManager.paginationStatus + .onEach { status -> updatePaginationStatus(status) } + .launchIn(modelScope) + + combine( + manageTokensListManager.currenciesToAdd, + manageTokensListManager.currenciesToRemove, + ::updateChangedItems, + ).launchIn(modelScope) + + modelScope.launch { + manageTokensListManager.launch(params.userWalletId) + } + } + + private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { + return if (userWalletId == null) { + createReadContentModel() + } else { + createManageContentModel() } } private fun createReadContentModel(): ManageTokensUM.ReadContent { return ManageTokensUM.ReadContent( popBack = router::pop, - isLoading = false, - items = initItems(), + isInitialBatchLoading = true, + isNextBatchLoading = false, + items = getInitialItems(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_search_tokens), onBackButtonClick = router::pop, @@ -59,14 +84,16 @@ internal class ManageTokensModel @Inject constructor( isActive = false, onActiveChange = ::toggleSearchBar, ), + loadMore = ::loadMoreItems, ) } private fun createManageContentModel(): ManageTokensUM.ManageContent { return ManageTokensUM.ManageContent( popBack = router::pop, - isLoading = false, - items = initItems(), + isInitialBatchLoading = true, + isNextBatchLoading = false, + items = getInitialItems(), topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, @@ -82,11 +109,85 @@ internal class ManageTokensModel @Inject constructor( isActive = false, onActiveChange = ::toggleSearchBar, ), - onSaveClick = ::onSaveClick, hasChanges = false, + saveChanges = ::onSaveClick, + loadMore = ::loadMoreItems, ) } + private fun updateItems(items: ImmutableList) { + state.update { state -> + state.copySealed( + items = items, + ) + } + } + + private fun updatePaginationStatus(status: PaginationStatus<*>) { + state.update { state -> + when (status) { + is PaginationStatus.InitialLoading -> { + if (state.search.isActive) { + state + } else { + state.copySealed( + isInitialBatchLoading = true, + ) + } + } + is PaginationStatus.NextBatchLoading -> state.copySealed( + isNextBatchLoading = true, + ) + is PaginationStatus.InitialLoadingError -> { + val message = SnackbarMessage( + message = status.throwable.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + is PaginationStatus.None, + is PaginationStatus.Paginating, + is PaginationStatus.EndOfPagination, + -> state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + } + } + + private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { + state.update { state -> + state.copySealed( + hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), + ) + } + } + + private fun loadMoreItems(): Boolean { + val state = state.value + if (state.isInitialBatchLoading) return false + + modelScope.launch { + manageTokensListManager.loadMore( + userWalletId = params.userWalletId, + query = state.search.query, + ) + } + + return true + } + + private fun getInitialItems(): ImmutableList { + return persistentListOf() + } + private fun onAddCustomToken() { // TODO: [REDACTED_JIRA] } @@ -95,149 +196,35 @@ internal class ManageTokensModel @Inject constructor( // TODO: [REDACTED_JIRA] } - @Suppress("UnusedPrivateMember") private fun searchCurrencies(query: String) { - // TODO: [REDACTED_JIRA] - val newItems = if (query.isBlank()) { - initItems() - } else { - state.value.items.filter { currency -> - currency.model.name.contains(query, ignoreCase = true) - }.toPersistentList() - } state.update { state -> - state.copySealed(search = state.search.copy(query = query), items = newItems) + state.copySealed( + search = state.search.copy( + query = query, + isActive = true, + ), + ) + } + + modelScope.launch { + manageTokensListManager.search(params.userWalletId, query) } } private fun toggleSearchBar(isActive: Boolean) { state.update { state -> state.copySealed( - search = state.search.copy(isActive = isActive), + search = state.search.copy( + query = if (isActive) state.search.query else "", + isActive = isActive, + ), ) } - } - private fun initItems() = List(size = 30) { index -> - if (index < 2) { - getCustomItem(index) - } else { - getBasicItem(index) - } - }.toPersistentList() - - private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( - id = index.toString(), - model = ChainRowUM( - name = "Custom token $index", - type = "CT$index", - icon = CurrencyIconState.CustomTokenIcon( - tint = Color.White, - background = Color.Black, - topBadgeIconResId = R.drawable.img_eth_22, - isGrayscale = false, - showCustomBadge = true, - ), - showCustom = true, - ), - onRemoveClick = {}, - ) - - private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( - id = index.toString(), - model = ChainRowUM( - name = "Currency $index", - type = "C$index", - icon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_btc_22, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, - ), - networks = if (index == 2) { - CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) - } else { - CurrencyItemUM.Basic.NetworksUM.Collapsed - }, - onExpandClick = { toggleCurrency(index) }, - ) - - private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> - CurrencyNetworkUM( - id = Network.ID(networkIndex.toString()), - name = "NETWORK$networkIndex", - type = "N$networkIndex", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = networkIndex == 0, - isSelected = false, - onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, - ) - }.toImmutableList() - - private fun toggleCurrency(index: Int) { - val updatedItem = when (val item = items[index]) { - is CurrencyItemUM.Basic -> item.copy( - networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) { - CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) - } else { - CurrencyItemUM.Basic.NetworksUM.Collapsed - }, - ) - is CurrencyItemUM.Custom -> return - } - - state.update { state -> - items = items.mutate { - it[index] = updatedItem + modelScope.launch { + if (!isActive) { + manageTokensListManager.reload(params.userWalletId) } - state.copySealed(items = items) - } - } - - private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) { - val updatedItem = when (val item = items[currencyIndex]) { - is CurrencyItemUM.Basic -> { - val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) - ?.copy( - networks = item.networks.networks.toPersistentList().mutate { - it.fastForEachIndexed { index, network -> - if (index == networkIndex) { - it[index] = network.copy( - iconResId = if (isSelected) { - R.drawable.img_eth_22 - } else { - R.drawable.ic_eth_16 - }, - isSelected = isSelected, - ) - } - } - }, - ) - ?: return - - item.copy(networks = updatedNetworks) - } - is CurrencyItemUM.Custom -> return - } - - val id = "${currencyIndex}_$networkIndex" - if (changedItemsIds.contains(id)) { - changedItemsIds.remove(id) - } else { - changedItemsIds.add(id) - } - - state.update { state -> - items = items.mutate { - it[currencyIndex] = updatedItem - } - state.copySealed( - items = items, - hasChanges = changedItemsIds.isNotEmpty(), - ) } } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt new file mode 100644 index 0000000000..c991cf95bb --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt @@ -0,0 +1,46 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal typealias ChangedCurrencies = Map> + +internal interface ChangedCurrenciesManager { + + val currenciesToAdd: MutableStateFlow + val currenciesToRemove: MutableStateFlow + + fun updateChangedItems( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + removeFromIfPresent: MutableStateFlow, + addToIfNotPresent: MutableStateFlow, + ) { + val present = removeFromIfPresent.value[currencyId].orEmpty() + + if (networkId in present) { + removeFromIfPresent.update { items -> + items.toMutableMap().apply { + val ids = present - networkId + + if (ids.isEmpty()) { + remove(currencyId) + } else { + set(currencyId, ids) + } + } + } + } else { + addToIfNotPresent.update { items -> + val alreadyAdded = items[currencyId] ?: emptySet() + if (networkId in alreadyAdded) { + return@update items + } + + items + (currencyId to alreadyAdded + networkId) + } + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt new file mode 100644 index 0000000000..ebf3369bce --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -0,0 +1,206 @@ +package com.tangem.features.managetokens.utils.list + +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.CheckHasLinkedTokensUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.impl.R +import com.tangem.pagination.BatchAction +import com.tangem.pagination.BatchListState +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class ManageTokensListManager @Inject constructor( + private val getManagedTokensUseCase: GetManagedTokensUseCase, + private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, + private val messageSender: UiMessageSender, + private val dispatchers: CoroutineDispatcherProvider, +) : ChangedCurrenciesManager, + ManageTokensUiManager( + messageSender = messageSender, + dispatchers = dispatchers, + ) { + + override lateinit var scope: CoroutineScope + + private val jobHolder = JobHolder() + private val actionsFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + override val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) + + val paginationStatus: MutableStateFlow> = MutableStateFlow(PaginationStatus.None) + + override val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) + override val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) + + @OptIn(ExperimentalCoroutinesApi::class) + val uiItems: Flow> = state + .mapLatest { state -> + state.uiBatches.asSequence() + .flatMap { it.data } + .toImmutableList() + } + .distinctUntilChanged() + + suspend fun launch(userWalletId: UserWalletId?) = coroutineScope { + scope = this + + val batchFlow = getManagedTokensUseCase( + context = ManageTokensListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = this, + ), + ) + + batchFlow.state + .onEach { state -> updateState(state, userWalletId) } + .flowOn(dispatchers.default) + .launchIn(scope = this) + .saveIn(jobHolder) + + // Initial load + reload(userWalletId) + } + + suspend fun reload(userWalletId: UserWalletId?) { + actionsFlow.emit( + BatchAction.Reload( + requestParams = ManageTokensListConfig(userWalletId, searchText = null), + ), + ) + } + + suspend fun loadMore(userWalletId: UserWalletId?, query: String) { + actionsFlow.emit( + BatchAction.LoadMore( + requestParams = ManageTokensListConfig(userWalletId, query), + ), + ) + } + + suspend fun search(userWalletId: UserWalletId?, query: String) { + state.value = ManageTokensListState() + actionsFlow.emit( + BatchAction.Reload( + requestParams = ManageTokensListConfig( + userWalletId = userWalletId, + searchText = query, + ), + ), + ) + } + + private fun updateState( + batchListState: BatchListState>, + userWalletId: UserWalletId?, + ) { + paginationStatus.value = batchListState.status + + state.update { state -> + val newBatches = batchListState.data + val currentBatches = state.currencyBatches + + // Distinct until changed + if (newBatches.size == currentBatches.size && + newBatches.map { it.key } == currentBatches.map { it.key } && + newBatches.flatMap { it.data } == currentBatches.flatMap { it.data } + ) { + return + } + + val canEditItems = userWalletId != null + state.copy( + userWalletId = userWalletId, + currencyBatches = newBatches, + uiBatches = getUiBatches(newBatches, canEditItems), + canEditItems = canEditItems, + ) + } + } + + override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + + sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true) + } + + override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + + sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false) + } + + override fun checkNeedToShowRemoveNetworkWarning( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + ): Boolean = networkId !in currenciesToRemove.value[currencyId].orEmpty() && + networkId !in currenciesToAdd.value[currencyId].orEmpty() + + private fun sendSelectCurrencyAction( + batchKey: Int, + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + isSelected: Boolean, + ) { + val request = ManageTokensUpdateAction.AddCurrency( + currencyId = currencyId, + networkId = networkId, + isSelected = isSelected, + ) + val action = BatchAction.UpdateBatches( + keys = setOf(batchKey), + async = true, + updateRequest = request, + ) + + actionsFlow.tryEmit(action) + } + + override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean { + return checkHasLinkedTokensUseCase(userWalletId, network).getOrElse { + Timber.e( + it, + """ + Failed to check linked tokens + |- User wallet ID: $userWalletId + |- Network ID: ${network.id} + """.trimIndent(), + ) + + val message = SnackbarMessage( + message = it.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + false + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt new file mode 100644 index 0000000000..20ba2a7e55 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt @@ -0,0 +1,43 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction + +internal typealias ManageTokensBatchAction = BatchAction + +internal data class ManageTokensListState( + val userWalletId: UserWalletId? = null, + val uiBatches: List>> = mutableListOf(), + val currencyBatches: List>> = mutableListOf(), + val canEditItems: Boolean = true, +) { + + fun batchIndexByCurrencyId(currencyId: ManagedCryptoCurrency.ID): Int { + return currencyBatches + .indexOfFirst { batch -> batch.data.any { it.id == currencyId } } + .takeIf { it != -1 } + ?: error("Batch with currency '$currencyId' not found") + } + + fun updateUiBatchesItem( + indexToBatch: Pair>>, + indexToItem: Pair, + ): ManageTokensListState { + val updatedUiBatch = indexToBatch.second.copy( + data = indexToBatch.second.data.toMutableList().apply { + set(indexToItem.first, indexToItem.second) + }, + ) + + return copy( + uiBatches = uiBatches.toMutableList().apply { + set(indexToBatch.first, updatedUiBatch) + }, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt new file mode 100644 index 0000000000..f52bd4970d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -0,0 +1,200 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning +import com.tangem.features.managetokens.ui.dialog.HideTokenWarning +import com.tangem.features.managetokens.utils.mapper.toUiModel +import com.tangem.features.managetokens.utils.ui.toggleExpanded +import com.tangem.features.managetokens.utils.ui.update +import com.tangem.pagination.Batch +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal abstract class ManageTokensUiManager( + private val messageSender: UiMessageSender, + private val dispatchers: CoroutineDispatcherProvider, +) { + + abstract val scope: CoroutineScope + abstract val state: MutableStateFlow + + protected fun getUiBatches( + newCurrencyBatches: List>>, + canEditItems: Boolean, + ): List>> { + val currentUiBatches = state.value.uiBatches + val batches = currentUiBatches.toMutableList() + + newCurrencyBatches.forEach { (key, data) -> + val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key } + + if (indexToUpdate == -1) { + val newBatch = Batch( + key = key, + data = data.map { item -> + item.toUiModel( + isEditable = canEditItems, + onRemoveCustomCurrencyClick = ::removeCustomCurrency, + onExpandNetworksClick = ::toggleCurrencyNetworksVisibility, + ) + }, + ) + + batches.add(newBatch) + } else { + val uiBatchToUpdate = currentUiBatches[indexToUpdate] + + if (uiBatchToUpdate.data == data) { + return@forEach + } + + val currentCurrencyBatches = state.value.currencyBatches + val currencyBatch = currentCurrencyBatches[indexToUpdate] + val updatedBatch = uiBatchToUpdate.copy( + data = data.mapIndexed { index, item -> + if (item == currencyBatch.data[index]) { + return@mapIndexed uiBatchToUpdate.data[index] + } + + val previousUiItem = uiBatchToUpdate.data.getOrNull(index) + if (previousUiItem == null || previousUiItem.id != item.id) { + item.toUiModel( + isEditable = canEditItems, + onRemoveCustomCurrencyClick = ::removeCustomCurrency, + onExpandNetworksClick = ::toggleCurrencyNetworksVisibility, + ) + } else { + previousUiItem.update(item) + } + }, + ) + + batches[indexToUpdate] = updatedBatch + } + } + + return batches + } + + private fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) = scope.launch(dispatchers.default) { + showRemoveNetworkWarning( + currency = currency, + network = currency.network, + isCoin = currency is ManagedCryptoCurrency.Custom.Coin, + onConfirm = { + // TODO: [REDACTED_JIRA] + }, + ) + } + + private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch( + dispatchers.default, + ) { + state.update { batches -> + val batchIndex = batches.batchIndexByCurrencyId(currency.id) + val currencyBatch = batches.currencyBatches[batchIndex] + val currencyIndex = currencyBatch.currencyIndexById(currency.id) + + val uiBatch = batches.uiBatches[batchIndex] + val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded( + currency = currencyBatch.data[currencyIndex], + isEditable = batches.canEditItems, + onSelectCurrencyNetwork = { networkId, isSelected -> + selectNetwork(currencyBatch.key, currency, networkId, isSelected) + }, + ) + + batches.updateUiBatchesItem( + indexToBatch = batchIndex to uiBatch, + indexToItem = currencyIndex to updatedUiItem, + ) + } + } + + private fun selectNetwork( + batchKey: Int, + currency: ManagedCryptoCurrency, + source: ManagedCryptoCurrency.SourceNetwork, + isSelected: Boolean, + ) = scope.launch(dispatchers.default) { + if (currency !is ManagedCryptoCurrency.Token) return@launch + + if (isSelected) { + addCurrency(batchKey, currency.id, source.id) + } else { + if (checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { + showRemoveNetworkWarning( + currency = currency, + network = source.network, + isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, + onConfirm = { + removeCurrency(batchKey, currency.id, source.id) + }, + ) + } else { + removeCurrency(batchKey, currency.id, source.id) + } + } + } + + protected abstract fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + protected abstract fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + protected abstract fun checkNeedToShowRemoveNetworkWarning( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + ): Boolean + + private suspend fun showRemoveNetworkWarning( + currency: ManagedCryptoCurrency, + network: Network, + isCoin: Boolean, + onConfirm: () -> Unit, + ) { + val userWalletId = state.value.userWalletId + val hasLinkedTokens = if (userWalletId == null || !isCoin) { + false + } else { + checkHasLinkedTokens(userWalletId, network) + } + + val message = ContentMessage { onDismiss -> + if (hasLinkedTokens) { + HasLinkedTokensWarning( + currency = currency, + network = network, + onDismiss = onDismiss, + ) + } else { + HideTokenWarning( + currency = currency, + onConfirm = { + onConfirm() + onDismiss() + }, + onDismiss = onDismiss, + ) + } + } + + messageSender.send(message) + } + + protected abstract suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean + + private fun Batch>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int { + return data + .indexOfFirst { it.id == id } + .takeIf { it != -1 } + ?: error("Currency with currency '$id' not found in batch #$key") + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt new file mode 100644 index 0000000000..f2bbfb18be --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt @@ -0,0 +1,77 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.utils.ui.getIconRes + +internal fun ManagedCryptoCurrency.toUiModel( + isEditable: Boolean, + onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit, + onRemoveCustomCurrencyClick: (ManagedCryptoCurrency.Custom) -> Unit, +): CurrencyItemUM = when (this) { + is ManagedCryptoCurrency.Custom -> toUiModel(onRemoveCustomCurrencyClick) + is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onExpandNetworksClick) +} + +private fun ManagedCryptoCurrency.Custom.toUiModel( + onRemoveCustomCurrency: (ManagedCryptoCurrency.Custom) -> Unit, +): CurrencyItemUM = CurrencyItemUM.Custom( + id = id, + name = name, + symbol = symbol, + icon = when (this) { + is ManagedCryptoCurrency.Custom.Coin -> { + CurrencyIconState.CoinIcon( + url = iconUrl, + fallbackResId = network.id.getIconRes(isColored = true), + isGrayscale = false, + showCustomBadge = true, + ) + } + is ManagedCryptoCurrency.Custom.Token -> { + val background = tryGetBackgroundForTokenIcon(contractAddress) + + CurrencyIconState.TokenIcon( + url = iconUrl, + fallbackBackground = background, + fallbackTint = getTintForTokenIcon(background), + topBadgeIconResId = network.id.getIconRes(isColored = true), + isGrayscale = false, + showCustomBadge = true, + ) + } + }, + onRemoveClick = { + onRemoveCustomCurrency(this) + }, +) + +private fun ManagedCryptoCurrency.Token.toUiModel( + isEditable: Boolean, + onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit, +): CurrencyItemUM { + val background = TangemColorPalette.Black + + return CurrencyItemUM.Basic( + id = id, + name = name, + symbol = symbol, + icon = CurrencyIconState.TokenIcon( + url = iconUrl, + topBadgeIconResId = null, + isGrayscale = if (isEditable) !isAdded else false, + showCustomBadge = false, + fallbackTint = getTintForTokenIcon(background), + fallbackBackground = background, + ), + networks = NetworksUM.Collapsed, + onExpandClick = { + onExpandNetworksClick(this) + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt new file mode 100644 index 0000000000..7b210d5b19 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -0,0 +1,46 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.utils.ui.getIconRes +import kotlinx.collections.immutable.toImmutableList + +internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( + isExpanded: Boolean, + isItemsEditable: Boolean, + onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, +): NetworksUM { + return if (isExpanded) { + NetworksUM.Expanded( + networks = availableNetworks.map { + it.toUiModel( + isSelected = it.id in addedIn, + isEditable = isItemsEditable, + onSelectedStateChange = onSelectedStateChange, + ) + }.toImmutableList(), + ) + } else { + NetworksUM.Collapsed + } +} + +private fun SourceNetwork.toUiModel( + isSelected: Boolean, + isEditable: Boolean, + onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, +): CurrencyNetworkUM { + return CurrencyNetworkUM( + id = id, + name = network.name.uppercase(), + iconResId = id.getIconRes(isColored = isSelected || !isEditable), + isSelected = isSelected || !isEditable, + type = typeName, + isMainNetwork = this is SourceNetwork.Main, + onSelectedStateChange = { selected -> + onSelectedStateChange(this, selected) + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt new file mode 100644 index 0000000000..20e1c72a6e --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt @@ -0,0 +1,66 @@ +package com.tangem.features.managetokens.utils.ui + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.utils.mapper.toUiNetworksModel +import kotlinx.collections.immutable.toImmutableList + +internal fun CurrencyItemUM.toggleExpanded( + currency: ManagedCryptoCurrency, + isEditable: Boolean, + onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit, +): CurrencyItemUM { + if (currency !is ManagedCryptoCurrency.Token) return this + + return when (this) { + is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Basic -> { + val isExpanded = networks !is NetworksUM.Expanded + + copy( + icon = icon.copySealed( + isGrayscale = if (isEditable) !currency.isAdded && !isExpanded else false, + ), + networks = currency.toUiNetworksModel( + isExpanded = isExpanded, + isItemsEditable = isEditable, + onSelectedStateChange = onSelectCurrencyNetwork, + ), + ) + } + } +} + +internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyItemUM { + return when (this) { + is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Basic -> { + if (currency !is ManagedCryptoCurrency.Token) { + return this + } + + copy( + icon = icon.copySealed( + isGrayscale = networks is NetworksUM.Collapsed && !currency.isAdded, + ), + networks = updateNetworks(currency), + ) + } + } +} + +private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency.Token): NetworksUM = when (networks) { + is NetworksUM.Collapsed -> networks + is NetworksUM.Expanded -> networks.copy( + networks = networks.networks.map { network -> + val isSelected = network.id in currency.addedIn + + network.copy( + iconResId = network.id.getIconRes(isSelected), + isSelected = isSelected, + ) + }.toImmutableList(), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt new file mode 100644 index 0000000000..290c1a756f --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt @@ -0,0 +1,21 @@ +package com.tangem.features.managetokens.utils.ui + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.entity.CurrencyNetworkUM + +internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM { + return copy( + iconResId = id.getIconRes(isSelected), + isSelected = isSelected, + ) +} + +@DrawableRes +internal fun Network.ID.getIconRes(isColored: Boolean): Int = if (isColored) { + getActiveIconRes(value) +} else { + getGreyedOutIconRes(value) +} \ No newline at end of file From fe09323d6459bfb14cf5098ecf740fd89a107a77 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 13:09:15 +0400 Subject: [PATCH 13/90] Updated on 2026-08-14 --- .../managetokens/model/ManageTokensModel.kt | 13 ++--- .../managetokens/ui/ManageTokensScreen.kt | 18 +++---- .../utils/list/ChangedCurrenciesManager.kt | 21 ++++++-- .../utils/list/ManageTokensListManager.kt | 53 ++++++++++--------- .../utils/list/ManageTokensListState.kt | 2 + .../utils/list/ManageTokensUiActions.kt | 16 ++++++ .../utils/list/ManageTokensUiManager.kt | 49 +++++++++-------- 7 files changed, 103 insertions(+), 69 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 2d2ce96fe0..c56ae2b5b9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -55,7 +55,7 @@ internal class ManageTokensModel @Inject constructor( ).launchIn(modelScope) modelScope.launch { - manageTokensListManager.launch(params.userWalletId) + manageTokensListManager.launchPagination(params.userWalletId) } } @@ -110,7 +110,7 @@ internal class ManageTokensModel @Inject constructor( onActiveChange = ::toggleSearchBar, ), hasChanges = false, - saveChanges = ::onSaveClick, + saveChanges = ::saveChanges, loadMore = ::loadMoreItems, ) } @@ -126,7 +126,9 @@ internal class ManageTokensModel @Inject constructor( private fun updatePaginationStatus(status: PaginationStatus<*>) { state.update { state -> when (status) { - is PaginationStatus.InitialLoading -> { + is PaginationStatus.None, + is PaginationStatus.InitialLoading, + -> { if (state.search.isActive) { state } else { @@ -151,7 +153,6 @@ internal class ManageTokensModel @Inject constructor( isNextBatchLoading = false, ) } - is PaginationStatus.None, is PaginationStatus.Paginating, is PaginationStatus.EndOfPagination, -> state.copySealed( @@ -172,7 +173,7 @@ internal class ManageTokensModel @Inject constructor( private fun loadMoreItems(): Boolean { val state = state.value - if (state.isInitialBatchLoading) return false + if (state.isInitialBatchLoading || state.isNextBatchLoading) return false modelScope.launch { manageTokensListManager.loadMore( @@ -192,7 +193,7 @@ internal class ManageTokensModel @Inject constructor( // TODO: [REDACTED_JIRA] } - private fun onSaveClick() { + private fun saveChanges() { // TODO: [REDACTED_JIRA] } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 00a647c307..2104369a82 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -3,7 +3,6 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -173,7 +172,6 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun Currencies( items: ImmutableList, @@ -215,15 +213,13 @@ private fun Currencies( } } - item(key = "loading_indicator") { - AnimatedVisibility( - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - visible = showLoadingItem, - label = "loading_indicator_visibility", - ) { - ProgressIndicator() + if (showLoadingItem) { + item(key = "loading_item") { + ProgressIndicator( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt index c991cf95bb..3e2705f9e6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt @@ -7,12 +7,25 @@ import kotlinx.coroutines.flow.update internal typealias ChangedCurrencies = Map> -internal interface ChangedCurrenciesManager { +internal class ChangedCurrenciesManager { - val currenciesToAdd: MutableStateFlow - val currenciesToRemove: MutableStateFlow + val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) + val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) - fun updateChangedItems( + fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + } + + fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + } + + fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean { + return networkId in currenciesToAdd.value[currencyId].orEmpty() || + networkId in currenciesToRemove.value[currencyId].orEmpty() + } + + private fun updateChangedItems( currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID, removeFromIfPresent: MutableStateFlow, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index ebf3369bce..df07878c62 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -19,11 +19,11 @@ import com.tangem.features.managetokens.impl.R import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchListState import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow @@ -38,37 +38,37 @@ internal class ManageTokensListManager @Inject constructor( private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, -) : ChangedCurrenciesManager, - ManageTokensUiManager( - messageSender = messageSender, - dispatchers = dispatchers, - ) { +) : ManageTokensUiActions { - override lateinit var scope: CoroutineScope + private lateinit var scope: CoroutineScope private val jobHolder = JobHolder() - private val actionsFlow = MutableSharedFlow( + private val actionsFlow: MutableSharedFlow = MutableSharedFlow( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - override val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) + private val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) - val paginationStatus: MutableStateFlow> = MutableStateFlow(PaginationStatus.None) + private val changedCurrenciesManager = ChangedCurrenciesManager() + private val uiManager = ManageTokensUiManager( + state = state, + messageSender = messageSender, + dispatchers = dispatchers, + actions = this, + scopeProvider = Provider { scope }, + ) - override val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) - override val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) + val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd + val currenciesToRemove: StateFlow = changedCurrenciesManager.currenciesToRemove @OptIn(ExperimentalCoroutinesApi::class) - val uiItems: Flow> = state - .mapLatest { state -> - state.uiBatches.asSequence() - .flatMap { it.data } - .toImmutableList() - } + val paginationStatus: Flow> = state + .mapLatest { it.status } .distinctUntilChanged() + val uiItems: Flow> = uiManager.items - suspend fun launch(userWalletId: UserWalletId?) = coroutineScope { + suspend fun launchPagination(userWalletId: UserWalletId?) = coroutineScope { scope = this val batchFlow = getManagedTokensUseCase( @@ -120,7 +120,11 @@ internal class ManageTokensListManager @Inject constructor( batchListState: BatchListState>, userWalletId: UserWalletId?, ) { - paginationStatus.value = batchListState.status + state.update { state -> + state.copy( + status = batchListState.status, + ) + } state.update { state -> val newBatches = batchListState.data @@ -138,20 +142,20 @@ internal class ManageTokensListManager @Inject constructor( state.copy( userWalletId = userWalletId, currencyBatches = newBatches, - uiBatches = getUiBatches(newBatches, canEditItems), + uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), canEditItems = canEditItems, ) } } override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + changedCurrenciesManager.addCurrency(currencyId, networkId) sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true) } override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + changedCurrenciesManager.removeCurrency(currencyId, networkId) sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false) } @@ -159,8 +163,7 @@ internal class ManageTokensListManager @Inject constructor( override fun checkNeedToShowRemoveNetworkWarning( currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID, - ): Boolean = networkId !in currenciesToRemove.value[currencyId].orEmpty() && - networkId !in currenciesToAdd.value[currencyId].orEmpty() + ): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId) private fun sendSelectCurrencyAction( batchKey: Int, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt index 20ba2a7e55..ac6732fd19 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt @@ -7,10 +7,12 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.pagination.Batch import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus internal typealias ManageTokensBatchAction = BatchAction internal data class ManageTokensListState( + val status: PaginationStatus<*> = PaginationStatus.None, val userWalletId: UserWalletId? = null, val uiBatches: List>> = mutableListOf(), val currencyBatches: List>> = mutableListOf(), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt new file mode 100644 index 0000000000..9d73ea1e76 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -0,0 +1,16 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +internal interface ManageTokensUiActions { + + fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean + + suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index f52bd4970d..ffb78bccf2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -4,7 +4,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning import com.tangem.features.managetokens.ui.dialog.HideTokenWarning @@ -12,21 +11,36 @@ import com.tangem.features.managetokens.utils.mapper.toUiModel import com.tangem.features.managetokens.utils.ui.toggleExpanded import com.tangem.features.managetokens.utils.ui.update import com.tangem.pagination.Batch +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -internal abstract class ManageTokensUiManager( +internal class ManageTokensUiManager( + private val state: MutableStateFlow, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, + private val scopeProvider: Provider, + private val actions: ManageTokensUiActions, ) { - abstract val scope: CoroutineScope - abstract val state: MutableStateFlow + private val scope: CoroutineScope + get() = scopeProvider() - protected fun getUiBatches( + @OptIn(ExperimentalCoroutinesApi::class) + val items: Flow> = state + .mapLatest { state -> + state.uiBatches.asSequence() + .flatMap { it.data } + .toImmutableList() + } + .distinctUntilChanged() + + fun createOrUpdateUiBatches( newCurrencyBatches: List>>, canEditItems: Boolean, ): List>> { @@ -128,32 +142,23 @@ internal abstract class ManageTokensUiManager( if (currency !is ManagedCryptoCurrency.Token) return@launch if (isSelected) { - addCurrency(batchKey, currency.id, source.id) + actions.addCurrency(batchKey, currency.id, source.id) } else { - if (checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { + if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { showRemoveNetworkWarning( currency = currency, network = source.network, isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, onConfirm = { - removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency.id, source.id) }, ) } else { - removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency.id, source.id) } } } - protected abstract fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) - - protected abstract fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) - - protected abstract fun checkNeedToShowRemoveNetworkWarning( - currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, - ): Boolean - private suspend fun showRemoveNetworkWarning( currency: ManagedCryptoCurrency, network: Network, @@ -164,7 +169,7 @@ internal abstract class ManageTokensUiManager( val hasLinkedTokens = if (userWalletId == null || !isCoin) { false } else { - checkHasLinkedTokens(userWalletId, network) + actions.checkHasLinkedTokens(userWalletId, network) } val message = ContentMessage { onDismiss -> @@ -189,8 +194,6 @@ internal abstract class ManageTokensUiManager( messageSender.send(message) } - protected abstract suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean - private fun Batch>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int { return data .indexOfFirst { it.id == id } From fd7ac00fd43c2b3c86bed31c3053059868e9511e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 20:08:13 +0400 Subject: [PATCH 14/90] Updated on 2026-08-14 --- .../tangem/domain/common/CardTypesResolver.kt | 46 ---- .../domain/common/TangemCardTypesResolver.kt | 79 ------ features/wallet/impl/build.gradle.kts | 4 + .../presentation/wallet/domain/NoteImage.kt | 31 +++ .../wallet/domain/Wallet2CobrandImage.kt | 161 ++++++++++++ .../wallet/domain/WalletImageResolver.kt | 232 ++---------------- ...6.webp => ill_trillant_card2_120_106.webp} | Bin ...6.webp => ill_trillant_card3_120_106.webp} | Bin .../wallet/domain/NoteImageTest.kt | 26 ++ .../wallet/domain/Wallet2CobrandImageTest.kt | 22 ++ 10 files changed, 268 insertions(+), 333 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt rename features/wallet/impl/src/main/res/drawable/{ill_traillant_card2_120_106.webp => ill_trillant_card2_120_106.webp} (100%) rename features/wallet/impl/src/main/res/drawable/{ill_traillant_card3_120_106.webp => ill_trillant_card3_120_106.webp} (100%) create mode 100644 features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt create mode 100644 features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 55cc073069..29ce08c107 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -10,54 +10,8 @@ interface CardTypesResolver { fun isTangemWallet(): Boolean - fun isWhiteWallet2(): Boolean - - fun isAvroraWallet(): Boolean - - fun isTraillantWallet(): Boolean - fun isShibaWallet(): Boolean - fun isTronWallet(): Boolean - - fun isKaspaWallet(): Boolean - - fun isKaspa2Wallet(): Boolean - - fun isKaspaResellerWallet(): Boolean - - fun isBadWallet(): Boolean - - fun isJrWallet(): Boolean - - fun isGrimWallet(): Boolean - - fun isSatoshiFriendsWallet(): Boolean - - fun isBitcoinPizzaDayWallet(): Boolean - - fun isVeChainWallet(): Boolean - - fun isNewWorldEliteWallet(): Boolean - - fun isRedPandaWallet(): Boolean - - fun isCryptoSethWallet(): Boolean - - fun isKishuInuWallet(): Boolean - - fun isBabyDogeWallet(): Boolean - - fun isCOQWallet(): Boolean - - fun isCoinMetricaWallet(): Boolean - - fun isVoltInuWallet(): Boolean - - fun isVividWallet(): Boolean - - fun isPastelWallet(): Boolean - fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 6a4afdcfd9..85f6165a23 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -26,60 +26,10 @@ internal class TangemCardTypesResolver( card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable } - override fun isWhiteWallet2(): Boolean = card.batchId == WHITE_WALLET2_BATCH_ID - - override fun isAvroraWallet(): Boolean = card.batchId == AVRORA_WALLET_BATCH_ID - - override fun isTraillantWallet(): Boolean = card.batchId == TRILLIANT_WALLET_BATCH_ID - override fun isShibaWallet(): Boolean { return card.firmwareVersion.compareTo(FirmwareVersion.KeysImportAvailable) == 0 } - override fun isTronWallet(): Boolean = card.batchId == TRON_WALLET_BATCH_ID - - override fun isKaspaWallet(): Boolean = card.batchId == KASPA_WALLET_BATCH_ID - - override fun isKaspa2Wallet(): Boolean = card.batchId == KASPA2_WALLET_BATCH_ID - - override fun isKaspaResellerWallet(): Boolean = card.batchId == KASPA_RESELLER_WALLET_BATCH_ID - - override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID - - override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID - - override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID - - override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID - - override fun isBitcoinPizzaDayWallet(): Boolean = card.batchId == BITCOIN_PIZZA_DAY_WALLET_BATCH_ID - - override fun isVeChainWallet(): Boolean = card.batchId == VECHAIN_WALLET_BATCH_ID - - override fun isNewWorldEliteWallet(): Boolean = card.batchId == NEW_WORLD_ELITE_WALLET_BATCH_ID - - override fun isRedPandaWallet(): Boolean = card.batchId == RED_PANDA_WALLET_BATCH_ID - - override fun isCryptoSethWallet(): Boolean = card.batchId == CRYPTO_SETH_WALLET_BATCH_ID - - override fun isKishuInuWallet(): Boolean = card.batchId == KISHU_INU_WALLET_BATCH_ID - - override fun isBabyDogeWallet(): Boolean = card.batchId == BABY_DOGE_WALLET_BATCH_ID - - override fun isCOQWallet(): Boolean = card.batchId == COQ_WALLET_BATCH_ID - - override fun isCoinMetricaWallet(): Boolean = card.batchId == COIN_METRICA_WALLET_BATCH_ID - - override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID - - override fun isVividWallet(): Boolean = card.batchId == VIVID_LEMON_WALLET_BATCH_ID || - card.batchId == VIVID_AQUA_WALLET_BATCH_ID || - card.batchId == VIVID_GRAPEFRUIT_WALLET_BATCH_ID - - override fun isPastelWallet(): Boolean = card.batchId == PASTEL_PEACH_WALLET_BATCH_ID || - card.batchId == PASTEL_GRASS_WALLET_BATCH_ID || - card.batchId == PASTEL_AIR_WALLET_BATCH_ID - override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -183,34 +133,5 @@ internal class TangemCardTypesResolver( private companion object { const val DEV_KIT_CARD_BATCH_ID = "CB83" - const val TRON_WALLET_BATCH_ID = "AF07" - const val KASPA_WALLET_BATCH_ID = "AF08" - const val KASPA2_WALLET_BATCH_ID = "AF25" - const val KASPA_RESELLER_WALLET_BATCH_ID = "AF31" - const val BAD_WALLET_BATCH_ID = "AF09" - const val JR_WALLET_BATCH_ID = "AF14" - const val GRIM_WALLET_BATCH_ID = "AF13" - const val SATOSHI_WALLET_BATCH_ID = "AF19" - const val WHITE_WALLET2_BATCH_ID = "AF15" - const val TRILLIANT_WALLET_BATCH_ID = "AF16" - const val AVRORA_WALLET_BATCH_ID = "AF18" - const val BITCOIN_PIZZA_DAY_WALLET_BATCH_ID = "AF33" - const val VECHAIN_WALLET_BATCH_ID = "AF29" - const val NEW_WORLD_ELITE_WALLET_BATCH_ID = "AF26" - const val RED_PANDA_WALLET_BATCH_ID = "AF34" - const val CRYPTO_SETH_WALLET_BATCH_ID = "AF32" - const val KISHU_INU_WALLET_BATCH_ID = "AF52" - const val BABY_DOGE_WALLET_BATCH_ID = "AF51" - const val COQ_WALLET_BATCH_ID = "AF28" - const val COIN_METRICA_WALLET_BATCH_ID = "AF27" - const val VOLT_INU_WALLET_BATCH_ID = "AF35" - // VIVID WALLETS - const val VIVID_LEMON_WALLET_BATCH_ID = "AF40" - const val VIVID_AQUA_WALLET_BATCH_ID = "AF41" - const val VIVID_GRAPEFRUIT_WALLET_BATCH_ID = "AF42" - // PASTEL WALLETS - const val PASTEL_PEACH_WALLET_BATCH_ID = "AF43" - const val PASTEL_AIR_WALLET_BATCH_ID = "AF44" - const val PASTEL_GRASS_WALLET_BATCH_ID = "AF45" } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 9d6ff75448..4cdbb44b40 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -92,4 +92,8 @@ dependencies { implementation(projects.features.details.api) implementation(projects.features.pushNotifications.api) implementation(projects.features.markets.api) + + /** Test libraries */ + implementation(deps.test.junit) + implementation(deps.test.truth) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt new file mode 100644 index 0000000000..44d5b5b268 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import androidx.annotation.DrawableRes +import com.tangem.blockchain.common.Blockchain +import com.tangem.feature.wallet.impl.R + +/** + * Note image model + * + * @property blockchain blockchain + * @property imageResId image res id + * +[REDACTED_AUTHOR] + */ +internal enum class NoteImage( + val blockchain: Blockchain, + @DrawableRes val imageResId: Int, +) { + + Bitcoin(blockchain = Blockchain.Bitcoin, imageResId = R.drawable.ill_note_btc_120_106), + + Ethereum(blockchain = Blockchain.Ethereum, imageResId = R.drawable.ill_note_ethereum_120_106), + + Binance(blockchain = Blockchain.BSC, imageResId = R.drawable.ill_note_binance_120_106), + + Dogecoin(blockchain = Blockchain.Dogecoin, imageResId = R.drawable.ill_note_doge_120_106), + + Cardano(blockchain = Blockchain.Cardano, imageResId = R.drawable.ill_note_cardano_120_106), + + XRP(blockchain = Blockchain.XRP, imageResId = R.drawable.ill_note_xrp_120_106), +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt new file mode 100644 index 0000000000..82d07f7000 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import androidx.annotation.DrawableRes +import com.tangem.feature.wallet.impl.R + +/** + * Wallet2 cobrand image. + * To integrate a new cobrand, just implement new enum object and that all. + * + * @property cards2ResId image resource id for wallet set with 2 cards + * @property cards3ResId image resource id for wallet set with 3 cards + * @property batchIds set of unique batch ids for this cobrand + * +[REDACTED_AUTHOR] + */ +internal enum class Wallet2CobrandImage( + @DrawableRes val cards2ResId: Int, + @DrawableRes val cards3ResId: Int, + val batchIds: Set, +) { + + Avrora( + cards2ResId = R.drawable.ill_avrora_card2_120_106, + cards3ResId = R.drawable.ill_avrora_card3_120_106, + batchIds = setOf("AF18"), + ), + + BabyDoge( + cards2ResId = R.drawable.ill_baby_doge_card2_120_106, + cards3ResId = R.drawable.ill_baby_doge_card3_120_106, + batchIds = setOf("AF51"), + ), + + Bad( + cards2ResId = R.drawable.ill_bad_card2_120_106, + cards3ResId = R.drawable.ill_bad_card3_120_106, + batchIds = setOf("AF09"), + ), + + BitcoinPizzaDay( + cards2ResId = R.drawable.ill_pizza_day_card2_120_106, + cards3ResId = R.drawable.ill_pizza_day_card3_120_106, + batchIds = setOf("AF33"), + ), + + CoinMetrica( + cards2ResId = R.drawable.ill_coin_metrica_card2_120_106, + cards3ResId = R.drawable.ill_coin_metrica_card3_120_106, + batchIds = setOf("AF27"), + ), + + COQ( + cards2ResId = R.drawable.ill_coq_card2_120_106, + cards3ResId = R.drawable.ill_coq_card3_120_106, + batchIds = setOf("AF28"), + ), + + CryptoSeth( + cards2ResId = R.drawable.ill_crypto_seth_card2_120_106, + cards3ResId = R.drawable.ill_crypto_seth_card3_120_106, + batchIds = setOf("AF32"), + ), + + Grim( + cards2ResId = R.drawable.ill_grim_card2_120_106, + cards3ResId = R.drawable.ill_grim_card3_120_106, + batchIds = setOf("AF13"), + ), + + Jr( + cards2ResId = R.drawable.ill_jr_card2_120_106, + cards3ResId = R.drawable.ill_jr_card3_120_106, + batchIds = setOf("AF14"), + ), + + Kaspa( + cards2ResId = R.drawable.ill_kaspa_card2_120_106, + cards3ResId = R.drawable.ill_kaspa_card3_120_106, + batchIds = setOf("AF08"), + ), + + Kaspa2( + cards2ResId = R.drawable.ill_kaspa2_card2_120_106, + cards3ResId = R.drawable.ill_kaspa2_card3_120_106, + batchIds = setOf("AF25"), + ), + + KaspaReseller( + cards2ResId = R.drawable.ill_kaspa_reseller_card2_120_106, + cards3ResId = R.drawable.ill_kaspa_reseller_card3_120_106, + batchIds = setOf("AF31"), + ), + + KishuInu( + cards2ResId = R.drawable.ill_kishu_inu_card2_120_106, + cards3ResId = R.drawable.ill_kishu_inu_card3_120_106, + batchIds = setOf("AF52"), + ), + + NewWorldElite( + cards2ResId = R.drawable.ill_nwe_card2_120_106, + cards3ResId = R.drawable.ill_nwe_card3_120_106, + batchIds = setOf("AF26"), + ), + + // for multicolored cards use image of 3 cards in all cases + Pastel( + cards2ResId = R.drawable.ill_pastel_cards3_120_106, + cards3ResId = R.drawable.ill_pastel_cards3_120_106, + batchIds = setOf("AF43", "AF44", "AF45"), + ), + + RedPanda( + cards2ResId = R.drawable.ill_red_panda_card2_120_106, + cards3ResId = R.drawable.ill_red_panda_card3_120_106, + batchIds = setOf("AF34"), + ), + + SatoshiFriends( + cards2ResId = R.drawable.ill_satoshi_card2_120_106, + cards3ResId = R.drawable.ill_satoshi_card3_120_106, + batchIds = setOf("AF19"), + ), + + Trillant( + cards2ResId = R.drawable.ill_trillant_card2_120_106, + cards3ResId = R.drawable.ill_trillant_card3_120_106, + batchIds = setOf("AF16"), + ), + + Tron( + cards2ResId = R.drawable.ill_tron_card2_120_106, + cards3ResId = R.drawable.ill_tron_card3_120_106, + batchIds = setOf("AF07"), + ), + + VeChain( + cards2ResId = R.drawable.ill_vechain_card2_120_106, + cards3ResId = R.drawable.ill_vechain_card3_120_106, + batchIds = setOf("AF29"), + ), + + // for multicolored cards use image of 3 cards in all cases + Vivid( + cards2ResId = R.drawable.ill_vivid_cards3_120_106, + cards3ResId = R.drawable.ill_vivid_cards3_120_106, + batchIds = setOf("AF40", "AF41", "AF42"), + ), + + VoltInu( + cards2ResId = R.drawable.ill_volt_inu_card2_120_106, + cards3ResId = R.drawable.ill_volt_inu_card3_120_106, + batchIds = setOf("AF35"), + ), + + WhiteTangem( + cards2ResId = R.drawable.ill_white_card2_120_106, + cards3ResId = R.drawable.ill_white_card3_120_106, + batchIds = setOf("AF15"), + ), +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 2bd5ddb371..711cdaac37 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain import androidx.annotation.DrawableRes -import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.demo.DemoConfig @@ -13,7 +12,6 @@ import com.tangem.feature.wallet.impl.R * [REDACTED_AUTHOR] */ -// TODO: make flexible to integrate cobrands ([REDACTED_JIRA]) internal object WalletImageResolver { private const val WALLET_WITHOUT_BACKUP_COUNT = 1 @@ -25,42 +23,42 @@ internal object WalletImageResolver { @DrawableRes fun resolve(userWallet: UserWallet): Int? { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + + val cobrandImage = Wallet2CobrandImage.entries.firstOrNull { + it.batchIds.contains(userWallet.scanResponse.card.batchId) + } + + val noteImage by lazy { + val noteBlockchain = cardTypesResolver.getBlockchain() + + NoteImage.entries.firstOrNull { it.blockchain == noteBlockchain } + } + return when { cardTypesResolver.isDevKit() -> R.drawable.ill_dev_120_106 - cardTypesResolver.isWhiteWallet2() -> userWallet.resolveWhiteWallet2() - cardTypesResolver.isAvroraWallet() -> userWallet.resolveAvroraWallet() - cardTypesResolver.isTraillantWallet() -> userWallet.resolveTraillantWallet() - cardTypesResolver.isTronWallet() -> userWallet.resolveTronWallet() - cardTypesResolver.isKaspaWallet() -> userWallet.resolveKaspaWallet() - cardTypesResolver.isKaspa2Wallet() -> userWallet.resolveKaspa2Wallet() - cardTypesResolver.isKaspaResellerWallet() -> userWallet.resolveKaspaResellerWallet() - cardTypesResolver.isBadWallet() -> userWallet.resolveBadWallet() - cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() - cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() - cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet() - cardTypesResolver.isBitcoinPizzaDayWallet() -> userWallet.resolveBitcoinPizzaDayWallet() - cardTypesResolver.isVeChainWallet() -> userWallet.resolveVeChainWallet() - cardTypesResolver.isNewWorldEliteWallet() -> userWallet.resolveNewWorldEliteWallet() - cardTypesResolver.isRedPandaWallet() -> userWallet.resolveRedPandaWallet() - cardTypesResolver.isCryptoSethWallet() -> userWallet.resolveCryptoSethWallet() - cardTypesResolver.isKishuInuWallet() -> userWallet.resolveKishuInuWallet() - cardTypesResolver.isBabyDogeWallet() -> userWallet.resolveBabyDogeWallet() - cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet() - cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet() - cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet() - cardTypesResolver.isVividWallet() -> userWallet.resolveVividWallet() - cardTypesResolver.isPastelWallet() -> userWallet.resolvePastelWallet() + cobrandImage != null -> userWallet.resolveWallet2Cobrand(image = cobrandImage) cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() cardTypesResolver.isWhiteWallet() -> R.drawable.ill_wallet_old_white_120_106 cardTypesResolver.isTangemTwins() -> R.drawable.ill_twins_120_106 cardTypesResolver.isStart2Coin() -> R.drawable.ill_start2coin_120_106 - cardTypesResolver.isTangemNote() -> resolveNote(blockchain = cardTypesResolver.getBlockchain()) + cardTypesResolver.isTangemNote() -> noteImage?.imageResId else -> null } } + private fun UserWallet.resolveWallet2Cobrand(image: Wallet2CobrandImage): Int? { + return resolveWallet2(oneBackupResId = image.cards2ResId, twoBackupResId = image.cards3ResId) + } + + private fun UserWallet.resolveShibaWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_shiba_card2_120_106, + twoBackupResId = R.drawable.ill_shiba_card3_120_106, + ) + } + private fun UserWallet.resolveWallet2( @DrawableRes oneBackupResId: Int = R.drawable.ill_wallet2_cards2_120_106, @DrawableRes twoBackupResId: Int = R.drawable.ill_wallet2_cards3_120_106, @@ -76,176 +74,6 @@ internal object WalletImageResolver { } } - private fun UserWallet.resolveTronWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_tron_card2_120_106, - twoBackupResId = R.drawable.ill_tron_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspa2Wallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa2_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa2_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspaResellerWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa_reseller_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa_reseller_card3_120_106, - ) - } - - private fun UserWallet.resolveBadWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_bad_card2_120_106, - twoBackupResId = R.drawable.ill_bad_card3_120_106, - ) - } - - private fun UserWallet.resolveJrWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_jr_card2_120_106, - twoBackupResId = R.drawable.ill_jr_card3_120_106, - ) - } - - private fun UserWallet.resolveGrimWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_grim_card2_120_106, - twoBackupResId = R.drawable.ill_grim_card3_120_106, - ) - } - - private fun UserWallet.resolveSatoshiWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_satoshi_card2_120_106, - twoBackupResId = R.drawable.ill_satoshi_card3_120_106, - ) - } - - private fun UserWallet.resolveShibaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_shiba_card2_120_106, - twoBackupResId = R.drawable.ill_shiba_card3_120_106, - ) - } - - private fun UserWallet.resolveWhiteWallet2(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_white_card2_120_106, - twoBackupResId = R.drawable.ill_white_card3_120_106, - ) - } - - private fun UserWallet.resolveAvroraWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_avrora_card2_120_106, - twoBackupResId = R.drawable.ill_avrora_card3_120_106, - ) - } - - private fun UserWallet.resolveTraillantWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_traillant_card2_120_106, - twoBackupResId = R.drawable.ill_traillant_card3_120_106, - ) - } - - private fun UserWallet.resolveBitcoinPizzaDayWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_pizza_day_card2_120_106, - twoBackupResId = R.drawable.ill_pizza_day_card3_120_106, - ) - } - - private fun UserWallet.resolveVeChainWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_vechain_card2_120_106, - twoBackupResId = R.drawable.ill_vechain_card3_120_106, - ) - } - - private fun UserWallet.resolveNewWorldEliteWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_nwe_card2_120_106, - twoBackupResId = R.drawable.ill_nwe_card3_120_106, - ) - } - - private fun UserWallet.resolveRedPandaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_red_panda_card2_120_106, - twoBackupResId = R.drawable.ill_red_panda_card3_120_106, - ) - } - - private fun UserWallet.resolveCryptoSethWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_crypto_seth_card2_120_106, - twoBackupResId = R.drawable.ill_crypto_seth_card3_120_106, - ) - } - - private fun UserWallet.resolveKishuInuWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kishu_inu_card2_120_106, - twoBackupResId = R.drawable.ill_kishu_inu_card3_120_106, - ) - } - - private fun UserWallet.resolveBabyDogeWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_baby_doge_card2_120_106, - twoBackupResId = R.drawable.ill_baby_doge_card3_120_106, - ) - } - - private fun UserWallet.resolveCOQWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_coq_card2_120_106, - twoBackupResId = R.drawable.ill_coq_card3_120_106, - ) - } - - private fun UserWallet.resolveCoinMetricaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_coin_metrica_card2_120_106, - twoBackupResId = R.drawable.ill_coin_metrica_card3_120_106, - ) - } - - private fun UserWallet.resolveVoltInuWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_volt_inu_card2_120_106, - twoBackupResId = R.drawable.ill_volt_inu_card3_120_106, - ) - } - - private fun UserWallet.resolveVividWallet(): Int? { - // for multicolored cards use image of 3 cards in all cases - return resolveWallet2( - oneBackupResId = R.drawable.ill_vivid_cards3_120_106, - twoBackupResId = R.drawable.ill_vivid_cards3_120_106, - ) - } - - private fun UserWallet.resolvePastelWallet(): Int? { - // for multicolored cards use image of 3 cards in all cases - return resolveWallet2( - oneBackupResId = R.drawable.ill_pastel_cards3_120_106, - twoBackupResId = R.drawable.ill_pastel_cards3_120_106, - ) - } - private fun UserWallet.resolveWallet1(): Int? { return resolveWalletWithBackups { count -> when (count) { @@ -262,16 +90,4 @@ internal object WalletImageResolver { return if (count != null) resolve(count) else null } - - private fun resolveNote(blockchain: Blockchain): Int? { - return when (blockchain) { - Blockchain.Bitcoin -> R.drawable.ill_note_btc_120_106 - Blockchain.Ethereum -> R.drawable.ill_note_ethereum_120_106 - Blockchain.BSC -> R.drawable.ill_note_binance_120_106 - Blockchain.Dogecoin -> R.drawable.ill_note_doge_120_106 - Blockchain.Cardano -> R.drawable.ill_note_cardano_120_106 - Blockchain.XRP -> R.drawable.ill_note_xrp_120_106 - else -> null - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ill_traillant_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_trillant_card2_120_106.webp similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ill_traillant_card2_120_106.webp rename to features/wallet/impl/src/main/res/drawable/ill_trillant_card2_120_106.webp diff --git a/features/wallet/impl/src/main/res/drawable/ill_traillant_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_trillant_card3_120_106.webp similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ill_traillant_card3_120_106.webp rename to features/wallet/impl/src/main/res/drawable/ill_trillant_card3_120_106.webp diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt new file mode 100644 index 0000000000..c6da034569 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NoteImageTest { + + @Test + fun check() { + // check uniqueness + Truth.assertThat(NoteImage.entries.map { it.blockchain }).containsNoDuplicates() + Truth.assertThat(NoteImage.entries.map { it.imageResId }).containsNoDuplicates() + + // check blockchain matching + Truth.assertThat(NoteImage.Bitcoin.blockchain).isEqualTo(Blockchain.Bitcoin) + Truth.assertThat(NoteImage.Ethereum.blockchain).isEqualTo(Blockchain.Ethereum) + Truth.assertThat(NoteImage.Binance.blockchain).isEqualTo(Blockchain.BSC) + Truth.assertThat(NoteImage.Dogecoin.blockchain).isEqualTo(Blockchain.Dogecoin) + Truth.assertThat(NoteImage.Cardano.blockchain).isEqualTo(Blockchain.Cardano) + Truth.assertThat(NoteImage.XRP.blockchain).isEqualTo(Blockchain.XRP) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt new file mode 100644 index 0000000000..13d88a97fd --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.google.common.truth.Truth +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class Wallet2CobrandImageTest { + + @Test + fun checkUniqueness() { + val excluded = listOf(Wallet2CobrandImage.Pastel, Wallet2CobrandImage.Vivid) + (Wallet2CobrandImage.entries - excluded).forEach { + Truth.assertThat(it.cards2ResId != it.cards3ResId).isTrue() + } + + Truth.assertThat(Wallet2CobrandImage.entries.map { it.cards2ResId }).containsNoDuplicates() + Truth.assertThat(Wallet2CobrandImage.entries.map { it.cards3ResId }).containsNoDuplicates() + Truth.assertThat(Wallet2CobrandImage.entries.flatMap { it.batchIds }).containsNoDuplicates() + } +} \ No newline at end of file From ddecdf444dc1b20120f80c339df4afbaa15fe794 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 14:12:58 +0300 Subject: [PATCH 15/90] Updated on 2026-08-14 --- .../models/request/ActionRequestBody.kt | 12 +++++++-- .../tron/TronStakeKitTransaction.kt | 10 +++++++ .../data/staking/DefaultStakingRepository.kt | 27 ++++++++++++++++++- .../data/staking/di/StakingDataModule.kt | 2 ++ gradle/dependencies.toml | 2 +- 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index d6c09517c1..9063ff0b74 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -48,7 +48,7 @@ data class ActionRequestBodyArgs( @Json(name = "ledgerWalletAPICompatible") val ledgerWalletAPICompatible: Boolean? = null, @Json(name = "tronResource") - val tronResource: String? = null, + val tronResource: TronResource? = null, @Json(name = "signatureVerification") val signatureVerification: SignatureVerification? = null, @Json(name = "inputToken") @@ -60,4 +60,12 @@ data class SignatureVerification( val message: String, @Json(name = "signed") val signed: String, -) \ No newline at end of file +) + +enum class TronResource { + @Json(name = "ENERGY") + ENERGY, + + @Json(name = "BANDWIDTH") + BANDWIDTH, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt new file mode 100644 index 0000000000..f12f5fe5ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction.tron + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TronStakeKitTransaction( + @Json(name = "raw_data_hex") + val rawDataHex: String, +) \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 541762c34d..c4b9b8fad1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,10 +2,13 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.raise.catch +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey @@ -22,6 +25,7 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectListSync @@ -66,6 +70,7 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggle: StakingFeatureToggles, private val walletManagersFacade: WalletManagersFacade, + private val moshi: Moshi, ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() @@ -103,6 +108,9 @@ internal class DefaultStakingRepository( value = emptyMap(), ) + private val tronStakeKitTransactionAdapter: JsonAdapter = + moshi.adapter(TronStakeKitTransaction::class.java) + override fun isStakingSupported(integrationKey: String): Boolean { return integrationIdMap.containsKey(integrationKey) } @@ -509,6 +517,8 @@ internal class DefaultStakingRepository( amount = params.amount.toPlainString(), inputToken = tokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, + validatorAddresses = listOf(params.validatorAddress), // check on other networks + tronResource = getTronResource(network), ), ) } @@ -567,6 +577,11 @@ internal class DefaultStakingRepository( -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) Blockchain.Ethereum, -> TransactionData.Compiled.Data.RawString(unsignedTransaction) + Blockchain.Tron -> { + val tronStakeKitTransaction = tronStakeKitTransactionAdapter.fromJson(unsignedTransaction) + ?: error("Failed to parse Tron StakeKit transaction") + TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex) + } else -> error("Unsupported blockchain") } } @@ -598,6 +613,16 @@ internal class DefaultStakingRepository( private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId) + private fun getTronResource(network: Network): TronResource? { + val blockchain = Blockchain.fromNetworkId(network.backendId) + + return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) { + TronResource.ENERGY + } else { + null + } + } + private companion object { const val YIELDS_STORE_KEY = "yields" @@ -622,7 +647,7 @@ internal class DefaultStakingRepository( Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, - // Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, + Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index d261cdece4..32a9a1cdae 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -37,6 +37,7 @@ internal object StakingDataModule { stakingFeatureToggle: StakingFeatureToggles, cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, + @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, @@ -47,6 +48,7 @@ internal object StakingDataModule { cacheRegistry = cacheRegistry, stakingFeatureToggle = stakingFeatureToggle, walletManagersFacade = walletManagersFacade, + moshi = moshi, ) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 74b8993062..dbe4ee3f6d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-736" +tangemBlockchainSdk = "develop-740" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 78e022c1d209eb8c29de5673f40493bafa6841c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 16:35:04 +0500 Subject: [PATCH 16/90] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 2 + .../alerts/SendTransactionAlertConverter.kt | 49 ++++++++++++ .../ui/alerts/models/AlertDemoModeUM.kt | 13 +++ .../alerts/models/AlertTransactionErrorUM.kt | 21 +++++ .../tangem/common/ui/alerts/models/AlertUM.kt | 12 +++ .../transaction/error/SendTransactionError.kt | 4 +- .../usecase/SendTransactionUseCase.kt | 4 +- features/send/impl/build.gradle.kts | 1 + .../state/SendTransactionAlertConverter.kt | 4 +- features/staking/impl/build.gradle.kts | 1 + .../presentation/state/StakingAlertState.kt | 24 ------ .../state/StakingStateController.kt | 12 +++ .../impl/presentation/state/StakingUiState.kt | 1 + .../state/events/StakingAlertUM.kt | 29 +++++++ .../state/{ => events}/StakingEvent.kt | 5 +- .../state/events/StakingEventFactory.kt | 44 +++++++++++ .../state/stub/StakingClickIntentsStub.kt | 3 + .../presentation/ui/StakingEventEffect.kt | 79 +++++++++++++++++++ .../impl/presentation/ui/StakingScreen.kt | 8 ++ .../viewmodel/StakingClickIntents.kt | 2 + .../viewmodel/StakingViewModel.kt | 45 ++++++++--- 21 files changed, 317 insertions(+), 46 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt rename domain/transaction/{src/main/java => models/src/main/kotlin}/com/tangem/domain/transaction/error/SendTransactionError.kt (81%) delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/{ => events}/StakingEvent.kt (54%) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 40a779c416..2d6b9e12d6 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.transaction.models) + implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt new file mode 100644 index 0000000000..2cb03f4f16 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.common.ui.alerts + +import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.utils.converter.Converter + +class SendTransactionAlertConverter( + private val popBackStack: () -> Unit, + private val onFailedTxEmailClick: (String) -> Unit, +) : Converter { + override fun convert(value: SendTransactionError): AlertUM? { + return when (value) { + is SendTransactionError.DemoCardError -> AlertDemoModeUM( + onConfirmClick = popBackStack, + ) + is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM( + code = value.code.toString(), + cause = null, + causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), + onConfirmClick = { onFailedTxEmailClick(value.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM( + code = value.code.toString(), + cause = value.message, + onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> AlertTransactionErrorUM( + code = "", + cause = value.message, + onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> AlertTransactionErrorUM( + code = value.code.orEmpty(), + cause = value.message.orEmpty(), + onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> AlertTransactionErrorUM( + code = "", + cause = value.ex?.localizedMessage, + onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, + ) + else -> null + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt new file mode 100644 index 0000000000..e63ccb0229 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt @@ -0,0 +1,13 @@ +package com.tangem.common.ui.alerts.models + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference + +data class AlertDemoModeUM( + override val onConfirmClick: () -> Unit, +) : AlertUM { + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) + override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt new file mode 100644 index 0000000000..5fd83163be --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt @@ -0,0 +1,21 @@ +package com.tangem.common.ui.alerts.models + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList + +data class AlertTransactionErrorUM( + val code: String, + val cause: String?, + val causeTextReference: TextReference? = null, + override val onConfirmClick: (() -> Unit)? = null, +) : AlertUM { + 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.common_support) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt new file mode 100644 index 0000000000..1cf955bebe --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt @@ -0,0 +1,12 @@ +package com.tangem.common.ui.alerts.models + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +interface AlertUM { + val title: TextReference? + val message: TextReference + val confirmButtonText: TextReference + val onConfirmClick: (() -> Unit)? +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt similarity index 81% rename from domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt rename to domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt index 3f5f7485d4..afa825d759 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -1,7 +1,5 @@ package com.tangem.domain.transaction.error -import com.tangem.core.ui.extensions.TextReference - sealed class SendTransactionError { data object DemoCardError : SendTransactionError() @@ -16,7 +14,7 @@ sealed class SendTransactionError { data class CreateAccountUnderfunded(val amount: String) : SendTransactionError() - data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError() + data class TangemSdkError(val code: Int, val messageRes: Int, val args: List) : SendTransactionError() data class UnknownError(val ex: Exception? = null) : SendTransactionError() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 96b5db01fa..c6b5e9258c 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -10,7 +10,6 @@ import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.extensions.Result 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 @@ -133,8 +132,7 @@ class SendTransactionUseCase( 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) + SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) } is BlockchainSdkError.WrappedTangemError -> { parseWrappedError(tangemError) // todo remove when sdk errors are revised diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 9a3de6d8cb..14e47173a5 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.card) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt index d37954025b..7d184f5227 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.state +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.converter.Converter @@ -15,7 +17,7 @@ internal class SendTransactionAlertConverter( is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError( code = value.code.toString(), cause = null, - causeTextReference = value.messageReference, + causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) }, ) is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError( diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 15213f79d6..9d23634914 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory) /** Common */ diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt deleted file mode 100644 index c133fe00ca..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.staking.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.features.staking.impl.R - -@Immutable -internal sealed class StakingAlertState { - - 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? = TODO(), - override val onConfirmClick: () -> Unit, - ) : StakingAlertState() { - override val message: TextReference = resourceReference(R.string.common_unknown_error) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 1bf70095ff..8bb0aef58a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -3,8 +3,10 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer @@ -46,6 +48,16 @@ internal class StakingStateController @Inject constructor() { mutableUiState.update(function = titleTransformer::transform) } + fun updateEvent(event: StakingEvent?) { + mutableUiState.update { + it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent()) + } + } + + private fun dismissAlert() { + mutableUiState.update { it.copy(event = consumedEvent()) } + } + private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index e9b4c44faa..4b000bc130 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt new file mode 100644 index 0000000000..f89f99f6a0 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.staking.impl.presentation.state.events + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.alerts.models.AlertUM +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.staking.impl.R + +@Immutable +internal sealed class StakingAlertUM : AlertUM { + + data class GenericError( + override val onConfirmClick: () -> Unit, + ) : StakingAlertUM() { + override val title: TextReference = resourceReference(R.string.common_error) + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) + } + + data class StakingError( + val code: String, + override val onConfirmClick: () -> Unit, + ) : StakingAlertUM() { + override val title: TextReference = resourceReference(R.string.common_error) + override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code)) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt similarity index 54% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt index dbc800ea01..7e7700595e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt @@ -1,6 +1,7 @@ -package com.tangem.features.staking.impl.presentation.state +package com.tangem.features.staking.impl.presentation.state.events import androidx.compose.runtime.Immutable +import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference @Immutable @@ -8,5 +9,5 @@ internal sealed class StakingEvent { data class ShowSnackBar(val text: TextReference) : StakingEvent() - data class ShowAlert(val alert: StakingAlertState) : StakingEvent() + data class ShowAlert(val alert: AlertUM) : StakingEvent() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt new file mode 100644 index 0000000000..173975c66c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -0,0 +1,44 @@ +package com.tangem.features.staking.impl.presentation.state.events + +import com.tangem.common.ui.alerts.SendTransactionAlertConverter +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.staking.impl.presentation.state.StakingStateController + +internal class StakingEventFactory( + private val stateController: StakingStateController, + private val popBackStack: () -> Unit, + private val onFailedTxEmailClick: (String) -> Unit, +) { + + fun createGenericErrorAlert(error: String) { + val alert = StakingEvent.ShowAlert( + StakingAlertUM.GenericError( + onConfirmClick = { onFailedTxEmailClick(error) }, + ), + ) + stateController.updateEvent(alert) + } + + fun createSendTransactionErrorAlert(error: SendTransactionError?) { + val alert = error?.let { + SendTransactionAlertConverter( + popBackStack = popBackStack, + onFailedTxEmailClick = onFailedTxEmailClick, + ).convert(error) + }?.let { + StakingEvent.ShowAlert(it) + } + stateController.updateEvent(alert) + } + + fun createStakingErrorAlert(error: StakingError) { + val alert = StakingEvent.ShowAlert( + StakingAlertUM.StakingError( + code = error.toString(), + onConfirmClick = { onFailedTxEmailClick(error.toString()) }, + ), + ) + stateController.updateEvent(alert) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index c8e5a5d35a..55d130824f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -8,6 +8,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList +@Suppress("TooManyFunctions") object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} @@ -46,5 +47,7 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onShareClick() {} + override fun onFailedTxEmailClick(errorMessage: String) {} + override fun onActiveStake(activeStake: BalanceState) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt new file mode 100644 index 0000000000..7f1d1455ed --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt @@ -0,0 +1,79 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.alerts.models.AlertUM +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.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent + +@Composable +internal fun StakingEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { + val resources = LocalContext.current.resources + var alertConfig by remember { mutableStateOf(value = null) } + + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(key1 = alertConfig) { + keyboardController?.hide() + } + + alertConfig?.let { + StakingAlert(state = it, onDismiss = { alertConfig = null }) + } + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is StakingEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is StakingEvent.ShowAlert -> { + alertConfig = value.alert + } + } + }, + ) +} + +@Composable +internal fun StakingAlert(state: AlertUM, 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, + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index ef94fa0c1d..2bbb05b0c2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,6 +33,8 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { + val snackbarHostState = remember { SnackbarHostState() } + BackHandler(onBack = uiState.clickIntents::onPrevClick) Column( modifier = Modifier @@ -58,6 +61,11 @@ internal fun StakingScreen(uiState: StakingUiState) { ) StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) } + + StakingEventEffect( + event = uiState.event, + snackbarHostState = snackbarHostState, + ) } @Composable diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index db14a50094..f8fb30cfa9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -43,4 +43,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onExploreClick() fun onShareClick() + + fun onFailedTxEmailClick(errorMessage: String) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 09221fb03d..209c6c9304 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -43,6 +43,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer @@ -64,7 +65,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @HiltViewModel internal class StakingViewModel @Inject constructor( private val stateController: StakingStateController, @@ -122,6 +123,13 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() + private val stakingEventFactory: StakingEventFactory + get() = StakingEventFactory( + stateController = stateController, + popBackStack = stakingStateRouter::onBackClick, + onFailedTxEmailClick = ::onFailedTxEmailClick, + ) + private var stakingApproval: StakingApproval = StakingApproval.Empty private val allowanceTaskScheduler = SingleTaskScheduler() @@ -164,6 +172,8 @@ internal class StakingViewModel @Inject constructor( val amountState = value.amountState as? AmountState.Data ?: error("No amount provided") val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") + val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address") val stakingTransaction = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -172,15 +182,16 @@ internal class StakingViewModel @Inject constructor( actionCommonType = value.actionType, integrationId = yield.id, amount = amountValue, - address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: error("No available address"), + address = defaultAddress, validatorAddress = validatorState.chosenValidator.address, token = yield.token, passthrough = pendingAction?.passthrough, type = pendingAction?.type, ), ).getOrElse { - error(it) + Timber.e(it.toString()) + stakingEventFactory.createStakingErrorAlert(it) + return@launch } stakingTransaction @@ -190,8 +201,11 @@ internal class StakingViewModel @Inject constructor( networkId = cryptoCurrencyStatus.currency.network.id.value, fee = fee, transactionId = transaction.id, - ).getOrNull() ?: error("No constructed transaction") - + ).getOrElse { + Timber.e(it.toString()) + stakingEventFactory.createStakingErrorAlert(it) + return@launch + } sendStakingTransaction( transactionId = constructedTransaction.id, gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"), @@ -296,8 +310,7 @@ internal class StakingViewModel @Inject constructor( userWallet = userWallet, cryptoCurrency = cryptoCurrencyStatus.currency, ).getOrElse { - // TODO staking error - return + return stakingEventFactory.createGenericErrorAlert(it.toString()) } stateController.update( @@ -407,7 +420,7 @@ internal class StakingViewModel @Inject constructor( fee = TransactionFee.Single(fee), ), ) - // TODO staking error + stakingEventFactory.createGenericErrorAlert(error.message ?: error.toString()) return@launch }, ifRight = { it }, @@ -427,7 +440,7 @@ internal class StakingViewModel @Inject constructor( fee = TransactionFee.Single(fee), ), ) - // TODO staking error + stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { stateController.update(SetApprovalInProgressTransformer) @@ -489,6 +502,10 @@ internal class StakingViewModel @Inject constructor( // TODO staking [REDACTED_TASK_KEY] } + override fun onFailedTxEmailClick(errorMessage: String) { + // TODO staking sending feedback email [REDACTED_TASK_KEY] + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -505,7 +522,8 @@ internal class StakingViewModel @Inject constructor( userWallet = wallet }, ifLeft = { - // TODO staking error + Timber.e(it.toString()) + stakingEventFactory.createGenericErrorAlert(it.toString()) }, ) getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( @@ -530,7 +548,8 @@ internal class StakingViewModel @Inject constructor( ) }, ifLeft = { - // TODO staking error + Timber.e(it.toString()) + stakingEventFactory.createGenericErrorAlert(it.toString()) }, ) } @@ -579,7 +598,7 @@ internal class StakingViewModel @Inject constructor( pendingActionList = pendingActionList, ), ) - // todo add error dialog + stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { txHash -> submitHash(transactionId, txHash) From 18e5829fbecfdd1a448be8c05fd8b67804928404 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 16:58:01 +0500 Subject: [PATCH 17/90] Updated on 2026-08-14 --- .../impl/presentation/ui/StakingEventEffect.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt index 7f1d1455ed..80ba0d9029 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference @@ -45,24 +45,24 @@ internal fun StakingEventEffect(event: StateEvent, snackbarHostSta @Composable internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = { onActionClick() onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = onDismiss, ) From e51c82e6156f0f1a3fc9549108ade4b495af0adc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 15:13:03 +0300 Subject: [PATCH 18/90] Updated on 2026-08-14 --- .../common/ui/userwallet/UserWalletItem.kt | 19 +- .../components/buttons/common/TangemButton.kt | 57 +++++- .../impl/ui/AddToPortfolioBottomSheet.kt | 184 +++++++++++++++--- .../PreviewAddToPortfolioBSContentProvider.kt | 60 ++++-- .../ui/state/AddToPortfolioBSContentUM.kt | 2 + 5 files changed, 262 insertions(+), 60 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index baf96fd3cf..91f837f146 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -1,8 +1,11 @@ package com.tangem.common.ui.userwallet +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CardColors import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,6 +29,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.common.ui.R import com.tangem.core.ui.coil.RotationTransformation +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemThemePreview @@ -33,9 +37,14 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.persistentListOf @Composable -fun UserWalletItem(state: UserWalletItemUM, modifier: Modifier = Modifier) { +fun UserWalletItem( + state: UserWalletItemUM, + modifier: Modifier = Modifier, + blockColors: CardColors = TangemBlockCardColors, +) { BlockCard( modifier = modifier, + colors = blockColors, onClick = state.onClick, enabled = state.isEnabled, ) { @@ -142,6 +151,7 @@ private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview() { TangemThemePreview { @@ -174,7 +184,12 @@ private fun Preview() { ), ) - Column { + Column( + Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { list.fastForEach { userWalletItemUM -> UserWalletItem( modifier = Modifier.fillMaxWidth(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 11fcaeb099..3033f2221a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -1,5 +1,10 @@ package com.tangem.core.ui.components.buttons.common +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable @@ -36,6 +41,7 @@ fun TangemButton( textStyle: TextStyle = TangemTheme.typography.button, shape: Shape = size.toShape(), iconPadding: Dp = size.toIconPadding(), + animateContentChange: Boolean = false, ) { val multipleClickPreventer = remember { MultipleClickPreventer.get() } @@ -56,6 +62,7 @@ fun TangemButton( buttonIcon = icon, iconPadding = iconPadding, showProgress = showProgress, + animateContentChange = animateContentChange, progressIndicator = { CircularProgressIndicator( modifier = Modifier.buttonContentSize(maxContentSize), @@ -108,24 +115,54 @@ private inline fun RowScope.ButtonContentContainer( buttonIcon: TangemButtonIconPosition, iconPadding: Dp, showProgress: Boolean, + animateContentChange: Boolean, progressIndicator: @Composable RowScope.() -> Unit, - text: @Composable RowScope.() -> Unit, - icon: @Composable RowScope.(Int) -> Unit, + crossinline text: @Composable RowScope.() -> Unit, + crossinline icon: @Composable RowScope.(Int) -> Unit, additionalText: @Composable () -> Unit, ) { if (showProgress) { progressIndicator() } else { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Row(horizontalArrangement = Arrangement.Center) { - if (buttonIcon is TangemButtonIconPosition.Start) { - icon(buttonIcon.iconResId) - Spacer(modifier = Modifier.requiredWidth(iconPadding)) + if (animateContentChange) { + AnimatedContent( + targetState = buttonIcon, + transitionSpec = { + fadeIn(tween(durationMillis = 220)) togetherWith + fadeOut(tween(durationMillis = 220)) + }, + label = "button text with icon", + ) { iconState -> + Row(horizontalArrangement = Arrangement.Center) { + when (iconState) { + is TangemButtonIconPosition.Start -> { + icon(iconState.iconResId) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + text() + } + is TangemButtonIconPosition.End -> { + text() + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + icon(iconState.iconResId) + } + is TangemButtonIconPosition.None -> { + text() + } + } + } } - text() - if (buttonIcon is TangemButtonIconPosition.End) { - Spacer(modifier = Modifier.requiredWidth(iconPadding)) - icon(buttonIcon.iconResId) + } else { + Row(horizontalArrangement = Arrangement.Center) { + if (buttonIcon is TangemButtonIconPosition.Start) { + icon(buttonIcon.iconResId) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + } + text() + if (buttonIcon is TangemButtonIconPosition.End) { + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + icon(buttonIcon.iconResId) + } } } additionalText() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 8a5a2ae45f..e9b760f529 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -2,29 +2,36 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.util.fastForEachIndexed import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.SpacerW6 -import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow @@ -36,18 +43,18 @@ import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import kotlinx.coroutines.delay @Composable internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, containerColor = TangemTheme.colors.background.tertiary, + addBottomInsets = false, titleText = resourceReference(R.string.markets_add_to_portfolio_button), ) { Content( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing16), + modifier = Modifier.fillMaxWidth(), state = config.content as AddToPortfolioBSContentUM, ) } @@ -55,33 +62,101 @@ internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - UserWalletItem(state.selectedWallet) + var continueButtonAreaHeight by remember { mutableIntStateOf(0) } + val density = LocalDensity.current + val scrollState = rememberScrollState() - NetworkSelection( - modifier = Modifier.fillMaxWidth(), - state = state.selectNetworkUM, - ) - - AnimatedVisibility( - visible = state.isScanCardNotificationVisible, - modifier = Modifier.fillMaxWidth(), + Box(modifier = modifier) { + Column( + modifier = Modifier + .verticalScroll(state = scrollState) + .padding(horizontal = TangemTheme.dimens.spacing16), ) { - ScanWalletWarning(modifier = Modifier.fillMaxWidth()) + UserWalletItem( + state = state.selectedWallet, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) + + SpacerH12() + + NetworkSelection( + modifier = Modifier.fillMaxWidth(), + state = state.selectNetworkUM, + ) + + SpacerH12() + + AnimatedVisibility( + visible = state.isScanCardNotificationVisible, + modifier = Modifier.fillMaxWidth(), + ) { + Column { + ScanWalletWarning(modifier = Modifier.fillMaxWidth()) + SpacerH12() + } + + // Scroll to the bottom when the notification appears and the scroll is at the bottom + LaunchedEffect(Unit) { + if (scrollState.canScrollForward.not()) { + delay(timeMillis = 500) + scrollState.animateScrollTo(scrollState.maxValue) + } + } + } + + SpacerH(with(density) { continueButtonAreaHeight.toDp() }) } - PrimaryButtonIconEnd( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.common_continue), - iconResId = R.drawable.ic_tangem_24, - onClick = {}, + AnimatedVisibility( + visible = scrollState.canScrollForward, + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter), + ) { + BottomFade(Modifier.align(Alignment.BottomCenter)) + } + + ContinueButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .onGloballyPositioned { + continueButtonAreaHeight = it.size.height + }, + enabled = state.continueButtonEnabled, + onClick = state.onContinueButtonClick, ) } } +@Composable +private fun ContinueButton(enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemButton( + enabled = enabled, + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .navigationBarsPadding() + .fillMaxWidth(), + text = stringResource(R.string.common_continue), + icon = if (enabled) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + }, + showProgress = false, + size = TangemButtonSize.Default, + colors = TangemButtonsDefaults.primaryButtonColors, + onClick = onClick, + animateContentChange = true, + ) +} + @Suppress("LongMethod") @Composable private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { @@ -95,10 +170,7 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi ) }, ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()), - ) { + Column { Row( modifier = Modifier .fillMaxWidth() @@ -180,6 +252,7 @@ private fun ScanWalletWarning(modifier: Modifier = Modifier) { Icon( modifier = Modifier.requiredSize(TangemTheme.dimens.size20), imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + tint = TangemTheme.colors.icon.primary1, contentDescription = null, ) Text( @@ -237,4 +310,53 @@ private fun PreviewContentRtl( state = content, ) } +} + +// For on device testing +@Composable +@Preview +private fun PreviewContentTestOnDevice( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview( + alwaysShowBottomSheets = false, + ) { + var isShow by remember { mutableStateOf(false) } + + var contentState by remember { + mutableStateOf(content) + } + + LaunchedEffect(Unit) { + contentState = content.copy( + onContinueButtonClick = { + contentState = contentState.copy( + isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, + ) + }, + continueButtonEnabled = true, + selectedWallet = content.selectedWallet.copy( + onClick = { + contentState = contentState.copy( + continueButtonEnabled = !contentState.continueButtonEnabled, + ) + }, + ), + ) + } + + AddToPortfolioBottomSheet( + config = TangemBottomSheetConfig( + isShow = isShow, + content = contentState, + onDismissRequest = { isShow = false }, + ), + ) + + Button( + onClick = { isShow = !isShow }, + ) { + Text(text = "Toggle") + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index 2b33545fd3..d184d3aac6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -12,6 +12,14 @@ import kotlinx.collections.immutable.persistentListOf internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { + private val blockchainRow = BlockchainRowUM( + name = "Etherium 3", + type = "TEST", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = false, + ) + override val values: Sequence get() = sequenceOf( AddToPortfolioBSContentUM( @@ -29,32 +37,50 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider tokenName = "Etherium", tokenCurrencySymbol = "ETH", networks = persistentListOf( - BlockchainRowUM( - name = "Etherium", + blockchainRow.copy( type = "MAIN", - iconResId = R.drawable.ic_eth_16, isMainNetwork = true, isSelected = true, ), - BlockchainRowUM( - name = "Etherium 2", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ), - BlockchainRowUM( - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ), + blockchainRow, + blockchainRow, ), onNetworkSwitchClick = { _, _ -> }, iconUrl = null, ), isScanCardNotificationVisible = true, + continueButtonEnabled = true, + onContinueButtonClick = {}, + ), + AddToPortfolioBSContentUM( + selectedWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ), + selectNetworkUM = SelectNetworkUM( + tokenId = "etherium", + tokenName = "Etherium", + tokenCurrencySymbol = "ETH", + networks = persistentListOf( + blockchainRow.copy( + type = "MAIN", + isMainNetwork = true, + isSelected = true, + ), + *Array(25) { blockchainRow }, + ), + + onNetworkSwitchClick = { _, _ -> }, + iconUrl = null, + ), + isScanCardNotificationVisible = true, + continueButtonEnabled = false, + onContinueButtonClick = {}, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt index ad733c39cf..6b708502ce 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -7,4 +7,6 @@ internal data class AddToPortfolioBSContentUM( val selectedWallet: UserWalletItemUM, val selectNetworkUM: SelectNetworkUM, val isScanCardNotificationVisible: Boolean, + val continueButtonEnabled: Boolean, + val onContinueButtonClick: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file From 1f8af50d6ad180e29eaa575ade699aac7482c96a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 22:22:48 +0400 Subject: [PATCH 19/90] Updated on 2026-08-14 --- core/ui/build.gradle.kts | 3 +- .../core/ui/components/token}/TokenItem.kt | 123 +++++++++++++++--- .../token/internal}/LockedRectangle.kt | 2 +- .../token/internal}/NonFiatContentBlock.kt | 6 +- .../token/internal}/TokenCryptoAmount.kt | 6 +- .../token/internal}/TokenFiatAmount.kt | 6 +- .../components/token/internal}/TokenPrice.kt | 6 +- .../components/token/internal}/TokenTitle.kt | 6 +- .../components/token}/state/TokenItemState.kt | 4 +- .../presentation/common/WalletPreviewData.kt | 121 ++--------------- .../common/preview/WalletScreenPreviewData.kt | 2 +- .../organizetokens/OrganizeTokensScreen.kt | 2 +- .../organizetokens/model/DraggableItem.kt | 2 +- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- .../state/model/WalletTokensListState.kt | 2 +- .../converter/TokenItemStateConverter.kt | 2 +- .../multicurrency/MultiCurrencyContentItem.kt | 2 +- 17 files changed, 143 insertions(+), 154 deletions(-) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component => core/ui/src/main/java/com/tangem/core/ui/components/token}/TokenItem.kt (76%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/LockedRectangle.kt (88%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/NonFiatContentBlock.kt (93%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenCryptoAmount.kt (89%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenFiatAmount.kt (92%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenPrice.kt (95%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenTitle.kt (92%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common => core/ui/src/main/java/com/tangem/core/ui/components/token}/state/TokenItemState.kt (98%) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 4758911a25..3781bde624 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,5 +1,3 @@ -import com.android.ide.common.resources.generateLocaleList - plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -42,6 +40,7 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.reorderable) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt similarity index 76% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 1917d4e4c8..8bdd28b235 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component +package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -16,15 +16,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints +import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.internal.* +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.token.* -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState +import java.util.UUID import kotlin.math.max private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 @@ -35,7 +38,7 @@ private enum class LayoutId { } @Composable -internal fun TokenItem( +fun TokenItem( state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier, @@ -397,8 +400,8 @@ private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider:: private class TokenItemStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.tokenItemVisibleState.copy( - iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true), + tokenItemVisibleState.copy( + iconState = coinIconState.copy(showCustomBadge = true), titleState = TokenItemState.TitleState.Content( text = "PolygonPolygonPolygonPolygonPolygonPolygon", hasPending = true, @@ -414,13 +417,101 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider Date: Wed, 21 Aug 2024 12:53:58 +0500 Subject: [PATCH 20/90] Updated on 2026-08-14 --- .../domain/feedback/FeedbackDataBuilder.kt | 6 +++ .../feedback/SaveBlockchainErrorUseCase.kt | 2 +- .../feedback/models/FeedbackEmailType.kt | 8 +++ .../utils/EmailMessageBodyResolver.kt | 36 +++++++++++++ .../utils/EmailMessageTitleResolver.kt | 4 +- .../feedback/utils/EmailSubjectResolver.kt | 1 + features/staking/impl/build.gradle.kts | 1 + .../viewmodel/StakingViewModel.kt | 52 ++++++++++++++++++- 8 files changed, 106 insertions(+), 4 deletions(-) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index a3cbd7d4f8..30d5dd83a8 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -86,6 +86,12 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") } + fun addStakingInfo(validatorName: String?, transactionType: String?, unsignedTransaction: String?) { + builder.appendKeyValue("Validator", validatorName ?: "unknown") + builder.appendKeyValue("Action", transactionType ?: "unknown") + builder.appendKeyValue("Unsigned transaction", unsignedTransaction ?: "unknown") + } + fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun build(): String = builder.trimEnd().toString() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt index 596699a073..687a9859d1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt @@ -14,7 +14,7 @@ class SaveBlockchainErrorUseCase( private val feedbackRepository: FeedbackRepository, ) { - fun invoke(error: BlockchainErrorInfo) { + operator fun invoke(error: BlockchainErrorInfo) { feedbackRepository.saveBlockchainErrorInfo(error = error) } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 1e4971e4cf..1be0afb1ff 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -22,4 +22,12 @@ sealed interface FeedbackEmailType { /** User has problem with sending transaction */ data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType + + /** User has problem with staking */ + data class StakingProblem( + override val cardInfo: CardInfo, + val validatorName: String?, + val transactionType: String?, + val unsignedTransaction: String?, + ) : FeedbackEmailType } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index a9bb1a64dd..c314b7a4bc 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -23,6 +23,12 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) is FeedbackEmailType.ScanningProblem -> addScanningProblemBody() is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) + is FeedbackEmailType.StakingProblem -> addStakingProblemBody( + type.cardInfo, + type.validatorName, + type.transactionType, + type.unsignedTransaction, + ) } return build() @@ -72,6 +78,36 @@ internal class EmailMessageBodyResolver( addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } + private suspend fun FeedbackDataBuilder.addStakingProblemBody( + cardInfo: CardInfo, + validatorName: String?, + transactionType: String?, + unsignedTransaction: String?, + ) { + addCardInfo(cardInfo) + addDelimiter() + + val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) + val blockchainInfo = blockchainError?.let { + feedbackRepository.getBlockchainInfo( + userWalletId = userWalletId, + blockchainId = blockchainError.blockchainId, + derivationPath = blockchainError.derivationPath, + ) + } + + if (blockchainInfo != null) { + addBlockchainError(blockchainInfo, blockchainError) + addDelimiter() + } + + addStakingInfo(validatorName, transactionType, unsignedTransaction) + addDelimiter() + + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) { addCardInfo(cardInfo) addDelimiter() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 6ab001b775..bfa9cfdede 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -19,7 +19,9 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed - is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + is FeedbackEmailType.TransactionSendingProblem, + is FeedbackEmailType.StakingProblem, + -> R.string.feedback_preface_tx_failed } .let(resources::getString) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index beb89739bd..503f1f514b 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -26,6 +26,7 @@ internal class EmailSubjectResolver(private val resources: Resources) { is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + is FeedbackEmailType.StakingProblem -> R.string.feedback_subject_tx_failed } .let(resources::getString) } diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 9d23634914..808810f2d7 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory) + implementation(projects.domain.feedback) /** Common */ implementation(projects.common.ui) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 209c6c9304..391e3e0c02 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -17,6 +17,11 @@ import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.staking.* import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction @@ -24,6 +29,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase @@ -94,6 +100,9 @@ internal class StakingViewModel @Inject constructor( private val isApproveNeededUseCase: IsApproveNeededUseCase, private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, + private val feedbackManager: FeedbackManager, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -133,6 +142,8 @@ internal class StakingViewModel @Inject constructor( private var stakingApproval: StakingApproval = StakingApproval.Empty private val allowanceTaskScheduler = SingleTaskScheduler() + private var transactionInProgress: StakingTransaction? = null + private var approvalJobHolder: JobHolder = JobHolder() init { @@ -206,9 +217,13 @@ internal class StakingViewModel @Inject constructor( stakingEventFactory.createStakingErrorAlert(it) return@launch } + val gasEstimate = constructedTransaction.gasEstimate + ?: return@launch stakingEventFactory.createGenericErrorAlert("Gas estimate is null") + + transactionInProgress = constructedTransaction sendStakingTransaction( transactionId = constructedTransaction.id, - gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"), + gasEstimate = gasEstimate, txData = transactionData, pendingActionList = confirmationState.pendingActions, ) @@ -503,7 +518,39 @@ internal class StakingViewModel @Inject constructor( } override fun onFailedTxEmailClick(errorMessage: String) { - // TODO staking sending feedback email [REDACTED_TASK_KEY] + viewModelScope.launch { + val network = cryptoCurrencyStatus.currency.network + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } + val amountState = uiState.value.amountState as? AmountState.Data + val confirmationState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val feeState = confirmationState?.feeState as? FeeState.Content + + val validator = validatorState?.chosenValidator + val feeAmount = feeState?.fee?.amount + val amount = amountState?.amountTextField?.cryptoAmount + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage, + blockchainId = network.id.value, + derivationPath = network.derivationPath.value, + destinationAddress = validator?.address.orEmpty(), + tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol, + amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), + fee = feeAmount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), + ), + ) + + val email = FeedbackEmailType.StakingProblem( + cardInfo = cardInfo, + validatorName = validator?.name, + transactionType = transactionInProgress?.type?.name, + unsignedTransaction = transactionInProgress?.unsignedTransaction, + ) + + feedbackManager.sendEmail(email) + } } fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { @@ -601,6 +648,7 @@ internal class StakingViewModel @Inject constructor( stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { txHash -> + transactionInProgress = null submitHash(transactionId, txHash) scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( From f2585c4134d92d573b2a19c88fdc476254efd23d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 18:25:51 +0500 Subject: [PATCH 21/90] Updated on 2026-08-14 --- .../field/AmountFieldChangeTransformer.kt | 5 ++++ .../data/staking/DefaultStakingRepository.kt | 25 +++++++++++-------- .../model/stakekit/YieldBalanceList.kt | 7 ++---- .../staking/repositories/StakingRepository.kt | 2 ++ .../CurrenciesStatusesLceOperations.kt | 10 +++++--- .../CurrenciesStatusesOperations.kt | 10 +++++--- .../repository/MockStakingRepository.kt | 3 +++ .../AmountRequirementStateTransformer.kt | 5 ++++ gradle/dependencies.toml | 2 +- 9 files changed, 46 insertions(+), 23 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index d45e7c07bd..528d288023 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.common.ui.amountScreen.converters.field import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState @@ -81,6 +82,10 @@ class AmountFieldChangeTransformer( cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), isError = false, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), ), ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index c4b9b8fad1..05dfc8c0b3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -70,7 +70,7 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggle: StakingFeatureToggles, private val walletManagersFacade: WalletManagersFacade, - private val moshi: Moshi, + moshi: Moshi, ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() @@ -111,6 +111,10 @@ internal class DefaultStakingRepository( private val tronStakeKitTransactionAdapter: JsonAdapter = moshi.adapter(TronStakeKitTransaction::class.java) + override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { + rawNetworkId.plus(rawCurrencyId) + } + override fun isStakingSupported(integrationKey: String): Boolean { return integrationIdMap.containsKey(integrationKey) } @@ -165,7 +169,7 @@ internal class DefaultStakingRepository( val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) - val isSupported = isStakingSupported(cryptoCurrencyId.getIntegrationKey()) + val isSupported = isStakingSupported(getIntegrationKey(cryptoCurrencyId)) when { prefetchedYield != null && isSupported -> { @@ -273,7 +277,7 @@ internal class DefaultStakingRepository( ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: return@withContext val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() @@ -307,7 +311,7 @@ internal class DefaultStakingRepository( send(YieldBalance.Empty) } else { launch(dispatchers.io) { - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") stakingBalanceStore.get(userWalletId, integrationId) .collectLatest { @@ -340,7 +344,7 @@ internal class DefaultStakingRepository( } else { fetchSingleYieldBalance(userWalletId, cryptoCurrency) - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) ?: return@withContext YieldBalance.Error @@ -371,7 +375,7 @@ internal class DefaultStakingRepository( val availableCurrencies = cryptoCurrencies .mapNotNull { currency -> val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network) - val integrationId = integrationIdMap[currency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(currency.id)] if (integrationId != null && address != null) { address to integrationId @@ -561,7 +565,7 @@ internal class DefaultStakingRepository( } override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { - return when (cryptoCurrency.id.getIntegrationKey()) { + return when (getIntegrationKey(cryptoCurrency.id)) { Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> { StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) } @@ -575,6 +579,7 @@ internal class DefaultStakingRepository( Blockchain.Solana, Blockchain.Cosmos, -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) + Blockchain.BSC, Blockchain.Ethereum, -> TransactionData.Compiled.Data.RawString(unsignedTransaction) Blockchain.Tron -> { @@ -611,8 +616,6 @@ internal class DefaultStakingRepository( private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" - private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId) - private fun getTronResource(network: Network): TronResource? { val blockchain = Blockchain.fromNetworkId(network.backendId) @@ -629,11 +632,11 @@ internal class DefaultStakingRepository( const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" + const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" const val TRON_INTEGRATION_ID = "tron-trx-native-staking" const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" const val NEAR_INTEGRATION_ID = "near-near-native-staking" const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" @@ -645,11 +648,11 @@ internal class DefaultStakingRepository( Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID, Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, + Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, - // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, // Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID, // Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt index 01cc23bdc9..042aa9acbf 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt @@ -5,13 +5,10 @@ sealed class YieldBalanceList { data class Data( val balances: List, ) : YieldBalanceList() { - fun getBalance(rawCurrencyId: String?, networkName: String): YieldBalance { + fun getBalance(rawCurrencyId: String?): YieldBalance { return balances.firstOrNull { yield -> (yield as? YieldBalance.Data)?.balance?.items - ?.any { - rawCurrencyId == it.rawCurrencyId && - networkName.equals(it.rawNetworkId, ignoreCase = true) - } == true + ?.any { rawCurrencyId == it.rawCurrencyId } == true } ?: YieldBalance.Error } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 8a48c154a9..528f40dac0 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -22,6 +22,8 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface StakingRepository { + fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String + fun isStakingSupported(integrationKey: String): Boolean suspend fun fetchEnabledYields(refresh: Boolean) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 59461356b9..8d97dc1a83 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -144,10 +144,14 @@ internal class CurrenciesStatusesLceOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( - rawCurrencyId = currency.id.rawCurrencyId, - networkName = currency.network.name, + val isStakingSupported = stakingRepository.isStakingSupported( + stakingRepository.getIntegrationKey(currency.id), ) + val yieldBalance = if (isStakingSupported) { + (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + } else { + null + } createCurrencyStatus( currency = currency, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 55ba05a3d3..9954751464 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -260,10 +260,14 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( - rawCurrencyId = currency.id.rawCurrencyId, - networkName = currency.network.name, + val isStakingSupported = stakingRepository.isStakingSupported( + stakingRepository.getIntegrationKey(currency.id), ) + val yieldBalance = if (isStakingSupported) { + (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + } else { + null + } createCurrencyStatus( currency = currency, quote = quote, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index a19fea75b9..34ebfe1a1c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -24,6 +24,9 @@ import org.joda.time.DateTime import java.math.BigDecimal class MockStakingRepository : StakingRepository { + + override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = "" + override fun isStakingSupported(currencyId: String): Boolean = true override suspend fun fetchEnabledYields(refresh: Boolean) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index f16597ae54..59d4adfaf7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount +import androidx.compose.ui.text.input.ImeAction import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.extensions.resourceReference @@ -31,6 +32,7 @@ internal class AmountRequirementStateTransformer( val isRequirementError = isRequirementError(prevState, amountRequirements) return if (isRequirementError) { prevState.copy( + isPrimaryButtonEnabled = false, amountTextField = prevState.amountTextField.copy( isError = true, error = resourceReference( @@ -43,6 +45,9 @@ internal class AmountRequirementStateTransformer( ), ), ), + keyboardOptions = prevState.amountTextField.keyboardOptions.copy( + imeAction = ImeAction.None, + ), ), ) } else { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index dbe4ee3f6d..da78cb472c 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-740" +tangemBlockchainSdk = "develop-745" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From e356f020f383723802953e0a2aa32d8d011440a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 18:21:13 +0400 Subject: [PATCH 22/90] Updated on 2026-08-14 --- .../core/ui/components/token/TokenItem.kt | 68 +++++++++- .../components/token/internal/TokenPrice.kt | 55 ++++++-- .../components/token/state/TokenItemState.kt | 126 +++++++++++------- .../common/preview/WalletScreenPreviewData.kt | 6 +- .../converter/TokenItemStateConverter.kt | 8 +- 5 files changed, 191 insertions(+), 72 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 8bdd28b235..027db6c7d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -37,12 +37,43 @@ private enum class LayoutId { ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT } +/** + * Token item for non reorderable list + * + * @param state token item state + * @param isBalanceHidden flag that shows/hides balance + * @param modifier modifier + * + * @see Figma Component + */ +@Composable +fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + TokenItem( + state = state, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + reorderableTokenListState = null, + ) +} + +/** + * Token item for reorderable list + * + * @param state token item state + * @param isBalanceHidden flag that shows/hides balance + * @param reorderableTokenListState reorderable token list state + * @param modifier modifier + * + * @see Figma Component + */ @Composable fun TokenItem( state: TokenItemState, isBalanceHidden: Boolean, + reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier, - reorderableTokenListState: ReorderableLazyListState? = null, ) { val betweenRowsMargin = TangemTheme.dimens.spacing2 @@ -76,7 +107,7 @@ fun TokenItem( ) TokenPrice( - state = state.cryptoPriceState, + state = state.subtitleState, modifier = Modifier .layoutId(layoutId = LayoutId.CRYPTO_PRICE) .padding(end = TangemTheme.dimens.spacing8), @@ -195,6 +226,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier is TokenItemState.Unreachable, -> { firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + + if (state.subtitleState != null) { + secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + } } } @@ -232,7 +267,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier y = when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, - -> (layoutHeight - title.height).div(other = 2) + -> { + if (state.subtitleState == null) { + (layoutHeight - title.height).div(other = 2) + } else { + verticalPadding + } + } else -> verticalPadding }, ) @@ -411,7 +452,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider { + is TokenPriceState.CryptoPriceContent -> { PriceBlock( modifier = modifier, price = state.price, @@ -32,13 +37,12 @@ internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modi priceChangePercent = state.priceChangePercent, ) } - is TokenPriceChangeState.Unknown -> { - PriceText(text = DASH_SIGN, modifier = modifier) - } - is TokenPriceChangeState.Loading -> { + is TokenPriceState.TextContent -> PriceText(text = state.value, modifier = modifier) + is TokenPriceState.Unknown -> PriceText(text = DASH_SIGN, modifier = modifier) + is TokenPriceState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) } - is TokenPriceChangeState.Locked -> { + is TokenPriceState.Locked -> { LockedRectangle(modifier = modifier.placeholderSize()) } null -> Unit @@ -122,4 +126,37 @@ private fun Modifier.placeholderSize(): Modifier = composed { return@composed this .padding(vertical = TangemTheme.dimens.spacing2) .size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12) -} \ No newline at end of file +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview(@PreviewParameter(TokenPriceChangeStateProvider::class) state: TokenPriceState) { + TangemThemePreview { + TokenPrice(state = state) + } +} + +private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.UP, + ), + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.DOWN, + ), + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.NEUTRAL, + ), + TokenPriceState.TextContent(value = "Subtitle"), + TokenPriceState.Unknown, + TokenPriceState.Loading, + TokenPriceState.Locked, + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 832b849235..9c014e8067 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -4,58 +4,78 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType -/** Token item state */ +/** TokenItem component state */ @Immutable sealed class TokenItemState { + /** Unique id */ abstract val id: String + /** Token icon state */ abstract val iconState: CurrencyIconState + /** Token title state (in one row with [fiatAmountState]) */ abstract val titleState: TitleState + /** Token subtitle state (under [titleState] and in one row with [cryptoAmountState]) */ + abstract val subtitleState: SubtitleState? + + /** Token fiat amount state (in one row with [titleState]) */ abstract val fiatAmountState: FiatAmountState? + /** Token crypto amount state (under [fiatAmountState] and in one row with [subtitleState]) */ abstract val cryptoAmountState: CryptoAmountState? - abstract val cryptoPriceState: CryptoPriceState? - - /** Loading token state */ + /** + * Loading token state + * + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + */ data class Loading( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState.Content, + override val subtitleState: SubtitleState = SubtitleState.Loading, ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading - override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Loading } - /** Locked token state */ + /** + * Locked token state + * + * @property id unique id + */ data class Locked(override val id: String) : TokenItemState() { override val iconState: CurrencyIconState = CurrencyIconState.Locked override val titleState: TitleState = TitleState.Locked + override val subtitleState: SubtitleState = SubtitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked - override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Locked } /** * Content token state * - * @property id unique id - * @property iconState token icon state - * @property titleState token name - * @property onItemClick callback which will be called when an item is clicked - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property fiatAmountState token fiat amount + * @property cryptoAmountState token crypto amount + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState, override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, - override val cryptoPriceState: CryptoPriceState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() @@ -63,9 +83,10 @@ sealed class TokenItemState { /** * Draggable token state * - * @property id unique id - * @property iconState token icon state - * @property titleState token name + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property cryptoAmountState token crypto amount */ data class Draggable( override val id: String, @@ -73,48 +94,50 @@ sealed class TokenItemState { override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, ) : TokenItemState() { + override val subtitleState: SubtitleState? = null override val fiatAmountState: FiatAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } /** * Unreachable token state * - * @property id token id - * @property iconState token icon state - * @property titleState token name - * @property onItemClick callback which will be called when an item is clicked - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Unreachable( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState? = null, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } /** * No derivation address state * - * @property id token id - * @property iconState token icon state - * @property titleState token name - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property onItemLongClick callback which will be called when an item is long clicked */ data class NoAddress( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState? = null, val onItemLongClick: () -> Unit, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } @Immutable @@ -122,9 +145,27 @@ sealed class TokenItemState { data class Content(val text: String, val hasPending: Boolean = false) : TitleState() - object Loading : TitleState() + data object Loading : TitleState() - object Locked : TitleState() + data object Locked : TitleState() + } + + @Immutable + sealed class SubtitleState { + + data class CryptoPriceContent( + val price: String, + val priceChangePercent: String, + val type: PriceChangeType, + ) : SubtitleState() + + data class TextContent(val value: String) : SubtitleState() + + data object Unknown : SubtitleState() + + data object Loading : SubtitleState() + + data object Locked : SubtitleState() } @Immutable @@ -134,34 +175,19 @@ sealed class TokenItemState { val hasStaked: Boolean = false, ) : FiatAmountState() - object Loading : FiatAmountState() + data object Loading : FiatAmountState() - object Locked : FiatAmountState() + data object Locked : FiatAmountState() } @Immutable sealed class CryptoAmountState { data class Content(val text: String) : CryptoAmountState() - object Unreachable : CryptoAmountState() + data object Unreachable : CryptoAmountState() - object Loading : CryptoAmountState() + data object Loading : CryptoAmountState() - object Locked : CryptoAmountState() - } - - sealed class CryptoPriceState { - - data class Content( - val price: String, - val priceChangePercent: String?, - val type: PriceChangeType?, - ) : CryptoPriceState() - - object Unknown : CryptoPriceState() - - object Loading : CryptoPriceState() - - object Locked : CryptoPriceState() + data object Locked : CryptoAmountState() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 12dcfa641f..63585b4bbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -20,7 +20,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "0,35853044 BTC"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "34 496,75 \$", priceChangePercent = "0,43 %", type = PriceChangeType.DOWN, @@ -46,7 +46,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Ethereum"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "1,856660295 ETH"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "1 799,41 \$", priceChangePercent = "5,16 %", type = PriceChangeType.UP, @@ -68,7 +68,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Shiba Inu"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "6 200 220,00 SHIB"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "0.01 \$", priceChangePercent = "1,34 %", type = PriceChangeType.DOWN, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index e47f61c804..e9752df81b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -59,7 +59,7 @@ internal class TokenItemStateConverter( hasStaked = !getStakedBalance().isZero(), ), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), - cryptoPriceState = getCryptoPriceState(), + subtitleState = getCryptoPriceState(), onItemClick = { clickIntents.onTokenItemClick(this) }, onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) @@ -97,12 +97,12 @@ internal class TokenItemStateConverter( onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) - private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState { + private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.SubtitleState { val fiatRate = value.fiatRate val priceChange = value.priceChange return if (fiatRate != null && priceChange != null) { - TokenItemState.CryptoPriceState.Content( + TokenItemState.SubtitleState.CryptoPriceContent( price = fiatRate.getFormattedCryptoPrice(), priceChangePercent = BigDecimalFormatter.formatPercent( percent = priceChange, @@ -111,7 +111,7 @@ internal class TokenItemStateConverter( type = priceChange.getPriceChangeType(), ) } else { - TokenItemState.CryptoPriceState.Unknown + TokenItemState.SubtitleState.Unknown } } From 71dfa1c99f14a593fa1a92f38965d8ddc69d27bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Aug 2024 15:06:14 +0300 Subject: [PATCH 23/90] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 8 ++++-- core/res/src/main/res/values-ja/strings.xml | 7 +++-- core/res/src/main/res/values-ru/strings.xml | 3 ++- .../src/main/res/values-uk-rUA/strings.xml | 3 ++- .../src/main/res/values-zh-rTW/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 8 +++--- .../data/staking/DefaultStakingRepository.kt | 4 +-- .../domain/staking/model/StakingEntryInfo.kt | 5 ++-- .../repository/MockStakingRepository.kt | 4 +-- .../impl/presentation/state/FeeState.kt | 2 +- .../state/InnerConfirmationStakingState.kt | 2 +- .../presentation/state/StakingNotification.kt | 21 +++++++-------- .../impl/presentation/state/StakingUiState.kt | 2 +- .../state/bottomsheet/InfoType.kt | 9 +++++++ .../StakingInfoBottomSheetConfig.kt | 2 +- .../ConfirmationStatePreviewData.kt | 8 ++++-- .../state/stub/StakingClickIntentsStub.kt | 4 +-- .../SetConfirmationStateLoadingTransformer.kt | 8 ++++-- .../SetInitialDataStateTransformer.kt | 1 + .../ShowInfoBottomSheetStateTransformer.kt | 9 +------ .../amount/AmountChangeStateTransformer.kt | 2 +- .../amount/AmountMaxValueStateTransformer.kt | 2 +- .../viewmodel/StakingClickIntents.kt | 2 +- .../viewmodel/StakingViewModel.kt | 9 ++++--- .../tokendetails/TokenDetailsPreviewData.kt | 11 +++++--- .../tokendetails/state/StakingBlockUM.kt | 5 ++-- .../TokenDetailsLoadedBalanceConverter.kt | 17 ++++++++---- .../factory/TokenStakingStateConverter.kt | 22 +++++++++++----- .../tokendetails/state/utils/BalanceUtils.kt | 2 +- .../state/utils/RewardScheduleUtils.kt | 26 +++++++++++++++++++ .../components/staking/TokenStakingBlock.kt | 18 +++++-------- 31 files changed, 146 insertions(+), 81 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 21258cdee1..cc74abea34 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -835,8 +835,8 @@ Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. - Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. - Genehmigung in Arbeit + Das Genehmigungsverfahren ist derzeit im Gange und wird in Kürze abgeschlossen sein + Genehmigung läuft Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. @@ -854,12 +854,16 @@ Netzwerk erfordert eine Mindesteinzahlung Der Swap wird nach Abschluss der Transaktion %s verfügbar sein. Du hast aktive Transaktion + Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. + Genehmigung in Arbeit Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt. Du hast keine %s Coins in deiner Liste Keine Token zum Tauschen verfügbar Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s Die Gebühr %s kann nicht gedeckt werden Der zu erhaltende Betrag muss mindestens %s betragen + Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %@) + Ausgewähltes Paar vorübergehend nicht verfügbar Service vorübergehend nicht verfügbar Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s Der zu tauschende Betrag muss mindestens %s betragen diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 046d0156bc..70892f297f 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -98,6 +98,7 @@ 有効にする 有効 エラー + 交換 移動する 取引履歴を調べる エクスプローラー @@ -823,8 +824,8 @@ ウォレット設定 Tangem %sを使用するか、カードをスキャンしてウォレットにアクセスしてください - スワップの承認は現在進行中で、まもなく完了する予定です。 - 承認が進行中 + 許可付与のプロセスは現在進行中であり、まもなく完了する予定です。 + 承認中 カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 @@ -842,6 +843,8 @@ ネットワークには最低残高が必要です スワップは、%s の取引完了後に利用可能となります。 アクティブな取引があります + スワップの承認は現在進行中で、まもなく完了する予定です。 + 承認が進行中 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 あなたのリストには、交換可能な %s トークンがありません。 スワップ可能なトークンがありません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index fe433aaed9..723c6c4ffb 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -684,7 +684,7 @@ Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. - Повторный стейкинг + Сменить валидатора Застейкать вознаграждения Отозвать Переголосовать @@ -702,6 +702,7 @@ Стейкинг закрыт Застейкать еще Застейкать %s + Вывести %s Разблокировать Выведено из стейкинга Проверьте процесс завершения стейкинга, чтобы вывести свои средства. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 62d3baa6e6..10a8cd574b 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -704,7 +704,7 @@ Місяць Тиждень Винагороди - Застейкати + Стейкінг закрито Застейкати більше Застейкати %s Зняти зі стейкінгу %s @@ -869,6 +869,7 @@ Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s Неможливо покрити комісію %s Сума отримання не може бути меншою за %s + Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %@) Сервіс тимчасово недоступний Сума до обміну не повинна перевищувати %s Сума для обміну має бути не менше %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index eb5bc2de81..3c2329daab 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -63,6 +63,7 @@ OK 主卡片 + %1$s-%2$s 拒絕 重新命名 保存設置 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 79135b1d0d..08e26457c7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -669,10 +669,10 @@ The allocated time for activating participation in staking. Migrate Native staking - Staking allow you to earn %1$s. Your staking rewards arrive every day. - Staking allow you to earn %1$s. Your staking rewards arrive every hour. - Staking allow you to earn %1$s. Your staking rewards arrive every month. - Staking allow you to earn %1$s. Your staking rewards arrive every week. + Staking allows you to earn %1$s. Your staking rewards arrive every day. + Staking allows you to earn %1$s. Your staking rewards arrive every hour. + Staking allows you to earn %1$s. Your staking rewards arrive every month. + Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards Rewards stop accruing immediately after you unstake. The unstaking process takes %s. Rebond diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index c4b9b8fad1..07357b9c71 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -148,8 +148,8 @@ internal class DefaultStakingRepository( val yield = getYield(cryptoCurrencyId, symbol) StakingEntryInfo( - interestRate = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), - periodInDays = yield.metadata.cooldownPeriod.days, + apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), + rewardSchedule = yield.metadata.rewardSchedule, tokenSymbol = yield.token.symbol, ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt index 691aece740..c400337e61 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt @@ -1,9 +1,10 @@ package com.tangem.domain.staking.model +import com.tangem.domain.staking.model.stakekit.Yield import java.math.BigDecimal data class StakingEntryInfo( - val interestRate: BigDecimal, - val periodInDays: Int, + val apr: BigDecimal, + val rewardSchedule: Yield.Metadata.RewardSchedule, val tokenSymbol: String, ) \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index a19fea75b9..59137dec80 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -32,9 +32,9 @@ class MockStakingRepository : StakingRepository { override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo = StakingEntryInfo( - interestRate = 1.toBigDecimal(), - periodInDays = 2, + apr = 1.toBigDecimal(), tokenSymbol = "SOL", + rewardSchedule = Yield.Metadata.RewardSchedule.DAY, ) override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = Yield( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt index ad93ecd3a3..d74bc8fe32 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt @@ -6,7 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal @Immutable -sealed class FeeState { +internal sealed class FeeState { data class Content( val fee: Fee?, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt index babc93a2dd..29ce1f54f5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state -enum class InnerConfirmationStakingState { +internal enum class InnerConfirmationStakingState { ASSENT, IN_PROGRESS, COMPLETED, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index c338f2c047..f9da881678 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -1,10 +1,7 @@ package com.tangem.features.staking.impl.presentation.state 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.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.features.staking.impl.R internal sealed class StakingNotification(val config: NotificationConfig) { @@ -50,14 +47,10 @@ internal sealed class StakingNotification(val config: NotificationConfig) { ), ) { data class EarnRewards( - val subtitleResourceId: Int, - val currencyName: String, + val subtitleText: TextReference, ) : Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), - subtitle = resourceReference( - subtitleResourceId, - wrappedList(currencyName), - ), + subtitle = subtitleText, ) data class Unstake( @@ -66,7 +59,13 @@ internal sealed class StakingNotification(val config: NotificationConfig) { title = resourceReference(R.string.common_unstake), subtitle = resourceReference( R.string.staking_notification_unstake_text, - wrappedList(cooldownPeriodDays, cooldownPeriodDays), + wrappedList( + pluralReference( + id = R.plurals.common_days, + count = cooldownPeriodDays, + formatArgs = wrappedList(cooldownPeriodDays), + ), + ), ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 4b000bc130..0d7e1081b8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -9,8 +9,8 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt new file mode 100644 index 0000000000..b923734491 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt @@ -0,0 +1,9 @@ +package com.tangem.features.staking.impl.presentation.state.bottomsheet + +internal enum class InfoType { + ANNUAL_PERCENTAGE_RATE, + UNBONDING_PERIOD, + REWARD_CLAIMING, + WARMUP_PERIOD, + REWARD_SCHEDULE, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt index f8cc06e495..12b327a0d0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.bottomsheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference -data class StakingInfoBottomSheetConfig( +internal data class StakingInfoBottomSheetConfig( val title: TextReference, val text: TextReference, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 807e584ca3..f8a64a3695 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -3,6 +3,8 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType.Coin import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R @@ -81,8 +83,10 @@ internal object ConfirmationStatePreviewData { footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", notifications = persistentListOf( StakingNotification.Warning.EarnRewards( - currencyName = "Solana", - subtitleResourceId = R.string.staking_notification_earn_rewards_text_period_day, + subtitleText = resourceReference( + id = R.string.staking_notification_earn_rewards_text_period_day, + formatArgs = wrappedList("Solana"), + ), ), ), transactionDoneState = TransactionDoneState.Empty, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 55d130824f..7220af1f41 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -4,12 +4,12 @@ import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList @Suppress("TooManyFunctions") -object StakingClickIntentsStub : StakingClickIntents { +internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 1e77078e6e..07ba1f9fce 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,5 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R @@ -45,8 +47,10 @@ internal class SetConfirmationStateLoadingTransformer( ) } else { StakingNotification.Warning.EarnRewards( - currencyName = yield.token.name, - subtitleResourceId = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + subtitleText = resourceReference( + id = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + formatArgs = wrappedList(yield.token.name), + ), ) }, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 8d8a69e019..2c0bcbef78 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -18,6 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index 06f89bdd94..f0e0ade2cc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig import com.tangem.utils.transformer.Transformer @@ -42,12 +43,4 @@ internal class ShowInfoBottomSheetStateTransformer( ), ) } -} - -enum class InfoType { - ANNUAL_PERCENTAGE_RATE, - UNBONDING_PERIOD, - REWARD_CLAIMING, - WARMUP_PERIOD, - REWARD_SCHEDULE, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index b87f8a0251..098e959f7c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -8,8 +8,8 @@ import com.tangem.utils.transformer.Transformer internal class AmountChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val yield: Yield, private val value: String, + yield: Yield, ) : Transformer { private val amountRequirementStateTransformer = AmountRequirementStateTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index ae53eab63e..6314597d0d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.utils.transformer.Transformer internal class AmountMaxValueStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val yield: Yield, + yield: Yield, ) : Transformer { private val amountRequirementStateTransformer = AmountRequirementStateTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index f8fb30cfa9..fee1e2115d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 209c6c9304..3dbc94500f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -43,6 +43,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer @@ -327,7 +328,7 @@ internal class StakingViewModel @Inject constructor( } override fun onInitialInfoBannerClick() { - // innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) + innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } override fun onInfoClick(infoType: InfoType) { @@ -339,7 +340,7 @@ internal class StakingViewModel @Inject constructor( } override fun onAmountValueChange(value: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, value)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value, yield)) } override fun onAmountPasteTriggerDismiss() { @@ -369,7 +370,7 @@ internal class StakingViewModel @Inject constructor( StakingActionCommonType.PENDING_OTHER } stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) onNextClick(actionType, activeStake.pendingActions) } @@ -696,7 +697,7 @@ internal class StakingViewModel @Inject constructor( } private companion object { - const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking" + const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L const val BALANCE_UPDATE_DELAY = 11_000L } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index e7625b4013..9c69cd0b55 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -143,9 +143,14 @@ internal object TokenDetailsPreviewData { val stakingErrorBlock = StakingBlockUM.Error(iconState) val stakingAvailableBlock = StakingBlockUM.StakeAvailable( - interestRate = "7.38", - periodInDays = 4, - tokenSymbol = "XLM", + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList("3.27%"), + ), + subtitleText = resourceReference( + id = R.string.staking_notification_earn_rewards_text_period_day, + formatArgs = wrappedList("Solana"), + ), iconState = iconState, onStakeClicked = {}, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt index 91d76db70d..b29d476a32 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt @@ -21,9 +21,8 @@ internal sealed interface StakingBlockUM { data class StakeAvailable( val iconState: IconState, - val interestRate: String, - val periodInDays: Int, - val tokenSymbol: String, + val titleText: TextReference, + val subtitleText: TextReference, val onStakeClicked: () -> Unit, ) : StakingBlockUM } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 6aa3923cd9..0555333c41 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -22,6 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R @@ -191,13 +192,19 @@ internal class TokenDetailsLoadedBalanceConverter( stakingEntryInfo: StakingEntryInfo, iconState: IconState, ): StakingBlockUM.StakeAvailable { + val apr = BigDecimalFormatter.formatPercent( + percent = stakingEntryInfo.apr, + useAbsoluteValue = true, + ) return StakingBlockUM.StakeAvailable( - interestRate = BigDecimalFormatter.formatPercent( - percent = stakingEntryInfo.interestRate, - useAbsoluteValue = true, + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList(apr), + ), + subtitleText = resourceReference( + id = stakingEntryInfo.rewardSchedule.getStringResourceId(), + formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), ), - periodInDays = stakingEntryInfo.periodInDays, - tokenSymbol = stakingEntryInfo.tokenSymbol, iconState = iconState, onStakeClicked = clickIntents::onStakeBannerClick, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 3bc3e36023..b12e12eb53 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -1,12 +1,16 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -24,14 +28,20 @@ internal class TokenStakingStateConverter( ifLeft = { StakingBlockUM.Error(iconState = iconState) }, - ifRight = { + ifRight = { stakingEntryInfo -> + val apr = BigDecimalFormatter.formatPercent( + percent = stakingEntryInfo.apr, + useAbsoluteValue = true, + ) StakingBlockUM.StakeAvailable( - interestRate = BigDecimalFormatter.formatPercent( - percent = it.interestRate, - useAbsoluteValue = true, + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList(apr), + ), + subtitleText = resourceReference( + id = stakingEntryInfo.rewardSchedule.getStringResourceId(), + formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), ), - periodInDays = it.periodInDays, - tokenSymbol = it.tokenSymbol, iconState = iconState, onStakeClicked = clickIntents::onStakeBannerClick, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt index faa0144943..fe714ff2ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt @@ -3,7 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType import java.math.BigDecimal -fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { +internal fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) { this.plus(stakingAmount) } else { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt new file mode 100644 index 0000000000..8bb6550f79 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils + +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.features.tokendetails.impl.R + +internal fun Yield.Metadata.RewardSchedule.getStringResourceId(): Int { + return when (this) { + Yield.Metadata.RewardSchedule.BLOCK, + Yield.Metadata.RewardSchedule.DAY, + Yield.Metadata.RewardSchedule.ERA, + Yield.Metadata.RewardSchedule.EPOCH, + -> R.string.staking_notification_earn_rewards_text_period_day + + Yield.Metadata.RewardSchedule.HOUR, + -> R.string.staking_notification_earn_rewards_text_period_hour + + Yield.Metadata.RewardSchedule.WEEK, + -> R.string.staking_notification_earn_rewards_text_period_week + + Yield.Metadata.RewardSchedule.MONTH, + -> R.string.staking_notification_earn_rewards_text_period_month + + else + -> R.string.staking_notification_earn_rewards_text_period_day + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index c2d7940cbc..9d0087292d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerW8 +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.res.TangemThemePreview import com.tangem.core.ui.utils.getGreyScaleColorFilter @@ -86,10 +88,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi SpacerW8() Column { Text( - text = stringResource( - R.string.token_details_staking_block_title, - state.interestRate, - ), + text = state.titleText.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, ) @@ -97,11 +96,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) Text( - text = stringResource( - R.string.token_details_staking_block_subtitle, - state.tokenSymbol, - state.periodInDays, - ), + text = state.subtitleText.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) @@ -161,8 +156,9 @@ private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) } SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = "Loading", // TODO staking - onClick = { /* [REDACTED_TODO_COMMENT] */ }, + showProgress = true, + text = TextReference.EMPTY.resolveReference(), + onClick = { /* no-op */ }, ) } } From cb203637f5d285fc1ef7299eefbc807cbef0b07a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Aug 2024 21:03:42 +0500 Subject: [PATCH 24/90] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 3 + core/res/src/main/res/values/strings.xml | 14 ++++- .../converters/YieldBalanceConverter.kt | 1 + .../staking/model/stakekit/YieldBalance.kt | 2 + .../converters/YieldBalancesConverter.kt | 43 ++++++++++---- .../ui/StakingInitialInfoContent.kt | 56 ++++++++++--------- 6 files changed, 81 insertions(+), 38 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index cc74abea34..d7bd63adac 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -100,6 +100,7 @@ Aktivieren Aktiviert Fehler + Umtausch Erkunden Transaktionsverlauf einsehen Explorer @@ -878,6 +879,8 @@ Auf dieser Karte sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben. Geringe Anzahl von Unterschriften Token in verschiedenen Netzwerken können unterschiedliche Adressen haben. Überprüfe bei der Überweisung noch einmal, ob deine Adresse mit der des Netzwerks übereinstimmt. + MATIC wird auf POL migriert. Es gibt jedoch keine Frist, und MATIC wird noch nicht abgeschafft. Du kannst MATIC-Token weiterhin verwenden oder sie über eien Exchange gegen POL tauschen. + Migration von MATIC zu POL Verwende deine Karte, um eine Adresse für das %d-Netz zu erhalten Verwende deine Karte, um mehrere Adressen für die %d-Netzwerke zu erhalten diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 08e26457c7..86185e2839 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -114,6 +114,10 @@ Go to provider Go to token Import + + in %d day + in %d days + Later Locked Main network @@ -656,12 +660,12 @@ Market rating Metrics Minimum Requirement - No rewards to claim + No rewards Reward claiming Method of receiving staking rewards.\nIt can be either automatic, where the reward is credited to your address, or manual, where you need to withdraw the reward by creating a transaction to receive it. Reward schedule This is a schedule that determines when participants in staking receive their rewards. - Rewards to claim: %s + Rewards: %s Staking %s Unbonding Period The period you must wait after requesting to withdraw funds from staking before the tokens become available. @@ -674,7 +678,8 @@ Staking allows you to earn %1$s. Your staking rewards arrive every month. Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards - Rewards stop accruing immediately after you unstake. The unstaking process takes %s. + Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. + Ready to withdraw Rebond Restake Restake rewards @@ -695,6 +700,7 @@ Stake more Stake %s Unstake %s + Unbonding Unlock locked Unstaked Check unstaked to claim your assets @@ -876,6 +882,8 @@ Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. + MATIC is being migrated to POL. However there is no deadline set and MATIC isn\'t being deprecated yet. You can safely continue using MATIC token or use exchanges to swap it for POL. + MATIC to POL Migration Use your card to get an address for %d network Use your card to get an addresses for %d networks diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index b1c9d7d47c..ec86b52e73 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -26,6 +26,7 @@ internal class YieldBalanceConverter : Converter, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 85b64c0645..9be3086028 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -1,10 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.common.extensions.isZero -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -18,6 +15,8 @@ import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.toPersistentList +import org.joda.time.DateTime +import java.util.Calendar internal class YieldBalancesConverter( private val cryptoCurrencyStatusProvider: Provider, @@ -88,7 +87,7 @@ internal class YieldBalancesConverter( } val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - val unbondingPeriod = yield.metadata.cooldownPeriod.days + val unbonding = getUnbondingDate(balance.date) validator?.let { BalanceState( validator = validator, @@ -108,11 +107,7 @@ internal class YieldBalancesConverter( ), ), rawCurrencyId = balance.rawCurrencyId, - unbondingPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(unbondingPeriod), - ), + unbondingPeriod = unbonding, pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -153,4 +148,32 @@ internal class YieldBalancesConverter( BalanceType.UNKNOWN, -> false } + + private fun getUnbondingDate(date: DateTime?): TextReference { + val now = DateTime.now().millis + val nowCalendar = Calendar.getInstance() + nowCalendar.resetHours() + + val endDate = Calendar.getInstance() + endDate.timeInMillis = date?.millis ?: now + endDate.resetHours() + + val days = ((endDate.timeInMillis - nowCalendar.timeInMillis) / DAY_IN_MILLIS).toInt() + return if (days > 0) { + pluralReference(R.plurals.common_in_days, days, wrappedList(days)) + } else { + resourceReference(R.string.common_today) + } + } + + private fun Calendar.resetHours() { + this[Calendar.HOUR_OF_DAY] = 0 + this[Calendar.MINUTE] = 0 + this[Calendar.SECOND] = 0 + this[Calendar.MILLISECOND] = 0 + } + + private companion object { + const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000 + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 6174dcc79c..8bd4543778 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -232,31 +232,37 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli @Composable private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextReference { - return if (balanceType == BalanceType.UNSTAKING) { - combinedReference( - resourceReference(R.string.staking_details_unbonding_period), - annotatedReference { - appendSpace() - appendColored( - text = balance.unbondingPeriod.resolveReference(), - color = TangemTheme.colors.text.accent, - ) - }, - ) - } else { - combinedReference( - resourceReference(R.string.app_name), - annotatedReference { - appendSpace() - appendColored( - text = BigDecimalFormatter.formatPercent( - percent = balance.validator.apr.orZero(), - useAbsoluteValue = true, - ), - color = TangemTheme.colors.text.accent, - ) - }, - ) + return when (balanceType) { + BalanceType.UNSTAKING -> { + combinedReference( + resourceReference(R.string.staking_unbonding), + annotatedReference { + appendSpace() + appendColored( + text = balance.unbondingPeriod.resolveReference(), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } + BalanceType.UNSTAKED -> { + resourceReference(R.string.staking_ready_to_withdraw) + } + else -> { + combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent( + percent = balance.validator.apr.orZero(), + useAbsoluteValue = true, + ), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } } } From 56992e44313f223a39030f8558d545813ce0750d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Aug 2024 15:00:24 +0400 Subject: [PATCH 25/90] Updated on 2026-08-14 --- .../portfolio/impl/ui/PortfolioItem.kt | 330 ++++-------------- .../preview/PreviewMyPortfolioUMProvider.kt | 23 +- .../impl/ui/state/PortfolioTokenUM.kt | 32 +- 3 files changed, 91 insertions(+), 294 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt index 3a98293707..035d1dbb91 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -1,58 +1,26 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.StringsSigns +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState -// TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement) -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - Column(modifier) { - Row( - modifier = Modifier - .combinedClickable( - onClick = { - hapticManager.perform(TangemHapticEffect.View.ContextClick) - state.onClick() - }, - onLongClick = { - hapticManager.perform(TangemHapticEffect.View.LongPress) - state.onLongTap() - }, - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .padding( - vertical = TangemTheme.dimens.spacing15, - horizontal = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Content(state) - } + TokenItem(state = state.tokenItemState, isBalanceHidden = state.isBalanceHidden) PortfolioQuickActions( modifier = Modifier.padding( @@ -68,225 +36,77 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie } } -@Composable -private fun RowScope.Content(state: PortfolioTokenUM) { - // TODO add custom token - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, // TODO add disabled state - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - - Column( - modifier = Modifier.align(Alignment.CenterVertically), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - when (state.balanceContent) { - is PortfolioTokenUM.BalanceContent.Disabled -> { - Disabled( - state = state, - disabledText = state.balanceContent.text.resolveReference(), - ) - } - PortfolioTokenUM.BalanceContent.Loading -> { - Loading(state = state) - } - is PortfolioTokenUM.BalanceContent.TokenBalance -> { - TokenBalance( - state = state, - content = state.balanceContent, - ) - } - } - } -} - -@Composable -private fun ColumnScope.TokenBalance(state: PortfolioTokenUM, content: PortfolioTokenUM.BalanceContent.TokenBalance) { - val balance = if (content.hidden) { - StringsSigns.STARS - } else { - content.balance - } - val tokenAmount = if (content.hidden) { - StringsSigns.STARS - } else { - content.tokenAmount - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.alignByBaseline(), - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.alignByBaseline(), - text = balance, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Text( - text = tokenAmount, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -private fun ColumnScope.Loading(state: PortfolioTokenUM) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.alignByBaseline(), - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - TextShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size40) - .alignByBaseline(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - TextShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size40) - .alignByBaseline(), - style = TangemTheme.typography.caption2, - textSizeHeight = true, - ) - } -} - -@Composable -private fun Disabled(state: PortfolioTokenUM, disabledText: String, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - Modifier.weight(1f), - ) { - Text( - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - - Text( - text = disabledText, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview() { +private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) { TangemThemePreview { - var quickActionsShown by remember { mutableStateOf(false) } - var quickActionsShown2 by remember { mutableStateOf(false) } - val sampleToken = PreviewMyPortfolioUMProvider().sampleToken + var quickActionsShown by remember { mutableStateOf(value = false) } - Box( - Modifier - .fillMaxSize() - .background(TangemTheme.colors.background.primary), - ) { - Column { - PortfolioItem( - state = sampleToken - .copy( - onClick = { - if (quickActionsShown2) { - quickActionsShown2 = false - } - quickActionsShown = quickActionsShown.not() - }, - isQuickActionsShown = quickActionsShown, - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - onClick = { - if (quickActionsShown) { - quickActionsShown = false - } - quickActionsShown2 = quickActionsShown2.not() - }, - isQuickActionsShown = quickActionsShown2, - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - balanceContent = ( - sampleToken.balanceContent - as PortfolioTokenUM.BalanceContent.TokenBalance - ) - .copy(hidden = true), - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - balanceContent = PortfolioTokenUM.BalanceContent.Disabled( - stringReference("No Address"), - ), - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy(balanceContent = PortfolioTokenUM.BalanceContent.Loading), - lastInList = true, - ) - } + val onItemClick = { + quickActionsShown = quickActionsShown.not() } + + PortfolioItem( + state = tokenUM.copy( + tokenItemState = when (tokenUM.tokenItemState) { + is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + else -> tokenUM.tokenItemState + }, + isQuickActionsShown = quickActionsShown, + ), + lastInList = true, + ) + } +} + +private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider( + collection = listOf( + tokenUM.copy( + tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy( + fiatAmountState = contentFiatAmount.copy(hasStaked = true), + ), + ), + tokenUM.copy( + tokenItemState = tokenUM.tokenItemState.copy( + fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN), + cryptoAmountState = tokenUM.tokenItemState.cryptoAmountState.copy(text = DASH_SIGN), + ), + ), + tokenUM.copy(isBalanceHidden = true), + tokenUM.copy( + tokenItemState = TokenItemState.Unreachable( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemClick = {}, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.NoAddress( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.Loading( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content, + subtitleState = tokenUM.tokenItemState.subtitleState, + ), + ), + ), +) { + + companion object { + val tokenUM = PreviewMyPortfolioUMProvider().sampleToken + val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as TokenFiatAmountState.Content } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index b607386001..1f21b46f5b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -1,6 +1,8 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import kotlinx.collections.immutable.persistentListOf @@ -32,19 +34,18 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider - Сайт блокчейна + Веб-сайт Покупательское предпочтение Разница между объемом покупателей и продавцов Циркулирующее предложение @@ -577,18 +583,11 @@ Комиссия, которую нужно заплатить за использование каждого неиспользованного выхода транзакции (UTXO) в сети Kaspa. Чем больше UTXO вы используете в транзакции, тем выше будет комиссия. KAS за UTXO %1$s, %2$s - Адрес Код назначения Введите адрес Адрес совпадает с адресом кошелька - Недопустимый Tag. Он не будет добавлен в транзакцию. Недопустимый Memo. Он не будет добавлен в транзакцию. - Tag Memo - Включая комиссию - Низкая - Нормальная - Приоритетная Проверьте своё интернет соединение Информация о комиссии сети недоступна Из @@ -603,7 +602,7 @@ Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе. + Для сохранения вашего аккаунта в блокчейне и защиты от возможных рисков необходим баланс не менее %s. Эта сумма останется на вашем счете и не может быть снята. Экзистенциальный депозит Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Установлена высокая комиссия @@ -637,21 +636,15 @@ Отправка %s Вы отправляете **%1$s**, включая комиссию сети %2$s Вы отправляете **%1$s** и %2$s - Отправка %s - Всего - %1$s и %2$s будет отправлено - ≈ %1$s (вкл. комиссию: %2$s) - %s будет отправлено Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. - Неверный адрес Транзакция отправлена Подготовьтесь к сканированию карты, которую вы хотите настроить. Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя Активно - Для завершения стейкинга нажмите сюда + Для завершения стейкинга нажмите на блок выше Сумма для стейкинга должна быть не менее %s Забрать средства Годовая процентная ставка @@ -682,12 +675,12 @@ Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый месяц. Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг - Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. + Награда перестанет начисляться сразу после того, как вы начнете процесс завершения стейкинга. Процесс завершения длится %s. Сменить валидатора Застейкать вознаграждения Отозвать Переголосовать - Автоматически + Авто Вручную Блок День @@ -859,6 +852,8 @@ Для работы с сетью необходим депозит Обмен будет доступен после завершения %s транзакции У вас есть активная транзакция + Разрешение обмена в процессе и будет скоро завершено + Разрешение в процессе Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. У вас в списке нет монет доступных для обмена с %s Нет доступных для обмена токенов @@ -881,6 +876,8 @@ На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. Малое количество подписей Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. + Токен MATIC мигрирует на POL. Однако крайний срок не установлен, и MATIC пока не устарел. Вы можете спокойно продолжать использовать токен MATIC или воспользоваться биржами, чтобы обменять его на POL. + Миграция MATIC в POL Используйте вашу карту, чтобы получить адрес для %d сети Используйте вашу карту, чтобы получить адреса для %d сетей diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 329dcfb63e..fce9968300 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -104,7 +104,6 @@ Explore Explore transaction history Explorer - Fee Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s Fast Market @@ -173,6 +172,8 @@ Contract address Contract address is invalid Please select the network + This token has already been added to the list + Token already exists Decimal must be a valid integer, up to %li Custom derivation E. g. m/00\'/0000\'/0\'/0/0 @@ -348,6 +349,7 @@ My portfolio Market To generate addresses for selected networks, you must scan your Tangem Wallet card + This section’s data is sourced from the following networks: %s Unable to load the data… Quick actions Result @@ -374,7 +376,7 @@ Based on %d rating Based on %d ratings - Blockchain site + Website Buy pressure The difference between buyers volume and sellers volume Circulating supply @@ -572,18 +574,11 @@ The fee required for using each unspent transaction output (UTXO) in the Kaspa network. The more UTXOs you use in a transaction, the higher the fee will be. KAS per UTXO %1$s, %2$s - Address Destination Tag Enter address Address is the same as wallet address - Invalid Tag. It won\'t be added to the transaction. Invalid Memo. It won\'t be added to the transaction. - Tag Memo - Include fee - Low - Normal - Priority Check your network connection Network fee info unreachable From @@ -598,7 +593,7 @@ Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - The account will be wiped from the blockchain if a balance goes below the existential deposit. Please leave %s on your balance. + A balance of at least %s is required to keep your account on the blockchain to prevent security risks. This amount will remain in your balance and cannot be withdrawn. Existential deposit The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. Custom fee is high @@ -632,21 +627,15 @@ Send %s You are sending **%1$s** including a network fee of %2$s You are sending **%1$s** and %2$s - Sending %s - Total - %1$s and %2$s will be sent - ≈ %1$s (inc. fee: %2$s) - %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. - Invalid address Transaction sent Prepare to scan card you want to setup. Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name Active - To unstake your assets, click here. + To unstake your assets, tap the block above The amount to stake must be at least %s Claim unstaked Annual percentage rate diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 8a455b1b25..187e4090fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -101,7 +101,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { title = resourceReference(R.string.send_notification_existential_deposit_title), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), + text = resourceReference(R.string.common_ok), onClick = onConfirmClick, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 5f05279157..f31136c8dd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -475,10 +475,7 @@ internal class StateBuilder( ), iconResId = R.drawable.ic_alert_circle_24, buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference( - R.string.send_notification_leave_button, - wrappedList(deposit), - ), + text = resourceReference(R.string.common_ok), onClick = { actions.onLeaveExistentialDeposit( SwapAmount( From 6f80bdb6367fc60a5047cc9e2a5bbcff677e6c42 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 18:17:33 +0300 Subject: [PATCH 46/90] Updated on 2026-08-14 --- .../sdk/mocks/content/WalletMockContent.kt | 11 +++++++ .../presentation/state/fee/FeeStateFactory.kt | 29 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt index 8e059f5450..6168bdda1f 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -111,6 +111,10 @@ object WalletMockContent : MockContent { publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), ), extendedPublicKey = ExtendedPublicKey( publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), @@ -189,6 +193,13 @@ object WalletMockContent : MockContent { parentFingerprint = byteArrayOf(0, 0, 0, 0), childNumber = 0, ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), ), ), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 3a30e424b9..3ff511170b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -1,10 +1,12 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.extensions.isZero +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.features.send.impl.presentation.state.SendNotification @@ -15,6 +17,7 @@ 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 com.tangem.blockchain.common.AmountType as SdkAmountType /** * Factory to produce fee state for [SendUiState] @@ -94,7 +97,7 @@ internal class FeeStateFactory( feeState = feeState.copy( feeSelectorState = updatedFeeSelectorState, fee = fee, - isFeeApproximate = isFeeApproximate(fee), + isFeeApproximate = isFeeApproximate(state.amountState), ), ) } @@ -180,11 +183,29 @@ internal class FeeStateFactory( return noErrors && (isNotEmptyCustom || isNotCustom) } - private fun isFeeApproximate(fee: Fee): Boolean { + private fun isFeeApproximate(state: AmountState): Boolean { val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false + val amount = (state as? AmountState.Data)?.amountTextField?.cryptoAmount ?: return false return isFeeApproximateUseCase( networkId = cryptoCurrencyStatus.currency.network.id, - amountType = fee.amount.type, + amountType = amount.type.toSdkAmountType(), ) } + + private fun AmountType.toSdkAmountType(): SdkAmountType { + return when (this) { + AmountType.CoinType -> SdkAmountType.Coin + is AmountType.FiatType -> error("unsupported type FiatType") + AmountType.ReserveType -> SdkAmountType.Reserve + is AmountType.TokenType -> SdkAmountType.Token( + Token( + name = this.token.name, + symbol = this.token.symbol, + contractAddress = this.token.contractAddress, + decimals = this.token.decimals, + id = this.token.id.rawCurrencyId, + ), + ) + } + } } \ No newline at end of file From 5f338d293bb24459f78682b165e233313a6dac4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 22:26:45 +0300 Subject: [PATCH 47/90] Updated on 2026-08-14 --- .../impl/ui/AddToPortfolioBottomSheet.kt | 4 +- .../markets/portfolio/impl/ui/MyPortfolio.kt | 4 +- .../impl/ui/TokenActionsBottomSheet.kt | 10 +- .../impl/ui/WalletSelectorBottomSheet.kt | 142 ++++++++++++++++++ .../PreviewAddToPortfolioBSContentProvider.kt | 44 +++--- .../ui/state/AddToPortfolioBSContentUM.kt | 3 + .../portfolio/impl/ui/state/MyPortfolioUM.kt | 8 + ...SContent.kt => TokenActionsBSContentUM.kt} | 2 +- .../ui/state/WalletSelectorBSContentUM.kt | 11 ++ 9 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/{TokenActionsBSContent.kt => TokenActionsBSContentUM.kt} (95%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index e9b760f529..422bcdb678 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -55,8 +55,10 @@ internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { ) { Content( modifier = Modifier.fillMaxWidth(), - state = config.content as AddToPortfolioBSContentUM, + state = it, ) + + WalletSelectorBottomSheet(it.walletSelectorConfig) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 2acf18d893..88436fa864 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -69,6 +69,8 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { MyPortfolioUM.Unavailable -> UnavailableContent() } } + + AddToPortfolioBottomSheet(state.addToPortfolioBS) } @Composable @@ -112,7 +114,7 @@ private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: M verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { Text( - text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network", + text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network", // FIXME style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt index cfc6c814d1..bc725d98f3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -18,12 +18,12 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.rows.CornersToRound import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContent +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import kotlinx.collections.immutable.toImmutableList @Composable fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( + TangemBottomSheet( config = config, title = { content -> TangemBottomSheetTitle(content.title) @@ -34,7 +34,7 @@ fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { } @Composable -private fun Content(content: TokenActionsBSContent) { +private fun Content(content: TokenActionsBSContentUM) { Column( modifier = Modifier .padding( @@ -78,9 +78,9 @@ private fun Preview() { TangemBottomSheetConfig( isShow = true, onDismissRequest = {}, - content = TokenActionsBSContent( + content = TokenActionsBSContentUM( title = "Wallet 1", - actions = TokenActionsBSContent.Action.entries.toImmutableList(), + actions = TokenActionsBSContentUM.Action.entries.toImmutableList(), onActionClick = {}, ), ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt new file mode 100644 index 0000000000..a9e190928f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt @@ -0,0 +1,142 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider +import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + addBottomInsets = false, + title = { content -> + TangemTopAppBar( + title = resourceReference(R.string.manage_tokens_wallet_selector_title), + titleAlignment = Alignment.CenterHorizontally, + startButton = TopAppBarButtonUM.Back(content.onBack), + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) + }, + ) { content -> + Content( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing8, + ), + state = content, + ) + } +} + +@Composable +private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Column( + modifier = modifier + .verticalScroll(rememberScrollState()), + ) { + BlockCard( + modifier = Modifier.fillMaxSize(), + colors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) { + state.userWallets.forEach { state -> + key(state.id) { + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + state = state, + ) + } + } + } + SpacerH(bottomBarHeight) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + WalletSelectorBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = WalletSelectorBSContentUM( + userWallets = persistentListOf( + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.Checkmark, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + ), + onBack = {}, + onWalletSelected = {}, + ), + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewContent() { + TangemThemePreview { + Content( + state = WalletSelectorBSContentUM( + userWallets = persistentListOf( + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.Checkmark, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + ), + onBack = {}, + onWalletSelected = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index d184d3aac6..d218f4f8db 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -2,6 +2,8 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId @@ -20,18 +22,20 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider isSelected = false, ) + val userWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ) + override val values: Sequence get() = sequenceOf( AddToPortfolioBSContentUM( - selectedWallet = UserWalletItemUM( - id = UserWalletId("1"), - name = stringReference("Wallet 1"), - information = stringReference("3 cards, 10,123$"), - imageUrl = "", - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ), + selectedWallet = userWallet, selectNetworkUM = SelectNetworkUM( tokenId = "etherium", tokenName = "Etherium", @@ -51,17 +55,15 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider isScanCardNotificationVisible = true, continueButtonEnabled = true, onContinueButtonClick = {}, + walletSelectorConfig = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), + onWalletClick = {}, ), AddToPortfolioBSContentUM( - selectedWallet = UserWalletItemUM( - id = UserWalletId("1"), - name = stringReference("Wallet 1"), - information = stringReference("3 cards, 10,123$"), - imageUrl = "", - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ), + selectedWallet = userWallet, selectNetworkUM = SelectNetworkUM( tokenId = "etherium", tokenName = "Etherium", @@ -81,6 +83,12 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider isScanCardNotificationVisible = true, continueButtonEnabled = false, onContinueButtonClick = {}, + walletSelectorConfig = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), + onWalletClick = {}, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt index 6b708502ce..7369b5cf69 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.markets.portfolio.impl.ui.state import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent internal data class AddToPortfolioBSContentUM( @@ -9,4 +10,6 @@ internal data class AddToPortfolioBSContentUM( val isScanCardNotificationVisible: Boolean, val continueButtonEnabled: Boolean, val onContinueButtonClick: () -> Unit, + val onWalletClick: () -> Unit, + val walletSelectorConfig: TangemBottomSheetConfig, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt index 7fa4c11a4a..c4f8d45a46 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -1,11 +1,19 @@ package com.tangem.features.markets.portfolio.impl.ui.state import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class MyPortfolioUM { + val addToPortfolioBS: TangemBottomSheetConfig = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ) + data class Tokens( val tokens: ImmutableList, val buttonState: AddButtonState, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt similarity index 95% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt index 2252b38f70..7b2a115f54 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.ImmutableList -internal data class TokenActionsBSContent( +internal data class TokenActionsBSContentUM( val title: String, val actions: ImmutableList, val onActionClick: (Action) -> Unit, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt new file mode 100644 index 0000000000..ee727b3608 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +internal data class WalletSelectorBSContentUM( + val userWallets: ImmutableList, + val onWalletSelected: (UserWalletItemUM) -> Unit, + val onBack: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file From 99176f84ecf32b4d5cf6f2f1468861f61bcc9a5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 16:13:39 +0400 Subject: [PATCH 48/90] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 16 +++++ .../repository/DefaultCurrenciesRepository.kt | 37 +++++++++++ ...AllWalletsCryptoCurrencyStatusesUseCase.kt | 66 +++++++++++++++++++ .../CurrenciesStatusesOperations.kt | 2 +- .../tokens/repository/CurrenciesRepository.kt | 8 ++- .../repository/MockCurrenciesRepository.kt | 5 ++ 6 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.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 6e5da90b47..078e47c07e 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 @@ -104,6 +104,22 @@ internal object TokensDomainModule { ) } + @Provides + @Singleton + fun provideGetAllWalletsCryptoCurrencyStatusesUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + stakingRepository: StakingRepository, + ): GetAllWalletsCryptoCurrencyStatusesUseCase { + return GetAllWalletsCryptoCurrencyStatusesUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + } + @Provides @Singleton fun provideGetCurrencyWarningsUseCase( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 5e4c6b350e..83d87b583b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -40,6 +40,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -505,6 +506,42 @@ internal class DefaultCurrenciesRepository( } } + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { + return userWalletsStore.userWallets.flatMapLatest { userWallets -> + val userWalletsWithCurrencies = userWallets + .filterNot(UserWallet::isLocked) + .map { userWallet -> + if (userWallet.isMultiCurrency) { + getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> + val filterResponse = storedTokens.tokens.filter { it.id == currencyRawId } + + responseCurrenciesFactory.createCurrencies( + response = storedTokens.copy(tokens = filterResponse), + scanResponse = userWallet.scanResponse, + ) + } + } else { + flow { + val currency = getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId) + + val currencies = if (currency.id.rawCurrencyId == currencyRawId) { + listOf(currency) + } else { + emptyList() + } + + emit(currencies) + } + } + .map { userWallet to it } + } + + combine(userWalletsWithCurrencies) { it.toMap() } + .onEmpty { emit(value = emptyMap()) } + } + } + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt new file mode 100644 index 0000000000..816433f7be --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +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.wallets.models.UserWallet +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +/** + * Get crypto currency statuses by raw ID for all wallets + * + * @property currenciesRepository currencies repository + * @property quotesRepository quotes repository + * @property networksRepository networks repository + * @property stakingRepository staking repository + * +[REDACTED_AUTHOR] + */ +class GetAllWalletsCryptoCurrencyStatusesUseCase( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, +) { + + /** + * Get crypto currency statuses by [currencyRawId] for all wallets + * + * @param currencyRawId currency raw ID + */ + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke( + currencyRawId: String, + ): Flow>>> { + return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) + .flatMapLatest { userWalletsWithCurrencies: Map> -> + val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) -> + val operations = CurrenciesStatusesOperations( + userWalletId = userWallet.walletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + + val currencyStatusFlows = cryptoCurrencies.map { cryptoCurrency -> + operations.getCurrencyStatusFlow(cryptoCurrency) + .map { it.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } + } + + combine(currencyStatusFlows) { statuses -> userWallet to statuses.toList() } + .onEmpty { emit(userWallet to emptyList()) } + } + + combine(walletStatusFlows) { it.toMap() } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 74e207b2e7..1a9304db56 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -207,7 +207,7 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } - private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { + fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val quoteFlow = getQuotes(currenciesIds) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 1bd10511b8..dcf638ef97 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency 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.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -226,9 +227,7 @@ interface CurrenciesRepository { */ fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token - /** - * Creates token [cryptoCurrency] based on [contractAddress] and [networkId] it`s will be added - */ + /** Creates token [CryptoCurrency.Token] based on [contractAddress] and [networkId] for specified [userWalletId] */ suspend fun createTokenCurrency( userWalletId: UserWalletId, contractAddress: String, @@ -236,4 +235,7 @@ interface CurrenciesRepository { ): CryptoCurrency.Token suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean + + /** Get crypto currencies by [currencyRawId] from all user wallets */ + fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 17f718c351..317a1a3dbe 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency 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.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -152,4 +153,8 @@ internal class MockCurrenciesRepository( override suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean { return false } + + override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { + return emptyFlow() + } } \ No newline at end of file From 27347a13e8aecb93aa1a5f1452e6999197ddfd65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 12:59:18 +0300 Subject: [PATCH 49/90] Updated on 2026-08-14 --- .../java/com/tangem/datasource/api/stakekit/StakeKitApi.kt | 1 + .../java/com/tangem/data/staking/DefaultStakingRepository.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 1c9b1ab3e4..b1f2170fea 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -17,6 +17,7 @@ interface StakeKitApi { @GET("yields/enabled") suspend fun getMultipleYields( + @Query("preferredValidatorsOnly") preferredValidatorsOnly: Boolean? = null, @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null, @Query("type") type: YieldType? = null, @Query("revenueOption") revenueOption: RevenueOption? = null, diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 343c8f3116..24039a6bb3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -125,7 +125,9 @@ internal class DefaultStakingRepository( key = YIELDS_STORE_KEY, skipCache = refresh, block = { - val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + val stakingTokensWithYields = stakeKitApi.getMultipleYields(preferredValidatorsOnly = true) + .getOrThrow() + stakingYieldsStore.store(stakingTokensWithYields.data) }, ) From 6d3eb6de79eec302a33b833632c947cf7a1b43ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 10:06:13 +0400 Subject: [PATCH 50/90] Updated on 2026-08-14 --- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + features/markets/impl/build.gradle.kts | 12 +- .../impl/model/MarketsTokenDetailsModel.kt | 12 +- .../impl/DefaultMarketsPortfolioComponent.kt | 10 +- .../impl/model/MarketsPortfolioModel.kt | 70 +++++++++++- .../impl/model/MyPortfolioUMMFactory.kt | 80 ++++++++++++++ .../impl/model/PortfolioTokenUMConverter.kt | 44 ++++++++ .../impl/model/TokensPortfolioUMConverter.kt | 58 ++++++++++ .../markets/portfolio/impl/ui/MyPortfolio.kt | 103 +++++++++--------- libs/crypto/build.gradle.kts | 3 + .../com/tangem/lib/crypto/BlockchainUtils.kt | 6 + 11 files changed, 332 insertions(+), 67 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 9b00a821e5..9617ee7825 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -74,6 +74,7 @@ data class TangemDimens internal constructor( val size56: Dp = 56.dp, val size60: Dp = 60.dp, val size62: Dp = 62.dp, + val size63: Dp = 63.dp, val size64: Dp = 64.dp, val size68: Dp = 68.dp, val size70: Dp = 70.dp, diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 320f41fbc3..410e6dbd11 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -17,11 +17,16 @@ dependencies { implementation(projects.core.navigation) /* Domain */ - implementation(projects.domain.markets) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) - implementation(projects.domain.wallets.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.markets) + implementation(projects.domain.staking.models) + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) /* Compose */ implementation(deps.compose.coil) @@ -52,4 +57,7 @@ dependencies { implementation(projects.common.ui) implementation(projects.common.uiCharts) implementation(projects.common.routing) + + /* Libs */ + implementation(projects.libs.crypto) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index d2f3aa34f5..41a616a52f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -2,7 +2,9 @@ package com.tangem.features.markets.details.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse -import com.tangem.common.ui.charts.state.* +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartDataProducer +import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -22,13 +24,11 @@ import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice -import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween -import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -296,7 +296,9 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } - val networks = result.networks + val networks = result.networks?.filter { + BlockchainUtils.isSupportedNetworkId(it.networkId) + } networksState.value = if (networks.isNullOrEmpty()) { TokenNetworksState.NoNetworksAvailable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt index be951a14a5..73841ec05d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -2,14 +2,15 @@ package com.tangem.features.markets.portfolio.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -27,10 +28,9 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - MyPortfolio( - modifier = modifier, - state = MyPortfolioUM.Loading, - ) + val state by model.state.collectAsStateWithLifecycle() + + MyPortfolio(modifier = modifier, state = state) } @AssistedFactory diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index ba1f20d5c9..83fd674f85 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -1,30 +1,92 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import timber.log.Timber import javax.inject.Inject @Stable @ComponentScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, + getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - @Suppress("UnusedPrivateMember") + val state: StateFlow get() = _state + private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) + private val params = paramsContainer.require() - @Suppress("UnusedPrivateMember") + private val availableNetworks = MutableStateFlow?>(value = null) + + private val factory = MyPortfolioUMMFactory(::onAddClick, ::onTokenItemClick) + + init { + combine( + flow = getAllWalletsCryptoCurrencyStatusesUseCase(params.tokenId), + flow2 = availableNetworks, + flow3 = getSelectedAppCurrencyUseCase(), + flow4 = getBalanceHidingSettingsUseCase(), + ) { walletsWithCurrencyStatuses, networks, maybeAppCurrency, isBalanceHidden -> + val appCurrency = maybeAppCurrency.getOrElse { e -> + Timber.e("Failed to load app currency: $e") + AppCurrency.Default + } + + factory.create( + walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, + availableNetworks = networks, + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden.isBalanceHidden, + ) + } + .onEach { _state.value = it } + .launchIn(modelScope) + } + fun setTokenNetworks(networks: List) { - // TODO [REDACTED_TASK_KEY] + availableNetworks.value = networks } fun setNoNetworksAvailable() { - // TODO [REDACTED_TASK_KEY] + availableNetworks.value = emptyList() + } + + private fun onAddClick() { + // TODO [REDACTED_JIRA] + } + + private fun onTokenItemClick(token: CryptoCurrencyStatus) { + _state.update { + (_state.value as? MyPortfolioUM.Tokens)?.let { state -> + state.copy( + tokens = state.tokens.map { tokenUM -> + if (tokenUM.tokenItemState.id == token.currency.id.value) { + tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown) + } else { + tokenUM + } + } + .toImmutableList(), + ) + } ?: it + } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt new file mode 100644 index 0000000000..bd9042cac0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt @@ -0,0 +1,80 @@ +package com.tangem.features.markets.portfolio.impl.model + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM + +/** + * Factory for creating [MyPortfolioUM] + * + * @property onAddClick callback when user wants to add new token + * @property onTokenItemClick callback when user wants to see actions with token + * +[REDACTED_AUTHOR] + */ +internal class MyPortfolioUMMFactory( + private val onAddClick: () -> Unit, + private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, +) { + + /** + * Create [MyPortfolioUM] from data set + * + * @param walletsWithCurrencyStatuses wallets with currency statuses + * @param availableNetworks available networks + * @param appCurrency app currency + * @param isBalanceHidden flag that indicates if balance should be hidden + */ + fun create( + walletsWithCurrencyStatuses: Map>>, + availableNetworks: List?, + appCurrency: AppCurrency, + isBalanceHidden: Boolean, + ): MyPortfolioUM { + if (availableNetworks == null) return MyPortfolioUM.Loading + + if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable + + val availableWalletsWithStatuses = walletsWithCurrencyStatuses + .filterAvailable(networks = availableNetworks) + .flatMapStatuses() + + if (availableWalletsWithStatuses.isEmpty()) { + return MyPortfolioUM.AddFirstToken(onAddClick = onAddClick) + } + + return TokensPortfolioUMConverter( + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden, + availableNetworks = availableNetworks, + onAddClick = onAddClick, + onTokenItemClick = onTokenItemClick, + ) + .convert(availableWalletsWithStatuses) + } + + private fun Map>>.filterAvailable( + networks: List, + ): Map> { + val networkIds = networks.map { it.networkId } + + return mapValues { entry -> + entry.value.mapNotNull { maybeStatus -> + maybeStatus.getOrNull() + ?.takeIf { networkIds.contains(it.currency.network.backendId) } + } + } + } + + private fun Map>.flatMapStatuses(): Map { + return this + .flatMap { entry -> + entry.value.map { entry.key to it } + } + .toMap() + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt new file mode 100644 index 0000000000..d299172177 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState +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.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.utils.converter.Converter + +/** + * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] + * +[REDACTED_AUTHOR] + */ +internal class PortfolioTokenUMConverter( + private val appCurrency: AppCurrency, + private val isBalanceHidden: Boolean, + private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, +) : Converter, PortfolioTokenUM> { + + override fun convert(value: Map.Entry): PortfolioTokenUM { + val (userWallet, cryptoCurrencyStatus) = value + + val tokenItemStateConverter = TokenItemStateConverter( + appCurrency = appCurrency, + titleStateProvider = { TokenItemState.TitleState.Content(text = userWallet.name) }, + subtitleStateProvider = { + TokenItemState.SubtitleState.TextContent(value = cryptoCurrencyStatus.currency.name) + }, + onItemClick = onTokenItemClick, + onItemLongClick = null, + ) + + return PortfolioTokenUM( + tokenItemState = tokenItemStateConverter.convert(value = cryptoCurrencyStatus), + isBalanceHidden = isBalanceHidden, + isQuickActionsShown = false, + onQuickActionClick = { + // TODO: [REDACTED_JIRA] + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt new file mode 100644 index 0000000000..aa3b90ea8f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -0,0 +1,58 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens] + * +[REDACTED_AUTHOR] + */ +internal class TokensPortfolioUMConverter( + private val appCurrency: AppCurrency, + private val isBalanceHidden: Boolean, + private val availableNetworks: List, + private val onAddClick: () -> Unit, + private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, +) : Converter, MyPortfolioUM.Tokens> { + + override fun convert(value: Map): MyPortfolioUM.Tokens { + return MyPortfolioUM.Tokens( + tokens = PortfolioTokenUMConverter(appCurrency, isBalanceHidden, onTokenItemClick) + .convertList(value.entries) + .toImmutableList(), + buttonState = getAddButtonState( + availableWalletsWithStatuses = value, + availableNetworks = availableNetworks, + ), + onAddClick = onAddClick, + ) + } + + private fun getAddButtonState( + availableWalletsWithStatuses: Map, + availableNetworks: List, + ): AddButtonState { + val networkIds = availableNetworks.map { it.networkId } + + val allNetworksAdded = availableWalletsWithStatuses + // User can add currencies only in multi-currency wallets + .filterKeys(UserWallet::isMultiCurrency) + .entries + .groupBy( + keySelector = { it.key }, + valueTransform = { it.value.currency.network.backendId }, + ) + .mapValues { it.value.toSet() } + // Each wallets contains all available networks? + .all { it.value.containsAll(networkIds) } + + return if (allNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 88436fa864..6f5c6f01db 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SmallButtonShimmer @@ -26,51 +25,68 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState @Composable internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { InformationBlock( modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - Text( - text = stringResource(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, + contentHorizontalPadding = TangemTheme.dimens.spacing0, + title = { Title() }, action = { if (state !is MyPortfolioUM.Tokens) return@InformationBlock - when (state.buttonState) { - MyPortfolioUM.Tokens.AddButtonState.Loading -> { - SmallButtonShimmer( - modifier = Modifier.size(width = 63.dp, height = TangemTheme.dimens.size18), - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - ) - } - else -> { - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.markets_add_token), - icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), - onClick = state.onAddClick, - enabled = state.buttonState == MyPortfolioUM.Tokens.AddButtonState.Available, - ), - ) - } - } + AddButton(state = state.buttonState, onClick = state.onAddClick) }, ) { + val contentModifier = Modifier.padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ) + when (state) { is MyPortfolioUM.Tokens -> TokenList(state = state) - is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state) - MyPortfolioUM.Loading -> LoadingPlaceholder() - MyPortfolioUM.Unavailable -> UnavailableContent() + is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier) + MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier) + MyPortfolioUM.Unavailable -> UnavailableContent(modifier = contentModifier) } } - AddToPortfolioBottomSheet(state.addToPortfolioBS) + AddToPortfolioBottomSheet(config = state.addToPortfolioBS) +} + +@Composable +private fun Title() { + Text( + text = stringResource(R.string.markets_common_my_portfolio), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun AddButton(state: AddButtonState, onClick: () -> Unit) { + when (state) { + AddButtonState.Loading -> { + SmallButtonShimmer( + modifier = Modifier.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), + shape = RoundedCornerShape(TangemTheme.dimens.radius3), + ) + } + AddButtonState.Available, + AddButtonState.Unavailable, + -> { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.markets_add_token), + icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), + onClick = onClick, + enabled = state == AddButtonState.Available, + ), + ) + } + } } @Composable @@ -90,12 +106,7 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier @Composable private fun UnavailableContent(modifier: Modifier = Modifier) { Text( - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), + modifier = modifier, text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, @@ -105,12 +116,7 @@ private fun UnavailableContent(modifier: Modifier = Modifier) { @Composable private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { Column( - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), + modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { Text( @@ -118,6 +124,7 @@ private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: M style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) + PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.markets_add_to_portfolio_button), @@ -128,19 +135,13 @@ private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: M @Composable private fun LoadingPlaceholder(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), - ) { + Column(modifier = modifier) { TextShimmer( modifier = Modifier.fillMaxWidth(), style = TangemTheme.typography.body2, textSizeHeight = true, ) + TextShimmer( modifier = Modifier.fillMaxWidth(fraction = 0.7f), style = TangemTheme.typography.body2, diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 9a8ccaea31..1e443985c3 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -20,4 +20,7 @@ dependencies { /** Core */ implementation(projects.core.utils) + + /** Libs */ + implementation(projects.libs.blockchainSdk) } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 35b8386cd9..dd253860fd 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -2,6 +2,8 @@ package com.tangem.lib.crypto import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter import com.tangem.lib.crypto.models.XrpTaggedAddress @@ -55,4 +57,8 @@ object BlockchainUtils { val blockchain = Blockchain.fromId(networkId) return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet } + + fun isSupportedNetworkId(networkId: String): Boolean { + return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false + } } \ No newline at end of file From a816b14c6946776a5d4b96082d89444a9cea6706 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 14:58:46 +0400 Subject: [PATCH 51/90] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 7 +- .../converter/UserWalletItemUMConverter.kt | 102 ++++++++++++++++++ .../domain/tokens/model/TotalFiatBalance.kt | 0 .../details/utils/UserWalletMappers.kt | 102 ------------------ .../details/utils/UserWalletsFetcher.kt | 33 ++++-- 5 files changed, 130 insertions(+), 114 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt rename domain/tokens/{src/main/kotlin => models/src/main/java}/com/tangem/domain/tokens/model/TotalFiatBalance.kt (100%) delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index d7b55fb311..9380922c04 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -28,11 +28,12 @@ dependencies { implementation(projects.core.utils) /** Project - Domain */ - implementation(projects.domain.tokens.models) - implementation(projects.domain.wallets.models) - implementation(projects.domain.staking.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.legacy) + implementation(projects.domain.staking.models) + implementation(projects.domain.tokens.models) implementation(projects.domain.transaction.models) + implementation(projects.domain.wallets.models) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt new file mode 100644 index 0000000000..704315fc82 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -0,0 +1,102 @@ +package com.tangem.common.ui.userwallet.converter + +import com.tangem.common.ui.R +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.StringsSigns.DOT +import com.tangem.utils.StringsSigns.STARS +import com.tangem.utils.converter.Converter + +/** + * Converter from [UserWallet] to [UserWalletItemUM] + * + * @property onClick lambda be invoked when item is clicked + * @property appCurrency selected app currency + * @property balance wallet balance + * @property isLoading wallet loading state + * @property isBalanceHidden wallet balance is hidden + * +[REDACTED_AUTHOR] + */ +class UserWalletItemUMConverter( + private val onClick: (UserWalletId) -> Unit, + private val appCurrency: AppCurrency? = null, + private val balance: TotalFiatBalance? = null, + private val isLoading: Boolean = true, + private val isBalanceHidden: Boolean = false, +) : Converter { + + override fun convert(value: UserWallet): UserWalletItemUM { + return with(value) { + UserWalletItemUM( + id = walletId, + name = stringReference(name), + information = getInfo( + appCurrency = appCurrency, + balance = balance, + isBalanceHidden = isBalanceHidden, + isLoading = isLoading, + ), + imageUrl = artworkUrl, + isEnabled = !isLocked, + onClick = { onClick(value.walletId) }, + ) + } + } + + private fun UserWallet.getInfo( + appCurrency: AppCurrency?, + balance: TotalFiatBalance?, + isBalanceHidden: Boolean, + isLoading: Boolean, + ): TextReference { + val dividerRef = stringReference(value = " $DOT ") + + val cardCount = getCardsCount() ?: 1 + val cardCountRef = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + + return when { + isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) + isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) + isLoading -> cardCountRef + else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) + } + } + + private fun getBalanceInfo( + balance: TotalFiatBalance?, + appCurrency: AppCurrency?, + cardCountRef: TextReference, + dividerRef: TextReference, + ): TextReference { + val amount = when (balance) { + is TotalFiatBalance.Loaded -> balance.amount + is TotalFiatBalance.Failed, + is TotalFiatBalance.Loading, + null, + -> null + } + + return if (amount != null && appCurrency != null) { + val formattedAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + val amountRef = stringReference(formattedAmount) + combinedReference(cardCountRef, dividerRef, amountRef) + } else { + combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt similarity index 100% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt deleted file mode 100644 index f51b383fbd..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.details.utils - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.util.getCardsCount -import com.tangem.domain.tokens.model.TotalFiatBalance -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.details.impl.R -import com.tangem.utils.StringsSigns.STARS -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -internal fun List.toUiModels( - onClick: (UserWalletId) -> Unit, - appCurrency: AppCurrency? = null, - balances: Map = emptyMap(), - isLoading: Boolean = true, - isBalancesHidden: Boolean = false, -): ImmutableList = this.map { model -> - val balance = balances[model.walletId] - - model.toUiModel( - balance = balance, - appCurrency = appCurrency, - isLoading = isLoading, - isBalanceHidden = isBalancesHidden, - onClick = { onClick(model.walletId) }, - ) -}.toImmutableList() - -private fun UserWallet.toUiModel( - balance: TotalFiatBalance?, - appCurrency: AppCurrency?, - isLoading: Boolean, - isBalanceHidden: Boolean, - onClick: () -> Unit, -): UserWalletItemUM = UserWalletItemUM( - id = walletId, - name = stringReference(name), - information = getInfo( - appCurrency = appCurrency, - balance = balance, - isBalanceHidden = isBalanceHidden, - isLoading = isLoading, - ), - imageUrl = artworkUrl, - isEnabled = !isLocked, - onClick = onClick, -) - -private fun UserWallet.getInfo( - appCurrency: AppCurrency?, - balance: TotalFiatBalance?, - isBalanceHidden: Boolean, - isLoading: Boolean, -): TextReference { - val dividerRef = stringReference(value = " • ") - - val cardCount = getCardsCount() ?: 1 - val cardCountRef = TextReference.PluralRes( - id = R.plurals.card_label_card_count, - count = cardCount, - formatArgs = wrappedList(cardCount), - ) - - return when { - isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) - isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) - isLoading -> cardCountRef - else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) - } -} - -private fun getBalanceInfo( - balance: TotalFiatBalance?, - appCurrency: AppCurrency?, - cardCountRef: TextReference, - dividerRef: TextReference, -): TextReference { - val amount = when (balance) { - is TotalFiatBalance.Loaded -> balance.amount - is TotalFiatBalance.Failed, - is TotalFiatBalance.Loading, - null, - -> null - } - - return if (amount != null && appCurrency != null) { - val formattedAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - val amountRef = stringReference(formattedAmount) - combinedReference(cardCountRef, dividerRef, amountRef) - } else { - combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index cd5013c005..c9115930ec 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.utils import arrow.core.Either import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router @@ -25,6 +26,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -41,7 +43,10 @@ internal class UserWalletsFetcher @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> - emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) + val uiModels = UserWalletItemUMConverter(onClick = ::navigateToWalletSettings).convertList(wallets) + .toImmutableList() + + emit(uiModels) combine( getSelectedAppCurrencyUseCase().distinctUntilChanged(), @@ -75,20 +80,30 @@ internal class UserWalletsFetcher @Inject constructor( ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, - block = { maybeBalances.bindOrNull().orEmpty() }, + block = { + maybeBalances.bindOrNull().orEmpty() + .filterKeys { userWalletId -> wallets.any { it.walletId == userWalletId } } + .mapKeys { entry -> wallets.first { it.walletId == entry.key } } + }, ) + val appCurrency = withError( transform = { Error.UnableToGetAppCurrency }, block = { maybeAppCurrency.toLce().bind() }, ) - wallets.toUiModels( - appCurrency = appCurrency, - balances = balances, - onClick = ::navigateToWalletSettings, - isBalancesHidden = balanceHidingSettings.isBalanceHidden, - isLoading = maybeBalances.isLoading(), - ) + balances + .map { (userWallet, balance) -> + UserWalletItemUMConverter( + onClick = ::navigateToWalletSettings, + appCurrency = appCurrency, + balance = balance, + isBalanceHidden = balanceHidingSettings.isBalanceHidden, + isLoading = maybeBalances.isLoading(), + ) + .convert(userWallet) + } + .toImmutableList() } private fun navigateToWalletSettings(userWalletId: UserWalletId) { From f2bf27ee5faeee5f3fb407109148f1e7301b0eab Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 18:32:19 +0300 Subject: [PATCH 52/90] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index f31136c8dd..157e1ee276 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -363,8 +363,8 @@ internal class StateBuilder( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( - title = stringReference("//TODO"), - description = stringReference("//TODO"), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { @@ -993,8 +993,8 @@ internal class StateBuilder( warnings.add( 0, SwapWarning.TransactionInProgressWarning( - title = stringReference("//TODO"), - description = stringReference("//TODO"), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) return uiState.copy( From 3e7941113858c52af4497191fb63f814cd6a2f17 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 17:36:47 +0300 Subject: [PATCH 53/90] Updated on 2026-08-14 --- .../firebase/FirebaseAppInstanceIdProvider.kt | 24 +++++++++++++++++++ .../tap/di/analytics/AnalyticsModule.kt | 6 +++++ .../core/analytics/AppInstanceIdProvider.kt | 8 +++++++ .../analytics/DummyAppInstanceIdProvider.kt | 12 ++++++++++ features/details/impl/build.gradle.kts | 1 + .../preview/PreviewDetailsComponent.kt | 2 ++ .../features/details/utils/ItemsBuilder.kt | 10 +++++++- 7 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt new file mode 100644 index 0000000000..8d33e1522e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.common.analytics.handlers.firebase + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import com.tangem.core.analytics.AppInstanceIdProvider +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import kotlin.coroutines.resume + +internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider { + + override suspend fun getAppInstanceId(): String? = suspendCancellableCoroutine { continuation -> + Firebase.analytics.appInstanceId + .addOnSuccessListener { continuation.resume(it) } + .addOnFailureListener { + Timber.w("Fail to get appInstanceId") + continuation.resume(null) + } + } + + override fun getAppInstanceIdSync(): String? { + return Firebase.analytics.appInstanceId.result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt index c687d63c10..60fea2b718 100644 --- a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt @@ -1,9 +1,11 @@ package com.tangem.tap.di.analytics +import com.tangem.core.analytics.AppInstanceIdProvider import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase +import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,4 +25,8 @@ internal object AnalyticsModule { @Provides @Singleton fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy() + + @Provides + @Singleton + fun provideAppInstanceIdProvider(): AppInstanceIdProvider = FirebaseAppInstanceIdProvider() } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt b/core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt new file mode 100644 index 0000000000..0d1e973283 --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.core.analytics + +interface AppInstanceIdProvider { + + suspend fun getAppInstanceId(): String? + + fun getAppInstanceIdSync(): String? +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt b/core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt new file mode 100644 index 0000000000..94f92f0b3c --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt @@ -0,0 +1,12 @@ +package com.tangem.core.analytics + +class DummyAppInstanceIdProvider : AppInstanceIdProvider { + + override suspend fun getAppInstanceId(): String? { + return null + } + + override fun getAppInstanceIdSync(): String? { + return null + } +} \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index b8d9e6465d..9a16bc4892 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) implementation(projects.core.navigation) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common.routing) implementation(projects.common.ui) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 209cad1288..1a15b85fc5 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.analytics.DummyAppInstanceIdProvider import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.features.details.component.DetailsComponent @@ -18,6 +19,7 @@ internal class PreviewDetailsComponent : DetailsComponent { ItemsBuilder( router = DummyRouter(), urlOpener = DummyUrlOpener(), + appInstanceIdProvider = DummyAppInstanceIdProvider(), ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index f14c204327..2f1093804f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -1,6 +1,7 @@ package com.tangem.features.details.utils import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.AppInstanceIdProvider import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.url.UrlOpener @@ -19,6 +20,7 @@ import javax.inject.Inject internal class ItemsBuilder @Inject constructor( private val router: Router, private val urlOpener: UrlOpener, + private val appInstanceIdProvider: AppInstanceIdProvider, ) { fun buildAll(isWalletConnectAvailable: Boolean, onSupportClick: () -> Unit): ImmutableList = @@ -50,7 +52,7 @@ internal class ItemsBuilder @Inject constructor( block = BlockUM( text = resourceReference(R.string.details_buy_wallet), iconRes = R.drawable.ic_tangem_24, - onClick = { urlOpener.openUrl(BUY_TANGEM_URL) }, + onClick = { urlOpener.openUrl(buildBuyLink()) }, ), ), ), @@ -103,6 +105,12 @@ internal class ItemsBuilder @Inject constructor( ), ) + private fun buildBuyLink(): String { + return appInstanceIdProvider.getAppInstanceIdSync()?.let { + "$BUY_TANGEM_URL&app_instance_id=$it" + } ?: BUY_TANGEM_URL + } + private companion object { const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" } From 297a006e3d16a75768bbf2fc4a440d70e2a20d73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 15:25:08 +0400 Subject: [PATCH 54/90] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 29 ++++++- .../DefaultCustomTokensRepository.kt | 47 +++++++++++ .../managetokens/di/ManageTokensDataModule.kt | 8 ++ .../CheckIsCurrencyNotAddedUseCase.kt | 20 +++++ .../managetokens/CreateCurrencyUseCase.kt | 24 ++++++ .../domain/managetokens/FindTokenUseCase.kt | 54 +++++++++++++ .../managetokens/ValidateTokenFormUseCase.kt | 80 +++++++++++++++++++ .../managetokens/model/AddCustomTokenForm.kt | 25 ++++++ .../CustomTokenFormValidationException.kt | 24 ++++++ .../model/exceptoin/FindTokenException.kt | 8 ++ .../repository/CustomTokensRepository.kt | 33 ++++++++ 11 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index dd78da6dfd..0dd7b11804 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain -import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.* +import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import dagger.Module import dagger.Provides @@ -17,4 +18,30 @@ internal object ManageTokensDomainModule { fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase { return GetManagedTokensUseCase(manageTokensRepository) } + + @Provides + @Singleton + fun provideValidateTokenFormatUseCase(customTokensRepository: CustomTokensRepository): ValidateTokenFormUseCase { + return ValidateTokenFormUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideCreateCurrencyUseCase(customTokensRepository: CustomTokensRepository): CreateCurrencyUseCase { + return CreateCurrencyUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideFindTokenUseCase(customTokensRepository: CustomTokensRepository): FindTokenUseCase { + return FindTokenUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideCheckIsCurrencyNotAddedUseCase( + customTokensRepository: CustomTokensRepository, + ): CheckIsCurrencyNotAddedUseCase { + return CheckIsCurrencyNotAddedUseCase(customTokensRepository) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt new file mode 100644 index 0000000000..50f20df32c --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -0,0 +1,47 @@ +package com.tangem.data.managetokens + +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +internal class DefaultCustomTokensRepository : CustomTokensRepository { + + override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun isCurrencyNotAdded( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + contractAddress: String?, + ): Boolean { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun findToken( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Token? { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun createCoin( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun createCustomToken( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All, + ): CryptoCurrency.Token { + TODO("Should be implemented in [REDACTED_JIRA]") + } +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 5cb10e4936..f61ff6a0b3 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -1,11 +1,13 @@ package com.tangem.data.managetokens.di +import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -37,4 +39,10 @@ internal object ManageTokensDataModule { dispatchers, ) } + + @Provides + @Singleton + fun provideCustomTokensRepository(): CustomTokensRepository { + return DefaultCustomTokensRepository() + } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt new file mode 100644 index 0000000000..04aa0761eb --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class CheckIsCurrencyNotAddedUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + contractAddress: String?, + ): Either = Either.catch { + repository.isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress) + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt new file mode 100644 index 0000000000..dc935f08af --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network + +class CreateCurrencyUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All?, + ): Either = Either.catch { + if (formValues == null) { + repository.createCoin(networkId, derivationPath) + } else { + repository.createCustomToken(networkId, derivationPath, formValues) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt new file mode 100644 index 0000000000..3542154d4e --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt @@ -0,0 +1,54 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.tangem.domain.managetokens.model.exceptoin.FindTokenException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class FindTokenUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): Either = either { + val currency = findCurrency( + userWalletId = userWalletId, + contractAddress = contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ) + + ensureNotNull(currency) { + FindTokenException.NotFound + } + } + + private suspend fun Raise.findCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Token? = catch( + block = { + repository.findToken( + userWalletId = userWalletId, + contractAddress = contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ) + }, + catch = { + raise(FindTokenException.DataError(it)) + }, + ) +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt new file mode 100644 index 0000000000..ffbfa795bf --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt @@ -0,0 +1,80 @@ +package com.tangem.domain.managetokens + +import arrow.core.EitherNel +import arrow.core.nonEmptyListOf +import arrow.core.raise.* +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network + +class ValidateTokenFormUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + networkId: Network.ID, + formValues: AddCustomTokenForm.Raw, + ): EitherNel = either { + if (formValues.name.isBlank() && formValues.symbol.isBlank() && formValues.decimals.isBlank()) { + val contractAddress = withError({ nonEmptyListOf(it) }) { + ensureIsContractAddressValid(formValues.contractAddress, networkId) + } + + AddCustomTokenForm.Validated.ContractAddress(contractAddress) + } else { + zipOrAccumulate( + { ensureIsContractAddressValid(formValues.contractAddress, networkId) }, + { ensureIsDecimalsValid(formValues.decimals) }, + { ensure(formValues.name.isNotBlank()) { CustomTokenFormValidationException.EmptyName } }, + { ensure(formValues.symbol.isNotBlank()) { CustomTokenFormValidationException.EmptySymbol } }, + ) { contractAddress, decimals, _, _ -> + AddCustomTokenForm.Validated.All( + contractAddress = contractAddress, + symbol = formValues.symbol, + name = formValues.name, + decimals = decimals, + ) + } + } + } + + private suspend fun Raise.ensureIsContractAddressValid( + contractAddress: String, + networkId: Network.ID, + ): String { + ensure(contractAddress.isNotBlank()) { + CustomTokenFormValidationException.ContractAddress.Empty + } + + val isValid = catch({ repository.validateContractAddress(contractAddress, networkId) }) { + raise(CustomTokenFormValidationException.DataError(it)) + } + + ensure(isValid) { + CustomTokenFormValidationException.ContractAddress.Invalid + } + + return contractAddress + } + + private fun Raise.ensureIsDecimalsValid(decimals: String): Int { + ensure(condition = decimals.isNotBlank()) { + CustomTokenFormValidationException.Decimals.Empty + } + + val decimalsInt = ensureNotNull(decimals.toIntOrNull()) { + CustomTokenFormValidationException.Decimals.Invalid + } + ensure(condition = decimalsInt in MIN_DECIMALS..MAX_DECIMALS) { + CustomTokenFormValidationException.Decimals.Invalid + } + + return decimalsInt + } + + companion object { + const val MIN_DECIMALS = 1 + const val MAX_DECIMALS = 30 + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt new file mode 100644 index 0000000000..b70cd54ab9 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.managetokens.model + +sealed class AddCustomTokenForm { + + data class Raw( + val contractAddress: String, + val name: String, + val symbol: String, + val decimals: String, + ) : AddCustomTokenForm() + + sealed class Validated { + + data class ContractAddress( + val contractAddress: String, + ) : Validated() + + data class All( + val contractAddress: String, + val name: String, + val symbol: String, + val decimals: Int, + ) : Validated() + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt new file mode 100644 index 0000000000..a197980215 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class CustomTokenFormValidationException { + + sealed class ContractAddress : CustomTokenFormValidationException() { + + data object Empty : ContractAddress() + + data object Invalid : ContractAddress() + } + + sealed class Decimals : CustomTokenFormValidationException() { + + data object Empty : Decimals() + + data object Invalid : Decimals() + } + + data object EmptyName : CustomTokenFormValidationException() + + data object EmptySymbol : CustomTokenFormValidationException() + + data class DataError(val cause: Throwable) : CustomTokenFormValidationException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt new file mode 100644 index 0000000000..cc8b180305 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class FindTokenException { + + data object NotFound : FindTokenException() + + data class DataError(val cause: Throwable) : FindTokenException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt new file mode 100644 index 0000000000..b578627da0 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.managetokens.repository + +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +interface CustomTokensRepository { + + suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean + + suspend fun isCurrencyNotAdded( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + contractAddress: String?, + ): Boolean + + suspend fun findToken( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Token? + + suspend fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin + + suspend fun createCustomToken( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All, + ): CryptoCurrency.Token +} \ No newline at end of file From ce9335e3bda3dba1ceab417b3a88db8d9da54d73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 19:12:07 +0400 Subject: [PATCH 55/90] Updated on 2026-08-14 --- .../common/currency/CryptoCurrencyFactory.kt | 47 ++++++-- .../data/common/currency/NetworkOperations.kt | 15 +++ .../data/common/currency/TokensOperations.kt | 2 +- .../DefaultCustomTokensRepository.kt | 103 ++++++++++++++++-- .../managetokens/di/ManageTokensDataModule.kt | 14 ++- 5 files changed, 160 insertions(+), 21 deletions(-) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index 9af9e67943..38d8376ff2 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -12,6 +12,29 @@ import com.tangem.blockchain.common.Token as SdkToken // FIXME: Make internal class CryptoCurrencyFactory { + @Suppress("LongParameterList") // Yep, it's long + fun createToken( + network: Network, + rawId: String?, + name: String, + symbol: String, + decimals: Int, + contractAddress: String, + ): CryptoCurrency.Token { + val id = getTokenId(network, rawId, contractAddress) + + return CryptoCurrency.Token( + id = id, + network = network, + name = name, + symbol = symbol, + decimals = decimals, + iconUrl = rawId?.let(::getTokenIconUrlFromDefaultHost), + isCustom = isCustomToken(id, network), + contractAddress = contractAddress, + ) + } + fun createToken( sdkToken: SdkToken, blockchain: Blockchain, @@ -49,15 +72,7 @@ class CryptoCurrencyFactory { } val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null - return CryptoCurrency.Coin( - id = getCoinId(network, blockchain.toCoinId()), - network = network, - name = blockchain.fullName, - symbol = blockchain.currency, - iconUrl = getCoinIconUrl(blockchain), - decimals = blockchain.decimals(), - isCustom = isCustomCoin(network), - ) + return createCoin(network) } fun createCoin( @@ -69,6 +84,20 @@ class CryptoCurrencyFactory { return createCoin(blockchain, extraDerivationPath, derivationStyleProvider) } + fun createCoin(network: Network): CryptoCurrency.Coin { + val blockchain = Blockchain.fromId(network.id.value) + + return CryptoCurrency.Coin( + id = getCoinId(network, blockchain.toCoinId()), + network = network, + name = blockchain.fullName, + symbol = blockchain.currency, + iconUrl = getCoinIconUrl(blockchain), + decimals = blockchain.decimals(), + isCustom = isCustomCoin(network), + ) + } + fun createToken( token: Token, networkId: String, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index 8f2e5b60ac..6febfc5fb7 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -11,6 +11,21 @@ fun getBlockchain(networkId: Network.ID): Blockchain { return Blockchain.fromId(networkId.value) } +fun getNetwork(networkId: Network.ID, derivationPath: Network.DerivationPath): Network { + val blockchain = getBlockchain(networkId) + + return Network( + id = networkId, + backendId = blockchain.toNetworkId(), + name = blockchain.getNetworkName(), + isTestnet = blockchain.isTestnet(), + derivationPath = derivationPath, + currencySymbol = blockchain.currency, + standardType = getNetworkStandardType(blockchain), + hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + ) +} + fun getNetwork( blockchain: Blockchain, extraDerivationPath: String?, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt index 72018fc4ed..2c2c8d95ad 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt @@ -85,7 +85,7 @@ private fun getCurrencyIdBody(network: Network): CurrencyIdBody { } } -private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { +fun getTokenIconUrlFromDefaultHost(tokenId: String): String { return buildString { append(DEFAULT_TOKENS_ICONS_HOST) append('/') diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 50f20df32c..8488ff5b54 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -1,16 +1,46 @@ package com.tangem.data.managetokens +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetwork +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.supportedBlockchains +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext -internal class DefaultCustomTokensRepository : CustomTokensRepository { +internal class DefaultCustomTokensRepository( + private val tangemTechApi: TangemTechApi, + private val userWalletsStore: UserWalletsStore, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : CustomTokensRepository { - override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean { - TODO("Should be implemented in [REDACTED_JIRA]") - } + private val cryptoCurrencyFactory = CryptoCurrencyFactory() + + override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean = + withContext(dispatchers.io) { + when (val blockchain = Blockchain.fromId(networkId.value)) { + Blockchain.Unknown, + Blockchain.Binance, + Blockchain.BinanceTestnet, + -> true + Blockchain.Cardano -> blockchain.validateContractAddress(contractAddress.lowercase()) + else -> blockchain.validateAddress(contractAddress.lowercase()) + } + } override suspend fun isCurrencyNotAdded( userWalletId: UserWalletId, @@ -18,7 +48,17 @@ internal class DefaultCustomTokensRepository : CustomTokensRepository { derivationPath: Network.DerivationPath, contractAddress: String?, ): Boolean { - TODO("Should be implemented in [REDACTED_JIRA]") + return withContext(dispatchers.io) { + val storedCurrencies: UserTokensResponse = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?: error("User tokens not found") + + storedCurrencies.tokens.none { token -> + Blockchain.fromId(networkId.value).toNetworkId() == token.networkId && + derivationPath.value == token.derivationPath && + contractAddress.equals(token.contractAddress, ignoreCase = true) + } + } } override suspend fun findToken( @@ -26,15 +66,51 @@ internal class DefaultCustomTokensRepository : CustomTokensRepository { contractAddress: String, networkId: Network.ID, derivationPath: Network.DerivationPath, - ): CryptoCurrency.Token? { - TODO("Should be implemented in [REDACTED_JIRA]") + ): CryptoCurrency.Token? = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + ?: error("User wallet not found") + val network = getNetwork(networkId, derivationPath) + + val supportedTokenNetworkIds = userWallet.scanResponse.card + .supportedBlockchains(userWallet.scanResponse.cardTypesResolver) + .filter(Blockchain::canHandleTokens) + .map(Blockchain::toNetworkId) + + val response = tangemTechApi.getCoins( + contractAddress = contractAddress, + networkIds = network.backendId, + active = true, + ).getOrThrow() + + response.coins.firstNotNullOfOrNull { coin -> + val coinNetwork = coin.networks.firstOrNull { network -> + (network.contractAddress != null || network.decimalCount != null) && + network.contractAddress.equals(contractAddress, ignoreCase = true) && + network.networkId in supportedTokenNetworkIds + } + + if (coinNetwork != null) { + cryptoCurrencyFactory.createToken( + network = network, + rawId = coin.id, + name = coin.name, + symbol = coin.symbol, + decimals = coinNetwork.decimalCount!!.toInt(), + contractAddress = contractAddress, + ) + } else { + null + } + } } override suspend fun createCoin( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { - TODO("Should be implemented in [REDACTED_JIRA]") + val network = getNetwork(networkId, derivationPath) + + return cryptoCurrencyFactory.createCoin(network) } override suspend fun createCustomToken( @@ -42,6 +118,15 @@ internal class DefaultCustomTokensRepository : CustomTokensRepository { derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { - TODO("Should be implemented in [REDACTED_JIRA]") + val network = getNetwork(networkId, derivationPath) + + return cryptoCurrencyFactory.createToken( + network = network, + rawId = null, + name = formValues.name, + symbol = formValues.symbol, + decimals = formValues.decimals, + contractAddress = formValues.contractAddress, + ) } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index f61ff6a0b3..64d77526c1 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -42,7 +42,17 @@ internal object ManageTokensDataModule { @Provides @Singleton - fun provideCustomTokensRepository(): CustomTokensRepository { - return DefaultCustomTokensRepository() + fun provideCustomTokensRepository( + tangemTechApi: TangemTechApi, + userWalletsStore: UserWalletsStore, + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): CustomTokensRepository { + return DefaultCustomTokensRepository( + tangemTechApi, + userWalletsStore, + appPreferencesStore, + dispatchers, + ) } } \ No newline at end of file From e59c7e5c5ab22166a5b2819a762c2bc76d22a42f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 13:14:28 +0400 Subject: [PATCH 56/90] Updated on 2026-08-14 --- .../DefaultCustomTokensRepository.kt | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 8488ff5b54..eabde921a1 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -1,8 +1,11 @@ package com.tangem.data.managetokens +import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter +import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getBlockchain import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -70,6 +73,11 @@ internal class DefaultCustomTokensRepository( val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") val network = getNetwork(networkId, derivationPath) + val tokenAddress = convertTokenAddress( + networkId, + contractAddress, + symbol = null, + ) val supportedTokenNetworkIds = userWallet.scanResponse.card .supportedBlockchains(userWallet.scanResponse.cardTypesResolver) @@ -85,7 +93,7 @@ internal class DefaultCustomTokensRepository( response.coins.firstNotNullOfOrNull { coin -> val coinNetwork = coin.networks.firstOrNull { network -> (network.contractAddress != null || network.decimalCount != null) && - network.contractAddress.equals(contractAddress, ignoreCase = true) && + network.contractAddress.equals(tokenAddress, ignoreCase = true) && network.networkId in supportedTokenNetworkIds } @@ -96,7 +104,7 @@ internal class DefaultCustomTokensRepository( name = coin.name, symbol = coin.symbol, decimals = coinNetwork.decimalCount!!.toInt(), - contractAddress = contractAddress, + contractAddress = tokenAddress, ) } else { null @@ -119,6 +127,11 @@ internal class DefaultCustomTokensRepository( formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { val network = getNetwork(networkId, derivationPath) + val tokenAddress = convertTokenAddress( + networkId, + formValues.contractAddress, + formValues.symbol, + ) return cryptoCurrencyFactory.createToken( network = network, @@ -126,7 +139,24 @@ internal class DefaultCustomTokensRepository( name = formValues.name, symbol = formValues.symbol, decimals = formValues.decimals, - contractAddress = formValues.contractAddress, + contractAddress = tokenAddress, ) } + + private fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { + val convertedAddress = when (getBlockchain(networkId)) { + Blockchain.Hedera, + Blockchain.HederaTestnet, + -> HederaTokenAddressConverter().convertToTokenId(contractAddress) + Blockchain.Cardano -> { + // TODO: [REDACTED_JIRA] + CardanoTokenAddressConverter().convertToFingerprint(contractAddress, symbol) + } + else -> contractAddress + } + + return requireNotNull(convertedAddress) { + "Token contract address is invalid" + } + } } \ No newline at end of file From ed3b894c36367530dc5ca4bc1e3701727019d40e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 18:13:17 +0400 Subject: [PATCH 57/90] Updated on 2026-08-14 --- .../tangem/tap/domain/card/CryptoCurrenciesMocks.kt | 1 + .../data/common/currency/NetworkOperations.kt | 2 ++ .../java/com/tangem/domain/tokens/model/Network.kt | 1 + .../com/tangem/domain/tokens/mock/MockNetworks.kt | 3 +++ .../impl/DefaultAddCustomTokenComponent.kt | 3 ++- .../impl/DefaultCustomTokenFormComponent.kt | 2 +- .../preview/PreviewCustomTokenSelectorComponent.kt | 13 ++++++++----- .../entity/customtoken/AddCustomTokenConfig.kt | 8 +++++--- .../managetokens/entity/item/DerivationPathUM.kt | 8 +++----- .../managetokens/ui/AddCustomTokenBottomSheet.kt | 11 +++++++---- .../managetokens/ui/CustomTokenSelectorContent.kt | 13 ++++++++----- 11 files changed, 41 insertions(+), 24 deletions(-) diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt index 5ecd65378b..01faeadf30 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt @@ -68,6 +68,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { isTestnet = false, standardType = Network.StandardType.ERC20, hasFiatFeeRate = true, + canHandleTokens = true, ), name = "NEVER-MIND", symbol = "NEVER-MIND", diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index 6febfc5fb7..4cb4fb8d6e 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -23,6 +23,7 @@ fun getNetwork(networkId: Network.ID, derivationPath: Network.DerivationPath): N currencySymbol = blockchain.currency, standardType = getNetworkStandardType(blockchain), hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + canHandleTokens = blockchain.canHandleTokens(), ) } @@ -45,6 +46,7 @@ fun getNetwork( currencySymbol = blockchain.currency, standardType = getNetworkStandardType(blockchain), hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + canHandleTokens = blockchain.canHandleTokens(), ) } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 59d8d3a517..3543b7094b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -29,6 +29,7 @@ data class Network( val isTestnet: Boolean, val standardType: StandardType, val hasFiatFeeRate: Boolean, + val canHandleTokens: Boolean, ) { init { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 091154dda9..cd410b3487 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -22,6 +22,7 @@ internal object MockNetworks { currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, + canHandleTokens = true, ) val network2 = Network( @@ -33,6 +34,7 @@ internal object MockNetworks { currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, + canHandleTokens = true, ) val network3 = Network( @@ -44,6 +46,7 @@ internal object MockNetworks { currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, + canHandleTokens = true, ) val networkStatus1 = NetworkStatus( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index dd0faa0556..48e7a1b01f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -124,8 +124,9 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( userWalletId = config.userWalletId, network = config.selectedNetwork ?: error("Network is not selected"), derivationPath = config.selectedDerivationPath ?: SelectedDerivationPath( + id = config.selectedNetwork.id, value = config.selectedNetwork.derivationPath, - name = resourceReference(R.string.custom_token_derivation_path_default), + networkName = resourceReference(R.string.custom_token_derivation_path_default), ), formValues = config.formValues, onSelectNetworkClick = ::showNetworkSelector, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt index 982e4979e1..2ca0b7e1a1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt @@ -49,7 +49,7 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor( tokenForm = getInitialTokenForm(), derivationPath = ClickableFieldUM( label = resourceReference(R.string.custom_token_derivation_path), - value = params.derivationPath.name, + value = params.derivationPath.networkName, onClick = ::selectDerivationPath, ), saveToken = { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 1e773cc319..7d596d3f8d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -29,13 +29,15 @@ internal class PreviewCustomTokenSelectorComponent( when (params) { is Params.DerivationPathSelector -> { val d = SelectedDerivationPath( - value = "m/44'/0'/0'/0/$index", - name = stringReference(value = "Network $index"), + id = Network.ID(index.toString()), + value = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"), + networkName = stringReference(value = "Network $index"), ) DerivationPathUM( - value = "m/44'/0'/0'/0/$index", - blockchainName = d.name, + id = d.id.value, + value = d.value.value.orEmpty(), + networkName = d.networkName, isSelected = d.value == params.selectedDerivationPath?.value, onSelectedStateChange = { params.onDerivationPathSelected(d) }, ) @@ -44,7 +46,8 @@ internal class PreviewCustomTokenSelectorComponent( val n = SelectedNetwork( id = Network.ID(index.toString()), name = stringReference(value = "Network $index"), - derivationPath = "m/44'/0'/0'/0/$index", + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"), + canHandleTokens = false, ) CurrencyNetworkUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt index 417cd15585..6a43cdc40d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt @@ -28,11 +28,13 @@ internal data class AddCustomTokenConfig( internal data class SelectedNetwork( val id: Network.ID, val name: TextReference, - val derivationPath: String, + val derivationPath: Network.DerivationPath, + val canHandleTokens: Boolean, ) @Serializable internal data class SelectedDerivationPath( - val value: String, - val name: TextReference, + val id: Network.ID, + val value: Network.DerivationPath, + val networkName: TextReference, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt index 2190c50e4f..bf6387ff0d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt @@ -3,11 +3,9 @@ package com.tangem.features.managetokens.entity.item import com.tangem.core.ui.extensions.TextReference internal data class DerivationPathUM( + override val id: String, val value: String, - val blockchainName: TextReference, + val networkName: TextReference, override val isSelected: Boolean, override val onSelectedStateChange: (Boolean) -> Unit, -) : SelectableItemUM { - - override val id: String = value -} \ No newline at end of file +) : SelectableItemUM \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index b1b3cc0f0a..69ea2418d6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -103,7 +103,8 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< selectedNetwork = SelectedNetwork( id = Network.ID(value = "1"), name = stringReference("Ethereum"), - derivationPath = "m/44'/0'/0'/0/0", + derivationPath = Network.DerivationPath.None, + canHandleTokens = false, ), ), ), @@ -115,7 +116,8 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< selectedNetwork = SelectedNetwork( id = Network.ID(value = "0"), name = stringReference("Ethereum"), - derivationPath = "m/44'/0'/0'/0/0", + derivationPath = Network.DerivationPath.None, + canHandleTokens = false, ), ), ), @@ -125,8 +127,9 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR, popBack = {}, selectedDerivationPath = SelectedDerivationPath( - value = "m/44'/0'/0'/0/0", - name = stringReference("Ethereum"), + id = Network.ID(value = "0"), + value = Network.DerivationPath.None, + networkName = stringReference("Ethereum"), ), ), ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index f5545db44b..67c77ff303 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -154,7 +155,7 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) icon = CurrencyIconState.CoinIcon( url = null, fallbackResId = model.iconResId, - isGrayscale = !model.isSelected, + isGrayscale = false, showCustomBadge = false, ), showCustom = false, @@ -205,7 +206,7 @@ private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Mod contentAlignment = Alignment.CenterStart, ) { Text( - text = "${model.value} ${if (model.isSelected) "- selected" else ""}", + text = "${model.networkName.resolveReference()}: ${model.value} ${if (model.isSelected) "<" else ""}", color = TangemTheme.colors.text.primary1, ) } @@ -235,7 +236,8 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : selectedNetwork = SelectedNetwork( id = Network.ID(value = "0"), name = stringReference(""), - derivationPath = "m/44'/0'/0'/0/0", + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + canHandleTokens = false, ), onNetworkSelected = {}, ), @@ -244,8 +246,9 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = UserWalletId(stringValue = "321"), selectedDerivationPath = SelectedDerivationPath( - value = "m/44'/0'/0'/0/0", - name = stringReference(""), + id = Network.ID(value = "0"), + value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + networkName = stringReference(""), ), onDerivationPathSelected = {}, ), From 01d839b4c5d807e5ea42c1797b223e0e03f4e071 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 15:35:08 +0400 Subject: [PATCH 58/90] Updated on 2026-08-14 --- .../tap/domain/card/CryptoCurrenciesMocks.kt | 26 +++++++++++++++---- .../data/common/currency/NetworkOperations.kt | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt index 01faeadf30..a9e93345cc 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt @@ -1,8 +1,12 @@ package com.tangem.tap.domain.card import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetworkDerivationPath +import com.tangem.data.common.currency.getNetworkStandardType import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency @@ -40,11 +44,23 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { } private fun createCoin(blockchain: Blockchain): CryptoCurrency { - return factory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - derivationStyleProvider = scanResponse.derivationStyleProvider, - )!! + val network = Network( + id = Network.ID(blockchain.id), + backendId = blockchain.toNetworkId(), + name = blockchain.getNetworkName(), + isTestnet = blockchain.isTestnet(), + derivationPath = getNetworkDerivationPath( + blockchain, + extraDerivationPath = null, + scanResponse.derivationStyleProvider, + ), + currencySymbol = blockchain.currency, + standardType = getNetworkStandardType(blockchain), + hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + canHandleTokens = false, + ) + + return factory.createCoin(network = network) } // Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index 4cb4fb8d6e..3666f5f012 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -50,7 +50,7 @@ fun getNetwork( ) } -private fun getNetworkDerivationPath( +fun getNetworkDerivationPath( blockchain: Blockchain, extraDerivationPath: String?, cardDerivationStyleProvider: DerivationStyleProvider?, From 7b7cece46f2d4f96935012757d4252b2590b7b3e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 15:46:27 +0300 Subject: [PATCH 59/90] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 4 +- .../tangem/common/ui/tokens/TokenPriceText.kt | 55 +++++ .../response/TokenMarketInfoResponse.kt | 11 +- .../core/ui/components/fields/SearchBar.kt | 2 + .../core/ui/utils/BigDecimalFormatter.kt | 9 + .../java/com/tangem/utils/StringsSigns.kt | 1 + .../tangem/data/common/utils/RequestUtils.kt | 13 +- .../converters/TokenMarketInfoConverter.kt | 1 + .../repository/DefaultQuotesRepository.kt | 4 +- .../tangem/domain/markets/TokenMarketInfo.kt | 8 +- .../markets/GetCurrencyQuotesUseCase.kt | 24 +++ .../domain/markets/GetTokenQuotesUseCase.kt | 26 --- .../tokens/repository/QuotesRepository.kt | 4 +- .../tokens/repository/MockQuotesRepository.kt | 2 +- .../token/block/TokenMarketBlockComponent.kt | 2 + .../impl/model/MarketsTokenDetailsModel.kt | 133 ++++++------ .../model/converters/InsightsConverter.kt | 138 +++++++------ .../impl/model/converters/MetricsConverter.kt | 17 +- .../converters/PricePerformanceConverter.kt | 30 +-- .../converters/TokenMarketInfoConverter.kt | 12 +- .../formatter/MarketsDateTimeFormatters.kt | 19 +- .../impl/model/state/QuotesStateUpdater.kt | 83 ++++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 9 +- .../details/impl/ui/components/Description.kt | 3 +- .../details/impl/ui/components/LinksBlock.kt | 1 + .../ui/components/PricePerformanceBlock.kt | 5 +- .../ui/components/TokenMarketDetailsBody.kt | 7 +- .../details/impl/ui/state/MetricsUM.kt | 4 +- .../entry/impl/ui/EntryBottomSheetContent.kt | 11 +- .../block/impl/model/TokenMarketBlockModel.kt | 13 +- .../token/block/impl/ui/TokenMarketBlock.kt | 11 +- .../statemanager/MarketsListUMStateManager.kt | 2 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 2 +- .../impl/ui/components/MarketsListItem.kt | 191 +----------------- .../presentation/TokenDetailsFragment.kt | 1 + 35 files changed, 436 insertions(+), 422 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt delete mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 031784429c..f32af0c735 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -41,7 +41,7 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetTokenQuotesUseCase { - return GetTokenQuotesUseCase(quotesRepository = quotesRepository) + fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase { + return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt new file mode 100644 index 0000000000..6a337dae15 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.tokens + +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.res.TangemTheme + +/** + * Text view for token price. + * + * @param price Price of the token. + * @param priceChangeType Type of the price change. + */ +@Composable +fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { + val growColor = TangemTheme.colors.text.accent + val fallColor = TangemTheme.colors.text.warning + val generalColor = TangemTheme.colors.text.primary1 + + val color = remember(generalColor) { Animatable(generalColor) } + var animationSkipped by remember { mutableStateOf(false) } + + LaunchedEffect(price) { + if (animationSkipped.not()) { + animationSkipped = true + return@LaunchedEffect + } + + if (priceChangeType != null) { + val nextColor = when (priceChangeType) { + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + PriceChangeType.NEUTRAL -> return@LaunchedEffect + } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } + } + + Text( + modifier = modifier, + text = price, + color = color.value, + maxLines = 1, + style = TangemTheme.typography.body2, + overflow = TextOverflow.Visible, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index ef485e1829..21b38fdfb0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -67,7 +67,16 @@ data class TokenMarketInfoResponse( val buyPressureChange: Change?, @Json(name = "experienced_buyer_change") val experiencedBuyerChange: Change?, - ) + @Json(name = "networks") + val sourceNetworks: List?, + ) { + data class SourceNetwork( + @Json(name = "network_id") + val id: String, + @Json(name = "network_name") + val name: String, + ) + } data class Change( @Json(name = "24h") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 3de373a0e3..9863c30ba0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController @@ -69,6 +70,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie textStyle = TangemTheme.typography.body2.copy( color = TangemTheme.colors.text.primary1, ), + cursorBrush = SolidColor(TangemTheme.colors.icon.primary1), decorationBox = @Composable { innerTextField -> DecorationBox( state = state, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 91ef672575..fc916ea18a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -336,6 +336,15 @@ object BigDecimalFormatter { ): String { if (amount == null) return EMPTY_BALANCE_SIGN + if (amount < BigDecimal.ONE) { + return formatFiatPriceUncapped( + fiatAmount = amount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) + } + val rawAmount = formatCompactAmount( amount = amount, locale = locale, diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 15d69db683..b2d8906fb0 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -9,4 +9,5 @@ object StringsSigns { const val DASH_SIGN = "—" const val LOWER_SIGN = "<" const val TILDE_SIGN = "~" + const val NON_BREAKING_SPACE = '\u00A0' } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt index e7f3fbfe45..710a982835 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt @@ -7,8 +7,10 @@ import kotlinx.coroutines.yield import timber.log.Timber import kotlin.coroutines.cancellation.CancellationException -@Suppress("UnconditionalJumpStatementInLoop") -suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): T { +@Suppress("UnconditionalJumpStatementInLoop", "MagicNumber") +suspend fun retryOnError(priority: Boolean = false, startRetryDelay: Int = 500, call: suspend () -> T): T { + var currentDelay = startRetryDelay + var priorityCounter = 5 while (true) { return try { call() @@ -19,9 +21,12 @@ suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): Timber.e(e, "Error occurred during retryOnError block") - if (priority.not()) { + if (priority && priorityCounter > 0) { + --priorityCounter + } else { yield() - delay(timeMillis = 500) + delay(timeMillis = currentDelay.toLong()) + currentDelay *= 2 } continue diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index e9314b0d7e..d77d7618fe 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -57,6 +57,7 @@ internal object TokenMarketInfoConverter : Converter): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return appPreferencesStore.getObject( key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, ) .distinctUntilChanged() .filterNotNull() .flatMapLatest { appCurrency -> - fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false) + fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh) quotesStore.get(currenciesIds).map(quotesConverter::convertSet) } .cancellable() diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt index 7fe496e5f1..6f02eb0639 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -27,7 +27,13 @@ data class TokenMarketInfo( val liquidityChange: Change?, val buyPressureChange: Change?, val experiencedBuyerChange: Change?, - ) + val sourceNetworks: List, + ) { + data class SourceNetwork( + val id: String, + val name: String, + ) + } data class Change( val day: BigDecimal?, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt new file mode 100644 index 0000000000..1ec184fbdc --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.markets + +import arrow.core.* +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.repository.QuotesRepository +import kotlinx.coroutines.flow.* + +@Suppress("UnusedPrivateMember") +class GetCurrencyQuotesUseCase( + private val quotesRepository: QuotesRepository, +) { + // TODO apply interval parameter [REDACTED_TASK_KEY] + operator fun invoke( + currencyID: CryptoCurrency.ID, + interval: PriceChangeInterval, + refresh: Boolean, + ): Flow> { + return quotesRepository.getQuotesUpdates( + currenciesIds = setOf(currencyID), + refresh = refresh, + ).map { it.firstOrNull().toOption() }.catch { emit(None) } + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt deleted file mode 100644 index 059c80ac82..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain.markets - -import arrow.core.Either -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.repository.QuotesRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -@Suppress("UnusedPrivateMember") -class GetTokenQuotesUseCase( - private val quotesRepository: QuotesRepository, -) { - operator fun invoke(tokenId: String, interval: PriceChangeInterval): Flow> { - return flowOf( - Either.catch { - Quote( - rawCurrencyId = "USD", - fiatRate = 100.toBigDecimal(), // mock - priceChange = 10.toBigDecimal(), // mock - ) - }.mapLeft {}, - ) - // TODO implement quotes fetching from repository [REDACTED_TASK_KEY] - // quotesRepository.getQuotesUpdates() - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 4ebd284421..57a8429697 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -12,12 +12,12 @@ interface QuotesRepository { /** * Retrieves updates of quotes for a set of specified cryptocurrencies, identified by their unique IDs. * - * Loads remote quotes if they have expired. + * Loads remote quotes if they have expired or if [refresh] is `true`. * * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ - fun getQuotesUpdates(currenciesIds: Set): Flow> + fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean = false): Flow> /** * Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index 3d45f98879..393432cf16 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -13,7 +13,7 @@ internal class MockQuotesRepository( private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotesUpdates(currenciesIds: Set): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return quotes.map { it.getOrElse { e -> throw e } } } diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt index dbd1b4f877..9cc202bdb4 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.token.block import androidx.compose.runtime.Stable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.serialization.Serializable @Stable @@ -10,6 +11,7 @@ interface TokenMarketBlockComponent : ComposableContentComponent { @Serializable data class Params( + val cryptoCurrencyID: CryptoCurrency.ID, val tokenId: String, val tokenName: String, val tokenSymbol: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 41a616a52f..3f01b427d9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -13,7 +13,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter @@ -24,6 +23,10 @@ import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.formatter.* +import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice +import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween +import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval +import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM @@ -76,6 +79,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( urlOpener.openUrl(it.url) }, ) + private val descriptionConverter = DescriptionConverter( onReadModeClicked = { showInfoBottomSheet(it) @@ -95,7 +99,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( BigDecimalFormatter.formatFiatPriceUncapped( fiatAmount = value, fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = "", + fiatCurrencySymbol = currentAppCurrency.value.symbol, ) }, ) @@ -115,7 +119,8 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), ) - private var lastUpdatedTimestamp: Long = DateTime.now().millis + private val currentTokenInfo = MutableStateFlow(null) + private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis) val isVisibleOnScreen = MutableStateFlow(false) val networksState = MutableStateFlow(TokenNetworksState.Loading) @@ -155,6 +160,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), ) + private val quotesStateUpdater = QuotesStateUpdater( + currentAppCurrency = Provider { currentAppCurrency.value }, + state = state, + currentQuotes = currentQuotes, + lastUpdatedTimestamp = lastUpdatedTimestamp, + currentTokenInfo = currentTokenInfo, + ) + private val loadChartJobHolder = JobHolder() init { @@ -279,41 +292,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) tokenMarketInfo.fold( - ifRight = { result -> - currentQuotes.value = result.quotes - val percent = result.quotes.getPercentByInterval(interval = state.value.selectedInterval) - state.update { - it.copy( - priceText = result.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), - priceChangePercentText = result.quotes.getFormattedPercentByInterval( - interval = it.selectedInterval, - ), - priceChangeType = percent.percentChangeType(), - body = MarketsTokenDetailsUM.Body.Content( - description = descriptionConverter.convert(result), - infoBlocks = infoConverter.convert(result), - ), - ) - } - - val networks = result.networks?.filter { - BlockchainUtils.isSupportedNetworkId(it.networkId) - } - - networksState.value = if (networks.isNullOrEmpty()) { - TokenNetworksState.NoNetworksAvailable - } else { - TokenNetworksState.NetworksAvailable(networks) - } - - chartDataProducer.runTransaction { - updateLook { - it.copy( - type = getChartTypeByPercent(percent), - ) - } - } - }, + ifRight = { result -> updateInfo(result) }, ifLeft = { state.update { if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) { @@ -333,42 +312,50 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } - private suspend fun updateQuotes(newQuotes: TokenQuotes) { - val triggerPriceChangeType = getFormattedPriceChange( - currentPrice = currentQuotes.value.currentPrice, - updatedPrice = newQuotes.currentPrice, - ) - val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { - triggeredEvent( - data = triggerPriceChangeType, - onConsume = { - state.update { it.copy(triggerPriceChange = consumedEvent()) } - }, + private fun updateInfo(newInfo: TokenMarketInfo) { + lastUpdatedTimestamp.value = DateTime.now().millis + + currentTokenInfo.value = newInfo + currentQuotes.value = newInfo.quotes + + val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval) + state.update { + it.copy( + priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( + interval = it.selectedInterval, + ), + priceChangeType = percent.percentChangeType(), + body = MarketsTokenDetailsUM.Body.Content( + description = descriptionConverter.convert(newInfo), + infoBlocks = infoConverter.convert(newInfo), + ), ) - } else { - consumedEvent() } + val networks = newInfo.networks?.filter { + BlockchainUtils.isSupportedNetworkId(it.networkId) + } + + networksState.value = if (networks.isNullOrEmpty()) { + TokenNetworksState.NoNetworksAvailable + } else { + TokenNetworksState.NetworksAvailable(networks) + } + + chartDataProducer.runTransaction { + updateLook { + it.copy( + type = getChartTypeByPercent(percent), + ) + } + } + } + + private suspend fun updateQuotes(newQuotes: TokenQuotes) { + quotesStateUpdater.updateQuotes(newQuotes) + val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) - val priceChangeType = percent.percentChangeType() - - // wait until marker is removed - state.first { it.markerSet.not() } - - currentQuotes.value = newQuotes - lastUpdatedTimestamp = DateTime.now().millis - - state.update { stateToUpdate -> - stateToUpdate.copy( - priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency.value), - priceChangePercentText = newQuotes.getFormattedPercentByInterval( - interval = stateToUpdate.selectedInterval, - ), - priceChangeType = priceChangeType, - triggerPriceChange = trigger, - dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval), - ) - } chartDataProducer.runTransaction { updateLook { @@ -489,9 +476,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( launch { while (true) { delay(timeMillis) - // Update quotes only when the container bottom sheet is in the expanded state - - // and is visible on the screen + // Update quotes only when content visible on the screen isVisibleOnScreen.first { it } loadQuotes() @@ -504,7 +489,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( interval = interval, startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( interval = interval, - currentTimestamp = lastUpdatedTimestamp, + currentTimestamp = lastUpdatedTimestamp.value, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt index 9bbcff51d9..376bf704a0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -2,7 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo @@ -14,7 +14,7 @@ import com.tangem.utils.Provider import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @Stable @@ -48,7 +48,10 @@ internal class InsightsConverter( onInfoClick( InfoBottomSheetContent( title = resourceReference(R.string.markets_token_details_insights), - body = stringReference("//TODO"), + body = resourceReference( + R.string.markets_insights_info_description_message, + wrappedList(value.sourceNetworks.joinToString { it.name }), + ), ), ) }, @@ -62,74 +65,79 @@ internal class InsightsConverter( liquidityChange: BigDecimal?, buyPressureChange: BigDecimal?, ): ImmutableList { - return persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = experiencedBuyerChange.convertChange(), - change = experiencedBuyerChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - body = resourceReference(R.string.markets_token_details_experienced_buyers_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = buyPressureChange.convertChange(isFiatValue = true), - change = buyPressureChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_buy_pressure), - body = resourceReference(R.string.markets_token_details_buy_pressure_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = holdersChange.convertChange(), - change = holdersChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_holders), - body = resourceReference(R.string.markets_token_details_holders_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = liquidityChange.convertChange(), - change = liquidityChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_liquidity), - body = resourceReference(R.string.markets_token_details_liquidity_description), - ), - ) - }, - ), - ) + return listOfNotNull( + experiencedBuyerChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = experiencedBuyerChange.convertChange(), + change = experiencedBuyerChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + body = resourceReference(R.string.markets_token_details_experienced_buyers_description), + ), + ) + }, + ) + }, + buyPressureChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = buyPressureChange.convertChange(isFiatValue = true), + change = buyPressureChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_buy_pressure), + body = resourceReference(R.string.markets_token_details_buy_pressure_description), + ), + ) + }, + ) + }, + holdersChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = holdersChange.convertChange(), + change = holdersChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_holders), + body = resourceReference(R.string.markets_token_details_holders_description), + ), + ) + }, + ) + }, + liquidityChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = liquidityChange.convertChange(), + change = liquidityChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_liquidity), + body = resourceReference(R.string.markets_token_details_liquidity_description), + ), + ) + }, + ) + }, + ).toImmutableList() } - private fun BigDecimal?.changeType(): InfoPointUM.ChangeType? { + private fun BigDecimal.changeType(): InfoPointUM.ChangeType? { return when { - this == null -> null this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN else -> null } } - private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String { - if (this == null) return StringsSigns.DASH_SIGN - + private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { val value = if (isFiatValue) { val currency = appCurrency() BigDecimalFormatter.formatCompactFiatAmount( @@ -141,11 +149,9 @@ internal class InsightsConverter( BigDecimalFormatter.formatCompactAmount(amount = this.abs()) } - val spacing = if (isFiatValue) " " else "" - return when { - this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value - this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value + this > BigDecimal.ZERO -> StringsSigns.PLUS + value + this < BigDecimal.ZERO -> StringsSigns.MINUS + value this == BigDecimal.ZERO -> value else -> StringsSigns.DASH_SIGN } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt index 4bf9f1e111..c6c57682f7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -14,9 +14,6 @@ import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -import java.math.RoundingMode -import java.text.NumberFormat -import java.util.Locale @Stable internal class MetricsConverter( @@ -116,19 +113,15 @@ internal class MetricsConverter( private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { return if (crypto) { - val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply { - maximumFractionDigits = 0 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - formatter.format(this) + BigDecimalFormatter.formatCompactAmount( + amount = this ?: BigDecimal.ZERO, + ) } else { val currency = appCurrency() - BigDecimalFormatter.formatFiatAmount( - fiatAmount = this, + BigDecimalFormatter.formatCompactFiatAmount( + amount = this, fiatCurrencyCode = currency.code, fiatCurrencySymbol = currency.symbol, - decimals = 0, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt index 0ccf01cde7..38ee8682bf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt @@ -7,24 +7,23 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.utils.Provider import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter import java.math.BigDecimal import java.math.RoundingMode @Stable internal class PricePerformanceConverter( private val appCurrency: Provider, -) : Converter { +) { - override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM { + fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { return PricePerformanceUM( - h24 = value.day.convert(), - month = value.month.convert(), - all = value.allTime.convert(), + h24 = value.day.convert(currentPrice), + month = value.month.convert(currentPrice), + all = value.allTime.convert(currentPrice), ) } - private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value { + private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value { if (this == null) { return PricePerformanceUM.Value( low = StringsSigns.DASH_SIGN, @@ -36,7 +35,7 @@ internal class PricePerformanceConverter( return PricePerformanceUM.Value( low = low.convert(), high = high.convert(), - indicatorFraction = calculateFraction(), + indicatorFraction = calculateFraction(currentPrice), ) } @@ -50,10 +49,15 @@ internal class PricePerformanceConverter( ) } - private fun TokenMarketInfo.Range.calculateFraction(): Float { - if (low == null || high == null || low == BigDecimal.ZERO) return 0f - return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP) - .setScale(2, RoundingMode.HALF_UP) - .toFloat().coerceAtMost(1f) + private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float { + return when { + low == null || high == null || high == BigDecimal.ZERO || currentPrice < low -> 0f + currentPrice > high || low == high -> 1f + else -> { + (currentPrice - low!!).divide(high!! - low!!, RoundingMode.HALF_UP) + .setScale(2, RoundingMode.HALF_UP) + .toFloat().coerceAtMost(1f) + } + } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt index f03cbff9c4..e90ad2b0d3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -17,6 +17,9 @@ internal class TokenMarketInfoConverter( ) : Converter { private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) + + @Suppress("UnusedPrivateMember") + // TODO second markets iteration private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency) @@ -25,9 +28,14 @@ internal class TokenMarketInfoConverter( override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { return MarketsTokenDetailsUM.InformationBlocks( insights = value.insights?.let { insightsConverter.convert(it) }, - securityScore = securityScoreConverter.convert(Unit), + securityScore = null, metrics = value.metrics?.let { metricsConverter.convert(it) }, - pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) }, + pricePerformance = value.pricePerformance?.let { + pricePerformanceConverter.convert( + value = it, + currentPrice = value.quotes.currentPrice, + ) + }, links = value.links?.let { linksConverter.convert(it) }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt index acc0ec44fa..5abc296917 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -23,7 +23,7 @@ internal object MarketsDateTimeFormatters { private val dateFormatter = DateTimeFormatters.dateDDMMYYYY - internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { + fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { return when (interval) { PriceChangeInterval.H24 -> { value: BigDecimal -> value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter) @@ -44,7 +44,7 @@ internal object MarketsDateTimeFormatters { } } - internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { + fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { return when (interval) { PriceChangeInterval.H24 -> resourceReference(R.string.common_today) PriceChangeInterval.WEEK, @@ -78,10 +78,7 @@ internal object MarketsDateTimeFormatters { } } - internal fun formatDateByIntervalWithMarker( - interval: PriceChangeInterval, - markerTimestamp: BigDecimal, - ): TextReference { + fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference { return when (interval) { PriceChangeInterval.H24, PriceChangeInterval.WEEK, @@ -127,4 +124,14 @@ internal object MarketsDateTimeFormatters { PriceChangeInterval.ALL_TIME -> 0 } } + + fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference { + return formatDateByInterval( + interval = interval, + startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( + interval = interval, + currentTimestamp = currentTimestamp, + ), + ) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt new file mode 100644 index 0000000000..dd664b03ca --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -0,0 +1,83 @@ +package com.tangem.features.markets.details.impl.model.state + +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenQuotes +import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter +import com.tangem.features.markets.details.impl.model.formatter.* +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.utils.Provider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import org.joda.time.DateTime +import java.math.BigDecimal + +internal class QuotesStateUpdater( + private val currentAppCurrency: Provider, + private val state: MutableStateFlow, + private val currentQuotes: MutableStateFlow, + private val lastUpdatedTimestamp: MutableStateFlow, + private val currentTokenInfo: MutableStateFlow, +) { + private val pricePerformanceConverter = PricePerformanceConverter(currentAppCurrency) + + suspend fun updateQuotes(newQuotes: TokenQuotes) { + val triggerPriceChangeType = getFormattedPriceChange( + currentPrice = currentQuotes.value.currentPrice, + updatedPrice = newQuotes.currentPrice, + ) + val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { + triggeredEvent( + data = triggerPriceChangeType, + onConsume = { + state.update { it.copy(triggerPriceChange = consumedEvent()) } + }, + ) + } else { + consumedEvent() + } + + val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) + val priceChangeType = percent.percentChangeType() + + // wait until marker is removed + state.first { it.markerSet.not() } + + currentQuotes.value = newQuotes + lastUpdatedTimestamp.value = DateTime.now().millis + + state.update { stateToUpdate -> + stateToUpdate.copy( + priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()), + priceChangePercentText = newQuotes.getFormattedPercentByInterval( + interval = stateToUpdate.selectedInterval, + ), + priceChangeType = priceChangeType, + triggerPriceChange = trigger, + dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString( + stateToUpdate.selectedInterval, + currentTimestamp = lastUpdatedTimestamp.value, + ), + body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice), + ) + } + } + + private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body { + val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this + + return if (this is MarketsTokenDetailsUM.Body.Content) { + copy( + infoBlocks = infoBlocks.copy( + pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price), + ), + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index e801ed3cb0..23e21f4840 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.details.impl.ui +import android.content.res.Configuration import androidx.compose.animation.Animatable import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween @@ -193,7 +194,7 @@ private fun TokenPriceText( val fallColor = TangemTheme.colors.text.warning val generalColor = TangemTheme.colors.text.primary1 - val color = remember { Animatable(generalColor) } + val color = remember(generalColor) { Animatable(generalColor) } EventEffect(triggerPriceChange) { val nextColor = when (it) { @@ -268,12 +269,11 @@ fun PriceChangeInterval.getText(): TextReference { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview() { TangemThemePreview { - Content( - modifier = Modifier.background(TangemTheme.colors.background.tertiary), - addTopBarStatusBarInsets = false, + MarketsTokenDetailsContent( state = MarketsTokenDetailsUM( tokenName = "Token Name", priceText = "$0.00000000324", @@ -303,6 +303,7 @@ private fun Preview() { onBackClick = {}, backgroundColor = TangemTheme.colors.background.tertiary, portfolioBlock = {}, + addTopBarStatusBarPadding = false, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt index e895b421f5..aca7dd32ae 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer import com.tangem.features.markets.impl.R +import com.tangem.utils.StringsSigns @Composable internal fun Description( @@ -33,7 +34,7 @@ internal fun Description( append(description.resolveReference()) } withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { - append(" " + stringResource(R.string.common_read_more)) + append(" " + stringResource(R.string.common_read_more).replace(' ', StringsSigns.NON_BREAKING_SPACE)) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index ca7e4d4390..92d905ff2e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -60,6 +60,7 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { title = stringResource(id = R.string.markets_token_details_blockchain_site), links = state.blockchainSite, onLinkClick = state.onLinkClick, + lastBlock = true, ) } }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 66a09fb533..057d1bbed6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -124,15 +124,16 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi ) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( + modifier = Modifier.weight(1f), text = state.low, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, ) - SpacerW8() Text( + modifier = Modifier.weight(1f), text = state.high, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 620e182622..5d9e93fd4e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -126,9 +126,10 @@ private fun LazyListScope.loadingInfoBlocks() { InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) } - item("securityScore-loading") { - SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) - } + // TODO second markets iteration + // item("securityScore-loading") { + // SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) + // } item("metrics-loading") { MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt index b1982093d4..8b28533fb3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.markets.details.impl.ui.state -import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.ImmutableList internal data class MetricsUM( - val metrics: PersistentList, + val metrics: ImmutableList, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt index dccea6ad25..5f30e89585 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -30,7 +30,6 @@ internal fun EntryBottomSheetContent( modifier: Modifier = Modifier, ) { val primary = TangemTheme.colors.background.primary - val secondary = TangemTheme.colors.background.secondary val backgroundColor = remember { Animatable(primary) } LocalMainBottomSheetColor.current.value = backgroundColor.value @@ -73,7 +72,7 @@ private fun BackgroundColorEffects( bottomSheetState: State, ) { val primary = TangemTheme.colors.background.primary - val secondary = TangemTheme.colors.background.secondary + val tertiary = TangemTheme.colors.background.tertiary // Order of LaunchedEffects is important here @@ -81,7 +80,7 @@ private fun BackgroundColorEffects( when (activeChild) { is MarketsEntryChildFactory.Child.TokenDetails -> { backgroundColor.animateTo( - secondary, + tertiary, animationSpec = tween(durationMillis = 500), ) } @@ -99,7 +98,7 @@ private fun BackgroundColorEffects( when (bottomSheetState.value) { BottomSheetState.EXPANDED -> { backgroundColor.animateTo( - secondary, + tertiary, animationSpec = tween(durationMillis = 100), ) } @@ -113,12 +112,12 @@ private fun BackgroundColorEffects( } } - LaunchedEffect(primary, secondary) { + LaunchedEffect(primary, tertiary) { if (backgroundColor.isRunning) return@LaunchedEffect when (activeChild) { is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.snapTo(secondary) + backgroundColor.snapTo(tertiary) } MarketsEntryChildFactory.Child.TokenList -> { backgroundColor.snapTo(primary) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 42184de78e..5967eaa915 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -17,6 +17,8 @@ import com.tangem.domain.markets.* import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -29,7 +31,7 @@ internal class TokenMarketBlockModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, - private val getTokenQuotesUseCase: GetTokenQuotesUseCase, + private val getTokenQuotesUseCase: GetCurrencyQuotesUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { @@ -46,6 +48,7 @@ internal class TokenMarketBlockModel @Inject constructor( ) private var quotesState: QuotesState? = null + private val quotesUpdateJobHolder = JobHolder() val state = MutableStateFlow( TokenMarketBlockUM( @@ -65,10 +68,11 @@ internal class TokenMarketBlockModel @Inject constructor( private fun startFetching() { modelScope.launch { getTokenQuotesUseCase( - tokenId = params.tokenId, + currencyID = params.cryptoCurrencyID, interval = PriceChangeInterval.H24, + refresh = true, ).collect { - it.onRight { res -> + it.onSome { res -> quotesState = QuotesState( currentPrice = res.fiatRate, h24Percent = res.priceChange, @@ -90,7 +94,7 @@ internal class TokenMarketBlockModel @Inject constructor( ) } } - } + }.saveIn(quotesUpdateJobHolder) modelScope.launch(dispatchers.main) { val result = getTokenPriceChartUseCase( @@ -131,7 +135,6 @@ internal class TokenMarketBlockModel @Inject constructor( ), ) - // FIXME navigation crash [REDACTED_TASK_KEY] router.push( AppRoute.MarketsTokenDetails( token = tokenParam, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 67e5d7305e..8191f0addf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.TextShimmer @@ -83,19 +84,21 @@ private fun LeftSide( FlowRow( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { - Text( - text = priceText, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, + TokenPriceText( + modifier = Modifier.alignByBaseline(), + price = priceText, + priceChangeType = type, ) Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { PriceChangeInPercent( + modifier = Modifier.alignByBaseline(), valueInPercent = percentText, type = type, ) Text( + modifier = Modifier.alignByBaseline(), text = stringResource(id = R.string.wallet_marketprice_block_update_time), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 50170ce417..f22b26c622 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -192,7 +192,7 @@ internal class MarketsListUMStateManager( private fun state(): MarketsListUM = MarketsListUM( list = ListUM.Loading, searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + placeholderText = resourceReference(R.string.common_search), query = "", onQueryChange = { searchQuery = it }, isActive = false, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index e7164e73ab..fa2964129f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -256,7 +256,7 @@ private fun Preview() { onItemClick = {}, ), searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + placeholderText = resourceReference(R.string.common_search), query = "", onQueryChange = {}, isActive = false, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index 7658893fb7..c153024da0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -1,185 +1,46 @@ package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.IntOffset import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.features.markets.impl.R -import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.utils.StringsSigns.MINUS -import kotlinx.coroutines.launch -import kotlin.math.roundToInt import kotlin.random.Random -internal enum class DragValue { Start, End } - -const val SWIPE_THRESHOLD_PERCENT = 0.8f -const val SWIPE_VELOCITY_THRESHOLD = 20f - -@Suppress("LongMethod") -@OptIn(ExperimentalFoundationApi::class) @Composable -fun MarketsListItem( - model: MarketsListItemUM, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, - onSwipeToAction: () -> Unit = {}, -) { - val actionWidth = TangemTheme.dimens.size68 - val actionWidthPx = with(LocalDensity.current) { actionWidth.toPx() } - - val hapticManager = LocalHapticManager.current - - val anchors = DraggableAnchors { - DragValue.Start at 0f - DragValue.End at -actionWidthPx - } - val state = remember { - AnchoredDraggableState( - initialValue = DragValue.Start, - anchors = anchors, - positionalThreshold = { it * (1 - SWIPE_THRESHOLD_PERCENT) }, - velocityThreshold = { SWIPE_VELOCITY_THRESHOLD }, - animationSpec = tween(easing = FastOutSlowInEasing), - confirmValueChange = { it == DragValue.Start }, - ) - } - val dragInteractionSource = remember { MutableInteractionSource() } - val clickInteractionSource = remember { MutableInteractionSource() } - val isInDraggedState by dragInteractionSource.collectIsDraggedAsState() - - LaunchedEffect(Unit) { - var actionPerformed = false - var releasePerformed = true - launch { - snapshotFlow { state.offset } - .collect { - val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT - if (it < border && actionPerformed.not()) { - hapticManager.perform(TangemHapticEffect.View.GestureThresholdActivate) - actionPerformed = true - releasePerformed = false - } - - if (it > border) { - if (releasePerformed.not()) { - hapticManager.perform(TangemHapticEffect.View.GestureThresholdDeactivate) - releasePerformed = true - } - actionPerformed = false - } - } - } - launch { - snapshotFlow { isInDraggedState } - .collect { - if (it.not() && actionPerformed) { - releasePerformed = true - onSwipeToAction() - } - } - } - } - - Box( - modifier = Modifier - .height(intrinsicSize = IntrinsicSize.Min) - .fillMaxWidth(), - ) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .fillMaxHeight() - .offset { - IntOffset( - x = actionWidthPx.roundToInt() + - state - .requireOffset() - .toInt(), - y = 0, - ) - } - .width(actionWidth) - .background(TangemTheme.colors.control.checked), - contentAlignment = Alignment.Center, - ) { - Image( - modifier = Modifier.size(TangemTheme.dimens.size28), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_mini_28), - colorFilter = ColorFilter.tint(TangemTheme.colors.icon.primary2), - contentDescription = null, - ) - } - - Box( - modifier = modifier - .align(Alignment.CenterStart) - .clip(RectangleShape) - .offset { - IntOffset( - x = state - .requireOffset() - .toInt(), - y = 0, - ) - } - .anchoredDraggable( - state = state, - orientation = Orientation.Horizontal, - interactionSource = dragInteractionSource, - ) - .clickable( - enabled = true, - interactionSource = clickInteractionSource, - indication = rememberRipple(), - onClick = onClick, - ), - ) { - MarketsListItemContent(model = model) - } - } +internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { + MarketsListItemContent( + modifier = modifier + .fillMaxWidth() + .clip(RectangleShape) + .clickable(onClick = onClick), + model = model, + ) } @Composable @@ -325,38 +186,6 @@ private fun RowScope.TokenMarketCapText(text: String) { ) } -@Composable -private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { - val growColor = TangemTheme.colors.text.accent - val fallColor = TangemTheme.colors.text.warning - val generalColor = TangemTheme.colors.text.primary1 - - val color = remember { Animatable(generalColor) } - - LaunchedEffect(price) { - if (priceChangeType != null) { - val nextColor = when (priceChangeType) { - PriceChangeType.UP, - -> growColor - PriceChangeType.DOWN -> fallColor - PriceChangeType.NEUTRAL -> return@LaunchedEffect - } - - color.animateTo(nextColor, snap()) - color.animateTo(generalColor, tween(durationMillis = 500)) - } - } - - Text( - modifier = modifier, - text = price, - color = color.value, - maxLines = 1, - style = TangemTheme.typography.body2, - overflow = TextOverflow.Visible, - ) -} - @Composable private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) { val chartWidth = TangemTheme.dimens.size56 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index 9385875fb1..c942539d2f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -88,6 +88,7 @@ internal class TokenDetailsFragment : ComposeFragment() { val tokenId = id.rawCurrencyId ?: return null // token price is not available return TokenMarketBlockComponent.Params( + cryptoCurrencyID = id, tokenId = tokenId, tokenName = name, tokenSymbol = symbol, From 4f7e1822857f35826457b110af6fce8ef3679b0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 16:04:23 +0300 Subject: [PATCH 60/90] Updated on 2026-08-14 --- .../com/tangem/common/ui/charts/state/Utils.kt | 13 +++++++++++++ .../converter/PriceAndTimePointValuesConverter.kt | 11 ++--------- .../details/impl/model/MarketsTokenDetailsModel.kt | 5 +++-- .../token/block/impl/model/TokenMarketBlockModel.kt | 8 ++++++-- .../model/converters/MarketsTokenItemConverter.kt | 3 ++- 5 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt new file mode 100644 index 0000000000..558f63dbde --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt @@ -0,0 +1,13 @@ +package com.tangem.common.ui.charts.state + +import kotlinx.collections.immutable.toImmutableList + +fun MarketChartData.Data.sorted(): MarketChartData.Data { + val points = this.x.zip(this.y).sortedBy { it.first } + val (x, y) = points.unzip() + + return MarketChartData.Data( + x = x.toImmutableList(), + y = y.toImmutableList(), + ) +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt index 8eeec1a691..e51a8f72a6 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt @@ -22,11 +22,6 @@ class PriceAndTimePointValuesConverter( private val formatYValuesCache = mutableMapOf() private val formatXValuesCache = mutableMapOf() - private data class Point( - val x: BigDecimal, - val y: BigDecimal, - ) - override fun convert(data: MarketChartData.Data): MarketChartRawData { formatYValuesCache.clear() formatXValuesCache.clear() @@ -38,10 +33,8 @@ class PriceAndTimePointValuesConverter( ) minMaxCache = cache - val points = data.x.zip(data.y) { x, y -> Point(x, y) }.sortedBy { it.x } - - val normY = points.map { it.y }.normalizeToDouble(min = cache.minY, max = cache.maxY) - val normX = points.map { it.x }.normalizeTime(min = cache.minX, max = cache.maxX) + val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY) + val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX) return if (normX.size > MAX_POINTS) { LTThreeBuckets diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 3f01b427d9..e899cfeb23 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.sorted import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -237,9 +238,9 @@ internal class MarketsTokenDetailsModel @Inject constructor( chart.onRight { chartDataProducer.runTransactionSuspend { chartData = MarketChartData.Data( - x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(), y = it.priceY.toImmutableList(), - ) + x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted() updateLook { it.copy( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 5967eaa915..bb584f1731 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -105,13 +106,16 @@ internal class TokenMarketBlockModel @Inject constructor( ) result.onRight { res -> + // wait until quotes are loaded + state.first { it.currentPrice != null } + state.update { stateToUpdate -> stateToUpdate.copy( chartData = priceAndTimePointValuesConverter.convert( MarketChartData.Data( y = res.priceY.toImmutableList(), - x = res.timeStamps.sorted().map { it.toBigDecimal() }.toImmutableList(), - ), + x = res.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted(), ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index c43e29f406..295cebedcf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.tokenlist.impl.model.converters import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -115,7 +116,7 @@ internal class MarketsTokenItemConverter( MarketChartData.Data( y = ct.priceY.toImmutableList(), x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(), - ), + ).sorted(), ) } } From 4f3d1f10c7a264d40a2876c328973405d72e822f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 12:24:26 +0300 Subject: [PATCH 61/90] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 15 ++ .../DefaultTransactionRepository.kt | 15 ++ .../domain/feedback/FeedbackDataBuilder.kt | 9 +- .../feedback/models/FeedbackEmailType.kt | 4 +- .../utils/EmailMessageBodyResolver.kt | 10 +- .../transaction/TransactionRepository.kt | 8 + .../usecase/SendMultipleTransactionUseCase.kt | 147 ++++++++++++++++++ .../SetConfirmationStateAssentTransformer.kt | 18 +-- ...etConfirmationStateCompletedTransformer.kt | 18 +-- .../viewmodel/StakingViewModel.kt | 102 +++++++----- gradle/dependencies.toml | 2 +- 11 files changed, 268 insertions(+), 80 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt 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 73c0236567..e50891f228 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 @@ -42,6 +42,21 @@ internal object TransactionDomainModule { ) } + @Provides + @Singleton + fun provideSendMultipleTransactionUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, + walletManagersFacade: WalletManagersFacade, + ): SendMultipleTransactionUseCase { + return SendMultipleTransactionUseCase( + demoConfig = DemoConfig(), + cardSdkConfigRepository = cardSdkConfigRepository, + transactionRepository = transactionRepository, + walletManagersFacade = walletManagersFacade, + ) + } + @Provides @Singleton fun provideAssociateAssetUseCase( diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 87f16d07be..0452da5a7c 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -158,6 +158,21 @@ internal class DefaultTransactionRepository( (walletManager as TransactionSender).send(txData, signer) } + override suspend fun sendMultipleTransactions( + txsData: List, + 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).sendMultiple(txsData, signer) + } + override fun createTransactionDataExtras( data: String, network: Network, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 30d5dd83a8..91f1f6b990 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -86,10 +86,13 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") } - fun addStakingInfo(validatorName: String?, transactionType: String?, unsignedTransaction: String?) { + fun addStakingInfo(validatorName: String?, transactionTypes: List, unsignedTransactions: List) { builder.appendKeyValue("Validator", validatorName ?: "unknown") - builder.appendKeyValue("Action", transactionType ?: "unknown") - builder.appendKeyValue("Unsigned transaction", unsignedTransaction ?: "unknown") + builder.appendKeyValue("Action", transactionTypes.joinToString(separator = "\n").ifEmpty { "unknown" }) + builder.appendKeyValue( + key = "Unsigned transaction", + value = unsignedTransactions.joinToString(separator = "\n") { it ?: "unknown" }.ifEmpty { "unknown" }, + ) } fun addDelimiter(): StringBuilder = builder.appendDelimiter() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 1be0afb1ff..735365aeac 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -27,7 +27,7 @@ sealed interface FeedbackEmailType { data class StakingProblem( override val cardInfo: CardInfo, val validatorName: String?, - val transactionType: String?, - val unsignedTransaction: String?, + val transactionTypes: List, + val unsignedTransactions: List, ) : FeedbackEmailType } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index c314b7a4bc..ae17e1da30 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -26,8 +26,8 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.StakingProblem -> addStakingProblemBody( type.cardInfo, type.validatorName, - type.transactionType, - type.unsignedTransaction, + type.transactionTypes, + type.unsignedTransactions, ) } @@ -81,8 +81,8 @@ internal class EmailMessageBodyResolver( private suspend fun FeedbackDataBuilder.addStakingProblemBody( cardInfo: CardInfo, validatorName: String?, - transactionType: String?, - unsignedTransaction: String?, + transactionTypes: List, + unsignedTransactions: List, ) { addCardInfo(cardInfo) addDelimiter() @@ -102,7 +102,7 @@ internal class EmailMessageBodyResolver( addDelimiter() } - addStakingInfo(validatorName, transactionType, unsignedTransaction) + addStakingInfo(validatorName, transactionTypes, unsignedTransactions) addDelimiter() addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 6784c1726f..247f3c7605 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult +import com.tangem.blockchain.common.transaction.TransactionsSendResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.models.TransactionType @@ -58,6 +59,13 @@ interface TransactionRepository { network: Network, ): com.tangem.blockchain.extensions.Result + suspend fun sendMultipleTransactions( + txsData: List, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): com.tangem.blockchain.extensions.Result + fun createTransactionDataExtras( data: String, network: Network, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt new file mode 100644 index 0000000000..c7fce8b278 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt @@ -0,0 +1,147 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.transaction.TransactionsSendResult +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.network.ResultChecker +import com.tangem.common.core.TangemSdkError +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.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.DemoTransactionSender +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 +import com.tangem.utils.toFormattedString + +// TODO [REDACTED_TASK_KEY] merge with SendTransactionUseCase +class SendMultipleTransactionUseCase( + private val demoConfig: DemoConfig, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val transactionRepository: TransactionRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + suspend operator fun invoke( + txsData: List, + userWallet: UserWallet, + network: Network, + ): Either> { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner(cardId = card.cardId.takeIf { isCardNotBackedUp }) + + val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() + if (userWallet.scanResponse.card.isStart2Coin) { + cardSdkConfigRepository.setLinkedTerminal(false) + } + val sendResult = try { + if (demoConfig.isDemoCardId(cardId = userWallet.cardId)) { + sendDemo( + userWallet = userWallet, + network = network, + transactionsData = txsData, + signer = signer, + ) + } else { + val sendResult = transactionRepository.sendMultipleTransactions( + txsData = txsData, + signer = signer, + userWalletId = userWallet.walletId, + network = network, + ) + when (sendResult) { + is Result.Failure -> handleError(sendResult).left() + is Result.Success -> sendResult.data.right() + } + } + } catch (ex: Exception) { + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + SendTransactionError.DataError(ex.message).left() + } + + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + return sendResult.fold( + ifRight = { result -> result.hashes.right() }, + ifLeft = { it.left() }, + ) + } + + private suspend fun sendDemo( + userWallet: UserWallet, + network: Network, + transactionsData: List, + signer: TransactionSigner, + ): Either { + val demoTransactionSender = DemoTransactionSender( + walletManagersFacade + .getOrCreateWalletManager(userWallet.walletId, network) + ?: error("WalletManager is null"), + ) + + val result = demoTransactionSender.sendMultiple(transactionDataList = transactionsData, signer = signer) + + return if (result is Result.Failure && result.error.customMessage.contains(DemoTransactionSender.ID)) { + SendTransactionError.DemoCardError.left() + } else { + TransactionsSendResult(listOf("hash")).right() + } + } + + private fun handleError(result: Result.Failure): SendTransactionError { + if (ResultChecker.isNetworkError(result)) { + return SendTransactionError.NetworkError( + code = result.error.message, + message = result.error.customMessage, + ) + } + val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() + return when (error) { + is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) + is BlockchainSdkError.CreateAccountUnderfunded -> { + val minAmount = error.minReserve + val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty() + SendTransactionError.CreateAccountUnderfunded(minValue) + } + else -> { + SendTransactionError.BlockchainSdkError( + code = error.code, + message = error.customMessage, + ) + } + } + } + + private fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { + return if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + when (val tangemError = error.tangemError) { + is TangemSdkError -> { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) + } + is BlockchainSdkError.WrappedTangemError -> { + parseWrappedError(tangemError) // todo remove when sdk errors are revised + } + else -> { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index ca470397b6..6a214fde9a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -1,10 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState @@ -17,31 +15,23 @@ import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - private val stakingGasEstimate: StakingGasEstimate, + private val fee: Fee, private val pendingActionList: ImmutableList, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate), + confirmationState = prevState.confirmationState.copyWrapped(fee), ) } - private fun StakingStates.ConfirmationState.copyWrapped( - gasEstimate: StakingGasEstimate, - ): StakingStates.ConfirmationState { + private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( - fee = Fee.Common( - Amount( - currencySymbol = gasEstimate.token.symbol, - value = gasEstimate.amount, - decimals = gasEstimate.token.decimals, - ), - ), + fee = fee, rate = feeCryptoCurrencyStatus?.value?.fiatRate, isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 6b11ad41ab..27e6593bd0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,9 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -15,32 +13,24 @@ import com.tangem.utils.transformer.Transformer internal class SetConfirmationStateCompletedTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - private val stakingGasEstimate: StakingGasEstimate, + private val fee: Fee, private val txUrl: String, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate), + confirmationState = prevState.confirmationState.copyWrapped(fee), ) } - private fun StakingStates.ConfirmationState.copyWrapped( - gasEstimate: StakingGasEstimate, - ): StakingStates.ConfirmationState { + private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( isPrimaryButtonEnabled = true, innerState = InnerConfirmationStakingState.COMPLETED, feeState = FeeState.Content( - fee = Fee.Common( - Amount( - currencySymbol = gasEstimate.token.symbol, - value = gasEstimate.amount, - decimals = gasEstimate.token.decimals, - ), - ), + fee = fee, rate = feeCryptoCurrencyStatus?.value?.fiatRate, isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 42a33c0bfd..8dea58c5cd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -6,7 +6,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle @@ -28,7 +30,6 @@ import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams -import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.FetchPendingTransactionsUseCase @@ -37,10 +38,7 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase -import com.tangem.domain.transaction.usecase.GetAllowanceUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase @@ -70,6 +68,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber import java.math.BigDecimal +import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import kotlin.properties.Delegates @@ -87,6 +86,7 @@ internal class StakingViewModel @Inject constructor( private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, private val estimateGasUseCase: EstimateGasUseCase, private val sendTransactionUseCase: SendTransactionUseCase, + private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, private val submitHashUseCase: SubmitHashUseCase, @@ -143,7 +143,7 @@ internal class StakingViewModel @Inject constructor( private var stakingApproval: StakingApproval = StakingApproval.Empty private val allowanceTaskScheduler = SingleTaskScheduler() - private var transactionInProgress: StakingTransaction? = null + private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() private var approvalJobHolder: JobHolder = JobHolder() @@ -187,7 +187,7 @@ internal class StakingViewModel @Inject constructor( val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address") - val stakingTransaction = getStakingTransactionUseCase( + val stakingTransactions = getStakingTransactionUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, params = ActionParams( @@ -206,9 +206,9 @@ internal class StakingViewModel @Inject constructor( return@launch } - stakingTransaction + val fullTransactionsData = stakingTransactions .filterNot { it.type == StakingTransactionType.APPROVAL } - .forEach { transaction -> + .map { transaction -> val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase( networkId = cryptoCurrencyStatus.currency.network.id.value, fee = fee, @@ -218,17 +218,20 @@ internal class StakingViewModel @Inject constructor( stakingEventFactory.createStakingErrorAlert(it) return@launch } - val gasEstimate = constructedTransaction.gasEstimate - ?: return@launch stakingEventFactory.createGenericErrorAlert("Gas estimate is null") - transactionInProgress = constructedTransaction - sendStakingTransaction( - transactionId = constructedTransaction.id, - gasEstimate = gasEstimate, - txData = transactionData, - pendingActionList = confirmationState.pendingActions, + FullTransactionData( + stakeKitTransaction = constructedTransaction, + tangemTransaction = transactionData, ) } + + transactionsInProgress.addAll(fullTransactionsData.map { it.stakeKitTransaction }) + + sendStakingTransaction( + fullTransactionsData = fullTransactionsData, + fee = fee, + pendingActionList = confirmationState.pendingActions, + ) } } } @@ -291,7 +294,7 @@ internal class StakingViewModel @Inject constructor( validatorAddress: String, ) { val pendingAction = pendingActions.firstOrNull() - val stakingGasEstimate = estimateGasUseCase( + val gasEstimate = estimateGasUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, params = ActionParams( @@ -313,7 +316,13 @@ internal class StakingViewModel @Inject constructor( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - stakingGasEstimate = stakingGasEstimate, + fee = Fee.Common( + Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ), + ), pendingActionList = pendingActions, ), ) @@ -555,8 +564,8 @@ internal class StakingViewModel @Inject constructor( val email = FeedbackEmailType.StakingProblem( cardInfo = cardInfo, validatorName = validator?.name, - transactionType = transactionInProgress?.type?.name, - unsignedTransaction = transactionInProgress?.unsignedTransaction, + transactionTypes = transactionsInProgress.map { it.type.name }, + unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction }, ) feedbackManager.sendEmail(email) @@ -632,13 +641,12 @@ internal class StakingViewModel @Inject constructor( } private suspend fun sendStakingTransaction( - transactionId: String, - gasEstimate: StakingGasEstimate, - txData: TransactionData, + fullTransactionsData: List, + fee: Fee, pendingActionList: ImmutableList, ) { - sendTransactionUseCase( - txData = txData, + sendMultipleTransactionUseCase( + txsData = fullTransactionsData.map { it.tangemTransaction }, userWallet = userWallet, network = cryptoCurrencyStatus.currency.network, ).fold( @@ -648,18 +656,21 @@ internal class StakingViewModel @Inject constructor( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - stakingGasEstimate = gasEstimate, + fee = fee, pendingActionList = pendingActionList, ), ) stakingEventFactory.createSendTransactionErrorAlert(error) }, - ifRight = { txHash -> - transactionInProgress = null - submitHash(transactionId, txHash) + ifRight = { transactionHashes -> + transactionsInProgress.clear() + submitHash( + transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, + transactionHashes = transactionHashes, + ) scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( - txHash = txHash, + txHash = transactionHashes.last(), networkId = cryptoCurrencyStatus.currency.network.id, ).getOrElse { "" } @@ -667,7 +678,7 @@ internal class StakingViewModel @Inject constructor( SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - stakingGasEstimate = gasEstimate, + fee = fee, txUrl = txUrl, ), ) @@ -675,18 +686,22 @@ internal class StakingViewModel @Inject constructor( ) } - private suspend fun submitHash(transactionId: String, transactionHash: String) { - submitHashUseCase.submitHash( - transactionId = transactionId, - transactionHash = transactionHash, - ) - .onLeft { - saveUnsubmittedHashUseCase.invoke( + private suspend fun submitHash(transactionIds: List, transactionHashes: List) { + transactionIds + .zip(transactionHashes) + .forEach { (transactionId, transactionHash) -> + submitHashUseCase.submitHash( transactionId = transactionId, transactionHash = transactionHash, ) - }.onRight { - Timber.d("Successful hash submission") + .onLeft { + saveUnsubmittedHashUseCase.invoke( + transactionId = transactionId, + transactionHash = transactionHash, + ) + }.onRight { + Timber.d("Successful hash submission") + } } } @@ -750,6 +765,11 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } + private data class FullTransactionData( + val stakeKitTransaction: StakingTransaction, + val tangemTransaction: TransactionData.Compiled, + ) + private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 45137306fa..fc08cd3ad3 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-748" +tangemBlockchainSdk = "develop-750" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 9b23c9a7a2473b10e49f29cf5bee6819f3ed1856 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 15:32:52 +0500 Subject: [PATCH 62/90] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 15 ++ .../card/DefaultDerivationsRepository.kt | 7 +- .../domain/card/MissedDerivationsFinder.kt | 37 ++-- .../data/common/currency/TokensOperations.kt | 7 +- data/manage-tokens/build.gradle.kts | 2 + .../DefaultManageTokensRepository.kt | 204 +++++++++++++++++- .../managetokens/di/ManageTokensDataModule.kt | 12 +- .../utils/ManageTokensUpdateFetcher.kt | 4 +- .../utils/ManagedCryptoCurrencyFactory.kt | 26 ++- .../repository/DefaultCurrenciesRepository.kt | 2 +- .../utils/UserTokensBackwardCompatibility.kt | 2 +- .../card/repository/DerivationsRepository.kt | 4 + .../DefaultWalletManagersFacade.kt | 38 +++- .../walletmanager/WalletManagersFacade.kt | 3 + .../domain/walletmanager/model/TokenInfo.kt | 12 ++ domain/manage-tokens/build.gradle.kts | 9 +- .../model/ManagedCryptoCurrency.kt | 5 +- .../managetokens/SaveManagedTokensUseCase.kt | 33 +++ .../model/ManageTokensUpdateAction.kt | 2 +- .../repository/ManageTokensRepository.kt | 9 + .../PreviewCustomTokenSelectorComponent.kt | 12 +- .../preview/PreviewManageTokensComponent.kt | 13 +- .../entity/item/CurrencyNetworkUM.kt | 4 +- .../entity/managetokens/ManageTokensUM.kt | 3 + .../managetokens/model/ManageTokensModel.kt | 39 +++- .../managetokens/ui/ManageTokensScreen.kt | 9 +- .../utils/list/ChangedCurrenciesManager.kt | 36 ++-- .../utils/list/ManageTokensListManager.kt | 26 +-- .../utils/list/ManageTokensUiActions.kt | 6 +- .../utils/list/ManageTokensUiManager.kt | 8 +- .../utils/mapper/CurrencyNetworksMapper.kt | 4 +- .../utils/ui/CurrencyItemOperations.kt | 4 +- .../utils/ui/CurrencyNetworkOperations.kt | 2 +- 33 files changed, 484 insertions(+), 115 deletions(-) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 0dd7b11804..1513f4d484 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,5 +1,8 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.managetokens.SaveManagedTokensUseCase +import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -44,4 +47,16 @@ internal object ManageTokensDomainModule { ): CheckIsCurrencyNotAddedUseCase { return CheckIsCurrencyNotAddedUseCase(customTokensRepository) } + + @Provides + @Singleton + fun provideSaveManagedTokensUseCase( + manageTokensRepository: ManageTokensRepository, + derivationsRepository: DerivationsRepository, + ): SaveManagedTokensUseCase { + return SaveManagedTokensUseCase( + manageTokensRepository = manageTokensRepository, + derivationsRepository = derivationsRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 5f4b695932..a5d6f883b7 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -12,6 +12,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -30,6 +31,10 @@ internal class DefaultDerivationsRepository( ) : DerivationsRepository { override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { + derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) + } + + override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { @@ -38,7 +43,7 @@ internal class DefaultDerivationsRepository( } val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse) - .find(currencies) + .findByNetworks(networks) .ifEmpty { Timber.d("Nothing to derive") return diff --git a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt index 97c0a7bf8f..af980fd382 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt @@ -11,6 +11,7 @@ import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.operations.derivation.ExtendedPublicKeysMap private typealias DerivationData = Pair> @@ -26,8 +27,12 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { + return currencies.map { it.network }.let(::findByNetworks) + } + + fun findByNetworks(networks: List): Derivations { return buildMap> { - currencies + networks .mapToNewDerivations() .forEach { data -> val current = this[data.first] @@ -41,25 +46,25 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } } - private fun List.mapToNewDerivations(): List { + private fun List.mapToNewDerivations(): List { val config = CardConfig.createConfig(scanResponse.card) - return mapNotNull { currency -> - val blockchain = Blockchain.fromId(id = currency.network.id.value) + return mapNotNull { network -> + val blockchain = Blockchain.fromId(id = network.id.value) val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null - findNewDerivations(curve = curve, scanResponse = scanResponse, currency = currency) + findNewDerivations(curve = curve, scanResponse = scanResponse, network = network) } } private fun findNewDerivations( curve: EllipticCurve, scanResponse: ScanResponse, - currency: CryptoCurrency, + network: Network, ): DerivationData? { val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null val publicKey = wallet.publicKey.toMapKey() - val derivationCandidates = currency + val derivationCandidates = network .getDerivationCandidates(curve) .ifEmpty { return null } .filterAlreadyDerivedKeys(publicKey) @@ -68,13 +73,13 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { return publicKey to derivationCandidates } - private fun CryptoCurrency.getDerivationCandidates(curve: EllipticCurve): List { - val blockchain = Blockchain.fromId(id = network.id.value) + private fun Network.getDerivationCandidates(curve: EllipticCurve): List { + val blockchain = Blockchain.fromId(id = this.id.value) return buildList { add(blockchain.getDerivationPath(curve = curve)) - add(blockchain.getCustomDerivationPath(curve = curve, currency = this@getDerivationCandidates)) - add(blockchain.getCardanoDerivationPathIfNeeded(currency = this@getDerivationCandidates)) + add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates)) + add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates)) } .filterNotNull() .distinct() @@ -88,17 +93,17 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } } - private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, currency: CryptoCurrency): DerivationPath? { + private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? { return if (getSupportedCurves().contains(curve)) { - currency.network.derivationPath.value?.let(::DerivationPath) + network.derivationPath.value?.let(::DerivationPath) } else { null } } - private fun Blockchain.getCardanoDerivationPathIfNeeded(currency: CryptoCurrency): DerivationPath? { - return if (currency is CryptoCurrency.Coin && this == Blockchain.Cardano) { - currency.network.derivationPath.value?.let { + private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? { + return if (this == Blockchain.Cardano) { + network.derivationPath.value?.let { CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it)) } } else { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt index 2c2c8d95ad..724649c959 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt @@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency.ID import com.tangem.domain.tokens.model.Network import com.tangem.blockchain.common.Token as SdkToken @@ -65,10 +64,10 @@ fun getCoinIconUrl(blockchain: Blockchain): String? { return coinId?.let(::getTokenIconUrlFromDefaultHost) } -fun List.hasCoinForToken(token: CryptoCurrency.Token): Boolean { +fun List.hasCoinForToken(network: Network): Boolean { return any { - val blockchain = getBlockchain(networkId = token.network.id) - val tokenDerivation = token.network.derivationPath.value + val blockchain = getBlockchain(networkId = network.id) + val tokenDerivation = network.derivationPath.value it.id == blockchain.toCoinId() && it.derivationPath == tokenDerivation } } diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index 137a893c81..a6acc9a452 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -16,10 +16,12 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.legacy) /** Project - Data */ implementation(projects.core.datasource) implementation(projects.data.common) + implementation(projects.data.tokens) /** Project - Utils */ implementation(projects.core.utils) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index bb45239024..caea014d2c 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -3,15 +3,27 @@ package com.tangem.data.managetokens import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.getBlockchain +import com.tangem.data.common.currency.hasCoinForToken import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory +import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.TangemExpressValues +import com.tangem.datasource.api.express.models.request.AssetsRequestBody +import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver @@ -20,7 +32,11 @@ import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.pagination.BatchFetchResult @@ -28,16 +44,27 @@ import com.tangem.pagination.BatchListSource import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +@Suppress("LongParameterList") internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, - private val managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, + private val walletManagerFacade: WalletManagersFacade, + private val tangemExpressApi: TangemExpressApi, + private val expressAssetsStore: ExpressAssetsStore, ) : ManageTokensRepository { + private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory() + private val cryptoCurrencyFactory = CryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() + + // region getTokenListBatchFlow override fun getTokenListBatchFlow( context: ManageTokensListBatchingContext, batchSize: Int, @@ -58,7 +85,7 @@ internal class DefaultManageTokensRepository( prefetchDistance = batchSize, batchSize = batchSize, subFetcher = { request, _, isFirstBatchFetching -> - val userWallet = getUserWallet(request.params.userWalletId) + val userWallet = request.params.userWalletId?.let { getUserWallet(it) } val supportedBlockchains = getSupportedBlockchains(userWallet) val searchText = request.params.searchText?.takeIf { it.isNotBlank() } @@ -118,13 +145,9 @@ internal class DefaultManageTokensRepository( } } - private suspend fun getUserWallet(userWalletId: UserWalletId?): UserWallet? { - if (userWalletId == null) { - return null - } - + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet not found" + "Unable to find a user wallet with provided ID: $userWalletId" } } @@ -135,4 +158,169 @@ internal class DefaultManageTokensRepository( !it.isTestnet() && it.isSupportedInApp() } } + // endregion + + // region addManagedCurrencies + override suspend fun saveManagedCurrencies( + userWalletId: UserWalletId, + currenciesToAdd: Map>, + currenciesToRemove: Map>, + ) { + return withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, + ) + + val filteredTokens = currenciesToAdd.mapToTokenDataList() + .filterNot { tokenData -> + val blockchain = getBlockchain(networkId = tokenData.sourceNetwork.network.id) + val networkId = blockchain.toNetworkId() + val contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress + savedCurrencies.tokens.firstOrNull { token -> + token.contractAddress == contractAddress && + token.networkId == networkId && + token.derivationPath == tokenData.sourceNetwork.network.derivationPath.value + } != null + } + val newCoins = createCoinsForNewTokens( + userWalletId = userWalletId, + newTokensNetworks = filteredTokens.mapNotNull { tokenData -> + (tokenData.sourceNetwork as? SourceNetwork.Default)?.network + }, + savedCurrencies = savedCurrencies.tokens, + ) + val tokensToSave = filteredTokens.mapToUserResponseToken() + + val tokensToRemove = currenciesToRemove.mapToTokenDataList() + removeCurrenciesFromWalletManager(userWalletId = userWalletId, tokensDataList = tokensToRemove) + + val newCurrencies = (newCoins + tokensToSave).distinct() + val updatedResponse = savedCurrencies.copy( + tokens = savedCurrencies.tokens + newCurrencies - tokensToRemove.mapToUserResponseToken().toSet(), + ) + storeAndPushTokens(userWalletId = userWalletId, response = updatedResponse) + fetchExchangeableUserMarketCoinsByIds(userWalletId = userWalletId, userTokens = updatedResponse) + } + } + + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) + + pushTokens(userWalletId, response) + } + + private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) { + Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}") + } + } + + private suspend fun fetchExchangeableUserMarketCoinsByIds( + userWalletId: UserWalletId, + userTokens: UserTokensResponse, + ) { + try { + val tokensList = userTokens.tokens + .map { + LeastTokenInfo( + contractAddress = it.contractAddress ?: TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE, + network = it.networkId, + ) + } + + if (tokensList.isNotEmpty()) { + val response = tangemExpressApi.getAssets( + AssetsRequestBody( + tokensList = tokensList, + ), + ) + + expressAssetsStore.store(userWalletId, response.getOrThrow()) + } + } catch (e: Throwable) { + Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") + } + } + + private suspend fun removeCurrenciesFromWalletManager( + userWalletId: UserWalletId, + tokensDataList: List, + ) { + walletManagerFacade.removeTokensByTokenInfo( + userWalletId = userWalletId, + tokenInfos = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData -> + val contractAddress = + (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress ?: return@mapNotNullTo null + TokenInfo( + network = tokenData.sourceNetwork.network, + name = tokenData.token.name, + symbol = tokenData.token.symbol, + contractAddress = contractAddress, + decimals = tokenData.sourceNetwork.decimals, + id = tokenData.token.id.value, + ) + }, + ) + walletManagerFacade.remove( + userWalletId = userWalletId, + networks = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData -> + (tokenData.sourceNetwork as? SourceNetwork.Main)?.network + }, + ) + } + + private suspend fun createCoinsForNewTokens( + userWalletId: UserWalletId, + newTokensNetworks: List, + savedCurrencies: List, + ): List { + return newTokensNetworks + .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins + .mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = getBlockchain(networkId = it.id), + extraDerivationPath = it.derivationPath.value, + derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, + ) + } + .distinct() + .map(userTokensResponseFactory::createResponseToken) + } + + private fun List.mapToUserResponseToken(): List { + return map { tokenData -> + val blockchain = getBlockchain(tokenData.sourceNetwork.network.id) + UserTokensResponse.Token( + id = tokenData.token.id.value, + networkId = blockchain.toNetworkId(), + derivationPath = tokenData.sourceNetwork.network.derivationPath.value, + name = tokenData.token.name, + symbol = tokenData.token.symbol, + decimals = tokenData.sourceNetwork.decimals, + contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress, + ) + } + } + + private fun Map>.mapToTokenDataList(): List { + return flatMap { (token, networks) -> + token.availableNetworks + .filter { sourceNetwork -> networks.contains(sourceNetwork.network) } + .map { sourceNetwork -> TokenWithSourceNetwork(token = token, sourceNetwork = sourceNetwork) } + } + } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } + // endregion + + data class TokenWithSourceNetwork(val token: ManagedCryptoCurrency.Token, val sourceNetwork: SourceNetwork) } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 64d77526c1..37d4a334fb 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -3,12 +3,14 @@ package com.tangem.data.managetokens.di import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher -import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory +import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,18 +27,22 @@ internal object ManageTokensDataModule { fun provideManageTokensRepository( tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, - managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, + walletManagersFacade: WalletManagersFacade, + tangemExpressApi: TangemExpressApi, + expressAssetsStore: ExpressAssetsStore, ): ManageTokensRepository { return DefaultManageTokensRepository( tangemTechApi, userWalletsStore, - managedCryptoCurrencyFactory, manageTokensUpdateFetcher, appPreferencesStore, dispatchers, + walletManagersFacade, + tangemExpressApi, + expressAssetsStore, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt index 6c23f37b0b..66bfc00bc5 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt @@ -31,9 +31,9 @@ internal class ManageTokensUpdateFetcher @Inject constructor() : is ManagedCryptoCurrency.Token -> { currency.copy( addedIn = if (updateRequest.isSelected) { - currency.addedIn + updateRequest.networkId + currency.addedIn + updateRequest.network } else { - currency.addedIn - updateRequest.networkId + currency.addedIn - updateRequest.network }, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index de239a1f09..c848b36699 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -13,11 +13,8 @@ import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.tokens.model.Network -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -internal class ManagedCryptoCurrencyFactory @Inject constructor() { +internal class ManagedCryptoCurrencyFactory { fun create( coinsResponse: CoinsResponse, @@ -100,7 +97,7 @@ internal class ManagedCryptoCurrencyFactory @Inject constructor() { availableNetworks = coinResponse.networks.mapNotNull { network -> createSource(network, derivationStyleProvider) }, - addedIn = findAddedInNetworksIds(coinResponse.id, tokensResponse), + addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, derivationStyleProvider), ) } @@ -119,26 +116,35 @@ internal class ManagedCryptoCurrencyFactory @Inject constructor() { return if (contractAddress.isNullOrBlank()) { SourceNetwork.Main( network = network, + decimals = blockchain.decimals(), ) } else { SourceNetwork.Default( network = network, + decimals = requireNotNull(networkResponse.decimalCount?.toInt()), contractAddress = contractAddress, ) } } - private fun findAddedInNetworksIds(currencyId: String, tokensResponse: UserTokensResponse?): Set { + private fun findAddedInNetworks( + currencyId: String, + tokensResponse: UserTokensResponse?, + derivationStyleProvider: DerivationStyleProvider?, + ): Set { if (tokensResponse == null) return emptySet() return tokensResponse.tokens .filter { it.id == currencyId } - .map { it.networkId } - .mapNotNullTo(mutableSetOf()) { networkId -> - val blockchain = Blockchain.fromNetworkId(networkId) + .mapNotNullTo(mutableSetOf()) { token -> + val blockchain = Blockchain.fromNetworkId(token.networkId) if (blockchain != null && blockchain.isSupportedInApp()) { - Network.ID(blockchain.id) + getNetwork( + blockchain = blockchain, + extraDerivationPath = token.derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) } else { null } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 83d87b583b..93ad36b66f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -132,7 +132,7 @@ internal class DefaultCurrenciesRepository( savedCurrencies: List, ): List { return newTokens - .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins + .filterNot { savedCurrencies.hasCoinForToken(it.network) } // tokens without coins .mapNotNull { cryptoCurrencyFactory.createCoin( blockchain = getBlockchain(networkId = it.network.id), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt index ec40d6e52c..a4b3f4116d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt @@ -9,7 +9,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse * Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens * in new application with new IDs */ -internal class UserTokensBackwardCompatibility { +class UserTokensBackwardCompatibility { fun applyCompatibilityAndGetUpdated(userTokensResponse: UserTokensResponse): UserTokensResponse { return userTokensResponse.copy( diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt index d6b474839a..5c6512a557 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt @@ -3,6 +3,7 @@ package com.tangem.domain.card.repository import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -11,6 +12,9 @@ interface DerivationsRepository { @Throws suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) + @Throws + suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) + @Throws suspend fun derivePublicKeys( userWalletId: UserWalletId, 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 7b187af646..6d6c0b3a4b 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 @@ -31,6 +31,7 @@ import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.SmartContractMethod +import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.walletmanager.utils.* import com.tangem.domain.walletmanager.utils.WalletManagerFactory @@ -103,24 +104,45 @@ class DefaultWalletManagersFacade( tokens .groupBy(CryptoCurrency.Token::network) .forEach { (network, networkTokens) -> - removeTokens(userWalletId, network, networkTokens) + removeTokens( + userWalletId = userWalletId, + network = network, + networkTokens = sdkTokenConverter.convertList(networkTokens), + ) } } - private suspend fun removeTokens( - userWalletId: UserWalletId, - network: Network, - networkTokens: List, - ) { + override suspend fun removeTokensByTokenInfo(userWalletId: UserWalletId, tokenInfos: Set) { + if (tokenInfos.isEmpty()) return + + tokenInfos + .groupBy { it.network } + .forEach { (network, tokenInfoList) -> + removeTokens( + userWalletId = userWalletId, + network = network, + networkTokens = tokenInfoList.map { + Token( + name = it.name, + symbol = it.symbol, + contractAddress = it.contractAddress, + decimals = it.decimals, + id = it.id, + ) + }, + ) + } + } + + private suspend fun removeTokens(userWalletId: UserWalletId, network: Network, networkTokens: List) { withContext(dispatchers.io) { val walletManager = walletManagersStore.getSyncOrNull( userWalletId = userWalletId, blockchain = Blockchain.fromId(network.id.value), derivationPath = network.derivationPath.value, ) ?: return@withContext - val tokensToRemove = sdkTokenConverter.convertList(networkTokens) - tokensToRemove.forEach { token -> + networkTokens.forEach { token -> walletManager.removeToken(token) } 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 e55a359854..5104db3554 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 @@ -17,6 +17,7 @@ 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 +import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -54,6 +55,8 @@ interface WalletManagersFacade { suspend fun removeTokens(userWalletId: UserWalletId, tokens: Set) + suspend fun removeTokensByTokenInfo(userWalletId: UserWalletId, tokenInfos: Set) + /** * Returns [UpdateWalletManagerResult] with last pending transactions * diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt new file mode 100644 index 0000000000..9d1952b890 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.walletmanager.model + +import com.tangem.domain.tokens.model.Network + +data class TokenInfo( + val network: Network, + val name: String, + val symbol: String, + val contractAddress: String, + val decimals: Int, + val id: String? = null, +) \ No newline at end of file diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index b24f993032..51a0e5f6ab 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -1,8 +1,13 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.managetokens" +} + dependencies { /* Domain */ @@ -10,6 +15,8 @@ dependencies { api(projects.domain.core) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.card) + implementation(projects.domain.legacy) /* Core */ api(projects.core.pagination) diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index f7f82002cf..595a8e1077 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -42,7 +42,7 @@ sealed class ManagedCryptoCurrency { override val symbol: String, override val iconUrl: String, val availableNetworks: List, - val addedIn: Set, + val addedIn: Set, ) : ManagedCryptoCurrency() { val isAdded: Boolean = addedIn.isNotEmpty() @@ -54,6 +54,7 @@ sealed class ManagedCryptoCurrency { sealed class SourceNetwork { abstract val network: Network + abstract val decimals: Int val id: Network.ID get() = network.id @@ -73,10 +74,12 @@ sealed class ManagedCryptoCurrency { data class Main( override val network: Network, + override val decimals: Int, ) : SourceNetwork() data class Default( override val network: Network, + override val decimals: Int, val contractAddress: String, ) : SourceNetwork() diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt new file mode 100644 index 0000000000..5a6ad0a7ce --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.flatten +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class SaveManagedTokensUseCase( + private val manageTokensRepository: ManageTokensRepository, + private val derivationsRepository: DerivationsRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currenciesToAdd: Map>, + currenciesToRemove: Map>, + ): Either { + return Either.catch { + derivationsRepository.derivePublicKeysByNetworks( + userWalletId = userWalletId, + networks = currenciesToAdd.values.flatten(), + ) + manageTokensRepository.saveManagedCurrencies( + userWalletId = userWalletId, + currenciesToAdd = currenciesToAdd, + currenciesToRemove = currenciesToRemove, + ) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt index 75b1b2e4ad..0d3af7a007 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt @@ -6,7 +6,7 @@ sealed class ManageTokensUpdateAction { data class AddCurrency( val currencyId: ManagedCryptoCurrency.ID, - val networkId: Network.ID, + val network: Network, val isSelected: Boolean, ) : ManageTokensUpdateAction() } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt index 3104d98d64..fab91ff3bb 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt @@ -2,8 +2,17 @@ package com.tangem.domain.managetokens.repository import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId interface ManageTokensRepository { fun getTokenListBatchFlow(context: ManageTokensListBatchingContext, batchSize: Int): ManageTokensListBatchFlow + + suspend fun saveManagedCurrencies( + userWalletId: UserWalletId, + currenciesToAdd: Map>, + currenciesToRemove: Map>, + ) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 7d596d3f8d..1ebcff8b5d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -51,7 +51,17 @@ internal class PreviewCustomTokenSelectorComponent( ) CurrencyNetworkUM( - networkId = n.id, + network = Network( + id = n.id, + backendId = n.id.value, + name = "", + currencySymbol = "", + derivationPath = Network.DerivationPath.Card(""), + isTestnet = false, + standardType = Network.StandardType.ERC20, + hasFiatFeeRate = false, + canHandleTokens = false, + ), name = "Network $index", type = "N$index", iconResId = R.drawable.ic_eth_16, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index efa89d5b8e..76be782c89 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -54,6 +54,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { isNextBatchLoading = true, loadMore = { false }, saveChanges = {}, + isSavingInProgress = false, ), ) @@ -134,7 +135,17 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> CurrencyNetworkUM( - networkId = Network.ID(networkIndex.toString()), + network = Network( + id = Network.ID(networkIndex.toString()), + backendId = networkIndex.toString(), + name = "", + currencySymbol = "", + derivationPath = Network.DerivationPath.Card(""), + isTestnet = false, + standardType = Network.StandardType.ERC20, + hasFiatFeeRate = false, + canHandleTokens = false, + ), name = "NETWORK$networkIndex", type = "N$networkIndex", iconResId = R.drawable.ic_eth_16, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt index 84f3aabb87..b666a09c2a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.managetokens.entity.item import com.tangem.domain.tokens.model.Network internal data class CurrencyNetworkUM( - val networkId: Network.ID, + val network: Network, val name: String, val type: String, val iconResId: Int, @@ -12,5 +12,5 @@ internal data class CurrencyNetworkUM( override val onSelectedStateChange: (Boolean) -> Unit, ) : SelectableItemUM { - override val id: String = networkId.value + override val id: String = network.id.value } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index 4fc9d627c7..33f1524452 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -36,6 +36,7 @@ internal sealed class ManageTokensUM { override val loadMore: () -> Boolean, val saveChanges: () -> Unit, val hasChanges: Boolean, + val isSavingInProgress: Boolean, ) : ManageTokensUM() fun copySealed( @@ -44,6 +45,7 @@ internal sealed class ManageTokensUM { hasChanges: Boolean = this is ManageContent && this.hasChanges, isInitialBatchLoading: Boolean = this.isInitialBatchLoading, isNextBatchLoading: Boolean = this.isNextBatchLoading, + isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -52,6 +54,7 @@ internal sealed class ManageTokensUM { hasChanges = hasChanges, isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, + isSavingInProgress = isSavingInProgress, ) is ReadContent -> copy( search = search, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 941fdad919..87e6636653 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -21,12 +22,14 @@ import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.utils.list.ChangedCurrencies import com.tangem.features.managetokens.utils.list.ManageTokensListManager +import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ComponentScoped @@ -35,6 +38,7 @@ internal class ManageTokensModel @Inject constructor( private val router: Router, private val manageTokensListManager: ManageTokensListManager, private val messageSender: UiMessageSender, + private val saveManagedTokensUseCase: SaveManagedTokensUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -76,7 +80,7 @@ internal class ManageTokensModel @Inject constructor( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, - items = getInitialItems(), + items = persistentListOf(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_search_tokens), onBackButtonClick = router::pop, @@ -97,7 +101,7 @@ internal class ManageTokensModel @Inject constructor( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, - items = getInitialItems(), + items = persistentListOf(), topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, @@ -116,6 +120,7 @@ internal class ManageTokensModel @Inject constructor( hasChanges = false, saveChanges = ::saveChanges, loadMore = ::loadMoreItems, + isSavingInProgress = false, ) } @@ -157,9 +162,16 @@ internal class ManageTokensModel @Inject constructor( isNextBatchLoading = false, ) } - is PaginationStatus.Paginating, - is PaginationStatus.EndOfPagination, - -> state.copySealed( + is PaginationStatus.Paginating -> { + (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> + Timber.e(fetchError.throwable) + } + state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + is PaginationStatus.EndOfPagination -> state.copySealed( isInitialBatchLoading = false, isNextBatchLoading = false, ) @@ -189,10 +201,6 @@ internal class ManageTokensModel @Inject constructor( return true } - private fun getInitialItems(): ImmutableList { - return persistentListOf() - } - private fun navigateToAddCustomToken() { params.userWalletId?.let { bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it)) @@ -200,7 +208,18 @@ internal class ManageTokensModel @Inject constructor( } private fun saveChanges() { - // TODO: [REDACTED_JIRA] + modelScope.launch { + state.update { state -> state.copySealed(isSavingInProgress = true) } + saveManagedTokensUseCase.invoke( + userWalletId = requireNotNull(params.userWalletId), + currenciesToAdd = manageTokensListManager.currenciesToAdd.value, + currenciesToRemove = manageTokensListManager.currenciesToRemove.value, + ).fold( + ifLeft = { Timber.e(it, "Failed to save changes") }, + ifRight = { router.pop() }, + ) + state.update { state -> state.copySealed(isSavingInProgress = false) } + } } private fun searchCurrencies(query: String) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 547963c3b0..c8771d1fa6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -102,6 +102,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), isVisible = state.hasChanges, + showProgress = state.isSavingInProgress, onClick = state.saveChanges, ) } @@ -133,7 +134,12 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM } @Composable -private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun SaveChangesButton( + isVisible: Boolean, + showProgress: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { AnimatedVisibility( modifier = modifier, visible = isVisible, @@ -144,6 +150,7 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: PrimaryButtonIconEnd( text = stringResource(id = R.string.common_save), iconResId = R.drawable.ic_tangem_24, + showProgress = showProgress, onClick = onClick, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt index 3e2705f9e6..32aa63bf2a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt @@ -5,54 +5,54 @@ import com.tangem.domain.tokens.model.Network import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -internal typealias ChangedCurrencies = Map> +internal typealias ChangedCurrencies = Map> internal class ChangedCurrenciesManager { val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) - fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + fun addCurrency(currency: ManagedCryptoCurrency.Token, network: Network) { + updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd) } - fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + fun removeCurrency(currency: ManagedCryptoCurrency.Token, network: Network) { + updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove) } - fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean { - return networkId in currenciesToAdd.value[currencyId].orEmpty() || - networkId in currenciesToRemove.value[currencyId].orEmpty() + fun containsCurrency(currency: ManagedCryptoCurrency.Token, network: Network): Boolean { + return network in currenciesToAdd.value[currency].orEmpty() || + network in currenciesToRemove.value[currency].orEmpty() } private fun updateChangedItems( - currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, + currency: ManagedCryptoCurrency.Token, + network: Network, removeFromIfPresent: MutableStateFlow, addToIfNotPresent: MutableStateFlow, ) { - val present = removeFromIfPresent.value[currencyId].orEmpty() + val present = removeFromIfPresent.value[currency].orEmpty() - if (networkId in present) { + if (network in present) { removeFromIfPresent.update { items -> items.toMutableMap().apply { - val ids = present - networkId + val ids = present - network if (ids.isEmpty()) { - remove(currencyId) + remove(currency) } else { - set(currencyId, ids) + set(currency, ids) } } } } else { addToIfNotPresent.update { items -> - val alreadyAdded = items[currencyId] ?: emptySet() - if (networkId in alreadyAdded) { + val alreadyAdded = items[currency] ?: emptySet() + if (network in alreadyAdded) { return@update items } - items + (currencyId to alreadyAdded + networkId) + items + (currency to alreadyAdded + network) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 62ab953402..92dc53736c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -59,8 +59,8 @@ internal class ManageTokensListManager @Inject constructor( scopeProvider = Provider { scope }, ) - val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd - val currenciesToRemove: StateFlow = changedCurrenciesManager.currenciesToRemove + val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd.asStateFlow() + val currenciesToRemove: StateFlow = changedCurrenciesManager.currenciesToRemove.asStateFlow() @OptIn(ExperimentalCoroutinesApi::class) val paginationStatus: Flow> = state @@ -148,32 +148,32 @@ internal class ManageTokensListManager @Inject constructor( } } - override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - changedCurrenciesManager.addCurrency(currencyId, networkId) + override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { + changedCurrenciesManager.addCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true) + sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true) } - override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - changedCurrenciesManager.removeCurrency(currencyId, networkId) + override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { + changedCurrenciesManager.removeCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false) + sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) } override fun checkNeedToShowRemoveNetworkWarning( - currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, - ): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId) + currency: ManagedCryptoCurrency.Token, + network: Network, + ): Boolean = !changedCurrenciesManager.containsCurrency(currency, network) private fun sendSelectCurrencyAction( batchKey: Int, currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, + network: Network, isSelected: Boolean, ) { val request = ManageTokensUpdateAction.AddCurrency( currencyId = currencyId, - networkId = networkId, + network = network, isSelected = isSelected, ) val action = BatchAction.UpdateBatches( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt index 9d73ea1e76..7b9ab64f3f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -6,11 +6,11 @@ import com.tangem.domain.wallets.models.UserWalletId internal interface ManageTokensUiActions { - fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) - fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) - fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean + fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 546761dcfb..3ca2075789 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -142,19 +142,19 @@ internal class ManageTokensUiManager( if (currency !is ManagedCryptoCurrency.Token) return@launch if (isSelected) { - actions.addCurrency(batchKey, currency.id, source.id) + actions.addCurrency(batchKey, currency, source.network) } else { - if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { + if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) { showRemoveNetworkWarning( currency = currency, network = source.network, isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, onConfirm = { - actions.removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency, source.network) }, ) } else { - actions.removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency, source.network) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt index 71d9885d89..6fdacc516a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -16,7 +16,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( NetworksUM.Expanded( networks = availableNetworks.map { it.toUiModel( - isSelected = it.id in addedIn, + isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, ) @@ -33,7 +33,7 @@ private fun SourceNetwork.toUiModel( onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, ): CurrencyNetworkUM { return CurrencyNetworkUM( - networkId = id, + network = network, name = network.name.uppercase(), iconResId = id.getIconRes(isColored = isSelected || !isEditable), isSelected = isSelected || !isEditable, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt index d8b0a83776..557b49d960 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt @@ -55,10 +55,10 @@ private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency. is NetworksUM.Collapsed -> networks is NetworksUM.Expanded -> networks.copy( networks = networks.networks.map { network -> - val isSelected = network.networkId in currency.addedIn + val isSelected = network.network in currency.addedIn network.copy( - iconResId = network.networkId.getIconRes(isSelected), + iconResId = network.network.id.getIconRes(isSelected), isSelected = isSelected, ) }.toImmutableList(), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt index 562f0db8a2..fbc93fce2c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt @@ -8,7 +8,7 @@ import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM { return copy( - iconResId = networkId.getIconRes(isSelected), + iconResId = network.id.getIconRes(isSelected), isSelected = isSelected, ) } From a67d482c3aa3e2292701957367fb457377bdc457 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 14:51:47 +0400 Subject: [PATCH 63/90] Updated on 2026-08-14 --- .../converter/UserWalletItemUMConverter.kt | 2 + .../tokens/GetWalletTotalBalanceUseCase.kt | 4 +- .../portfolio/impl/loader/PortfolioData.kt | 26 +++++ .../impl/loader/PortfolioDataLoader.kt | 105 ++++++++++++++++++ .../impl/ui/WalletSelectorBottomSheet.kt | 2 - .../ui/state/WalletSelectorBSContentUM.kt | 1 - 6 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index 704315fc82..163168be81 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -30,6 +30,7 @@ class UserWalletItemUMConverter( private val balance: TotalFiatBalance? = null, private val isLoading: Boolean = true, private val isBalanceHidden: Boolean = false, + private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None, ) : Converter { override fun convert(value: UserWallet): UserWalletItemUM { @@ -45,6 +46,7 @@ class UserWalletItemUMConverter( ), imageUrl = artworkUrl, isEnabled = !isLocked, + endIcon = endIcon, onClick = { onClick(value.walletId) }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 0f4d3477da..012dd2847a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -29,7 +29,7 @@ class GetWalletTotalBalanceUseCase( private val stakingRepository: StakingRepository, ) { - suspend operator fun invoke( + operator fun invoke( userTallestIds: Collection, ): LceFlow> { val flows = userTallestIds.distinct() @@ -54,7 +54,7 @@ class GetWalletTotalBalanceUseCase( } @OptIn(ExperimentalCoroutinesApi::class) - suspend operator fun invoke(userWalletId: UserWalletId): LceFlow { + operator fun invoke(userWalletId: UserWalletId): LceFlow { val currenciesStatuses = getStatuses(userWalletId) return currenciesStatuses.transformLatest { maybeStatuses -> diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt new file mode 100644 index 0000000000..8ac05a4e90 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt @@ -0,0 +1,26 @@ +package com.tangem.features.markets.portfolio.impl.loader + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Portfolio data. Combined data from all flows that required to setup portfolio + * + * @property walletsWithCurrencyStatuses wallets with crypto currency statuses + * @property appCurrency app currency + * @property isBalanceHidden flag that indicates if balance should be hidden + * @property walletsWithBalance wallets with total balance + * +[REDACTED_AUTHOR] + */ +internal data class PortfolioData( + val walletsWithCurrencyStatuses: Map>, + val appCurrency: AppCurrency, + val isBalanceHidden: Boolean, + val walletsWithBalance: Map>, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt new file mode 100644 index 0000000000..e2e08f4b70 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -0,0 +1,105 @@ +package com.tangem.features.markets.portfolio.impl.loader + +import arrow.core.getOrElse +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +/** + * Loader of portfolio data + * + * @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses + * @property getSelectedAppCurrencyUseCase use case for getting selected app currency + * @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings + * @property getWalletTotalBalanceUseCase use case for getting wallet total balance + * +[REDACTED_AUTHOR] + */ +internal class PortfolioDataLoader @Inject constructor( + private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, +) { + + /** Load data by [currencyRawId] */ + @OptIn(ExperimentalCoroutinesApi::class) + fun load(currencyRawId: String): Flow { + return combine( + flow = getAllWalletsCryptoCurrencyStatusesFlow(currencyRawId = currencyRawId), + flow2 = getSelectedAppCurrencyFlow(), + flow3 = getBalanceHidingSettingsFlow(), + ) { walletsWithCurrencyStatuses, appCurrency, isBalanceHidden -> + PortfolioData( + walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden, + walletsWithBalance = emptyMap(), + ) + } + // setup balances for wallets from walletsWithCurrencyStatuses + .flatMapLatest { portfolioData -> + getWalletsWithTotalBalanceFlow( + ids = portfolioData.walletsWithCurrencyStatuses.keys.map(UserWallet::walletId), + ) + .map { portfolioData.copy(walletsWithBalance = it) } + } + } + + private fun getAllWalletsCryptoCurrencyStatusesFlow( + currencyRawId: String, + ): Flow>> { + return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) + .distinctUntilChanged() + .map { walletsWithMaybeStatuses -> + walletsWithMaybeStatuses.mapValues { entry -> + entry.value.mapNotNull { it.getOrNull() } + } + } + } + + private fun getSelectedAppCurrencyFlow(): Flow { + return getSelectedAppCurrencyUseCase() + .map { + it.getOrElse { e -> + Timber.e("Failed to load app currency: $e") + AppCurrency.Default + } + } + .distinctUntilChanged() + } + + private fun getBalanceHidingSettingsFlow(): Flow { + return getBalanceHidingSettingsUseCase() + .map { it.isBalanceHidden } + .distinctUntilChanged() + } + + private fun getWalletsWithTotalBalanceFlow( + ids: List, + ): Flow>> { + return combine( + flows = ids + .map { userWalletId -> + getWalletTotalBalanceUseCase(userWalletId) + .map { userWalletId to it } + .distinctUntilChanged() + }, + transform = { it.toMap() }, + ) + .distinctUntilChanged() + .onEmpty { ids.associateWith { Lce.Loading(partialContent = null) } } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt index a9e190928f..22ba89a8f7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt @@ -109,7 +109,6 @@ private fun Preview() { ), ), onBack = {}, - onWalletSelected = {}, ), ), ) @@ -135,7 +134,6 @@ private fun PreviewContent() { ), ), onBack = {}, - onWalletSelected = {}, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt index ee727b3608..fddc12c25e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt @@ -6,6 +6,5 @@ import kotlinx.collections.immutable.ImmutableList internal data class WalletSelectorBSContentUM( val userWallets: ImmutableList, - val onWalletSelected: (UserWalletItemUM) -> Unit, val onBack: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file From 0cea2545a7c4ce53d8a62e9b6367ea1d5b76aa0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 13:09:30 +0500 Subject: [PATCH 64/90] Updated on 2026-08-14 --- .../NavigationButtonsBlock.kt | 31 ------- .../NavigationButtonsState.kt | 1 - .../preview/NavigationButtonsPreview.kt | 9 -- core/res/src/main/res/values-de/strings.xml | 42 ++++----- core/res/src/main/res/values-fr/strings.xml | 25 ++--- core/res/src/main/res/values-it/strings.xml | 12 --- core/res/src/main/res/values-ja/strings.xml | 60 ++++++------ core/res/src/main/res/values-ru/strings.xml | 11 ++- .../src/main/res/values-uk-rUA/strings.xml | 42 +++------ .../src/main/res/values-zh-rTW/strings.xml | 14 --- core/res/src/main/res/values/strings.xml | 8 +- .../ui/components/inputrow/InputRowDefault.kt | 16 ++-- .../impl/presentation/state/StakingUiState.kt | 3 +- ...StakingActionSelectionBottomSheetConfig.kt | 11 +++ .../ConfirmationStatePreviewData.kt | 5 +- .../state/stub/StakingClickIntentsStub.kt | 5 +- .../SetButtonsStateTransformer.kt | 61 +----------- .../SetConfirmationStateAssentTransformer.kt | 5 +- ...tConfirmationStateInProgressTransformer.kt | 6 +- .../SetConfirmationStateLoadingTransformer.kt | 3 +- .../SetInitialDataStateTransformer.kt | 3 +- ...howActionSelectorBottomSheetTransformer.kt | 30 ++++++ .../state/utils/StakingPendingActionUtils.kt | 27 ++++++ .../impl/presentation/ui/StakingScreen.kt | 3 + .../ui/StakingValidatorListContent.kt | 2 +- .../StakingActionSelectorBottomSheet.kt | 92 +++++++++++++++++++ .../viewmodel/StakingClickIntents.kt | 9 +- .../viewmodel/StakingViewModel.kt | 64 ++++++++----- 28 files changed, 310 insertions(+), 290 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index 92a791ad8a..58cc324958 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -43,8 +43,6 @@ fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifi PreviousButton(state?.prevButton) PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) } - - SecondaryButton(state?.secondaryButton) } } @@ -79,35 +77,6 @@ private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = } } -@Composable -private fun SecondaryButton(secondaryButton: NavigationButton?) { - val wrappedButton by rememberNavigationButton(secondaryButton) - AnimatedContent( - targetState = wrappedButton, - transitionSpec = { navigationButtonsTransition() }, - contentAlignment = Alignment.Center, - label = "Animate show secondary button", - modifier = Modifier.fillMaxWidth(), - ) { button -> - if (button != null && button.textReference != TextReference.EMPTY) { - val icon = button.iconRes?.let { TangemButtonIconPosition.End(iconResId = it) } - ?: TangemButtonIconPosition.None - - TangemButton( - text = button.textReference.resolveReference(), - enabled = button.isEnabled, - onClick = button.onClick, - icon = icon, - showProgress = button.showProgress, - colors = TangemButtonsDefaults.secondaryButtonColors, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), - ) - } else { - Spacer(modifier = Modifier.fillMaxWidth()) - } - } -} - @Composable private fun PreviousButton(prevButton: NavigationButton?) { AnimatedVisibility( diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index c125a871d1..e9a1fd8a83 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -10,7 +10,6 @@ sealed class NavigationButtonsState { data class Data( val primaryButton: NavigationButton, val prevButton: NavigationButton?, - val secondaryButton: NavigationButton?, val extraButtons: ImmutableList, val txUrl: String? = null, ) : NavigationButtonsState() diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 738598de82..b98e5de371 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -30,14 +30,6 @@ internal object NavigationButtonsPreview { ), ) - private val next = NavigationButton( - textReference = resourceReference(R.string.common_next), - isSecondary = false, - isIconVisible = false, - showProgress = false, - isEnabled = true, - onClick = {}, - ) private val prev = NavigationButton( textReference = TextReference.EMPTY, iconRes = R.drawable.ic_back_24, @@ -60,7 +52,6 @@ internal object NavigationButtonsPreview { val allButtons = NavigationButtonsState.Data( primaryButton = finished, prevButton = prev, - secondaryButton = next, extraButtons = extraButtons, txUrl = "https://tangem.com", ) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 81ebcec61e..54492f5825 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -104,7 +104,6 @@ Erkunden Transaktionsverlauf einsehen Explorer - Gebühr Netzwerkgebühren sind Gebühren, die Nutzer für die Verarbeitung und Bestätigung von Transaktionen zahlen. Die Höhe der Gebühren kann von der Überlastung des Netzes, der Größe der Transaktion und der Ausführungspriorität abhängen. %s Schnell Markt @@ -114,6 +113,10 @@ Zum Anbieter gehen Zum Token Importieren + + in %d Tag + in %d Tagen + Später Gesperrt Hauptnetz @@ -169,7 +172,9 @@ Vertragsadresse Vertragsadresse ist ungültig Bitte wähle das Netzwerk - Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %li + Dieses Token wurde bereits zur Liste hinzugefügt + Token existiert bereits + Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %d Benutzerdefinierte Ableitung(derivation) E. g. m/00\'/0000\'/0\'/0/0 Benutzerdefinierte Ableitung (Derivation) eingeben @@ -339,12 +344,14 @@ Um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen, füge diesen Token zu mindestens 1 Netzwerk hinzu Dieses Asset ist nicht verfügbar Zum Portfolio hinzufügen - Token hinzufügen + Hinzufügen Verfügbare Netzwerke Mein Portfolio Markt Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen + Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s Die Daten konnten nicht geladen werden... + Keine Daten Schnelle Aktionen Ergebnis Token unter 100k Marktkapitalisierung anzeigen @@ -370,7 +377,7 @@ Bewertung, basierend auf %d Bewertungen, basierend auf %d - Blockchain-Site + Webseite Kaufdruck Die Differenz zwischen Käufer- und Verkäufervolumen Umlaufmenge @@ -466,7 +473,7 @@ Seed-Phrase verwenden Ungültige Seed-Phrase. Bitte überprüfe die Wortreihenfolge. Ungültige Seed-Phrase. Bitte überprüfe die Rechtschreibung. - veralteter Standard + Veralteter Standard Um zu überprüfen, ob du deine Seed-Phrase richtig aufgeschrieben hast, gib bitte das 2., 7. und 11 Wort ein. Eine letzte Prüfung! Um den Sicherungsvorgang zu starten, füge bis zu zwei Sicherungskarten hinzu. @@ -572,18 +579,11 @@ Die Gebühr, die für die Nutzung jeder nicht ausgegebenen Transaktionsausgabe (UTXO) im Kaspa-Netzwerk erforderlich ist. Je mehr UTXOs du in einer Transaktion verwenden, desto höher ist die Gebühr. KAS per UTXO %1$s, %2$s - Adresse Ziel-Tag Adresse eingeben Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein - Ungültiges Tag. Es wird der Transaktion nicht hinzugefügt. Ungültiges Memo. Sie wird der Transaktion nicht hinzugefügt. - Tag Memo - inkl. Gebühr - Niedrig - Normal - Priorität Überprüfe deine Netzwerkverbindung Informationen zur Netzwerkgebühr nicht erreichbar Von @@ -598,7 +598,7 @@ Abdeckung der Netzgebühren Unzureichende Mittel für die Überweisung, da die Summe aus Gebühr und Überweisungsbetrag das bestehende Guthaben übersteigt Gesamtbetrag übersteigt den Saldo - Das Konto wird von der Blockchain gelöscht, wenn der Kontostand unter die Mindesteinlage fällt. Bitte belasse %s auf deinem Konto. + Ein Guthaben von mindestens %s ist erforderlich, um dein Konto in der Blockchain aktiv zu halten und Sicherheitsrisiken zu vermeiden. Dieser Betrag verbleibt auf deinem Guthaben und kann nicht abgehoben werden. Mindesteinlage Der Kommissionsbetrag ist %s mal der empfohlene Betrag. Stelle sicher, dass die benutzerdefinierten Einstellungen korrekt sind. Die individuelle Gebühr ist hoch @@ -632,21 +632,15 @@ Versende %s Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s Du sendest **%1$s** und %2$s - Senden %s - Gesamt - %1$s und %2$s werden gesendet - ≈ %1$s (inkl. Gebühr: %2$s) - %s wird gesendet Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. - Ungültige Adresse Transaktion gesendet Bereite das Scannen der Karte vor, die du einrichten möchtest. Entferne diese Wallet Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name Aktiv - Um deine Kryptos zu unstaken, klick hier. + Um deine Vermögenswerte freizugeben, tippe auf den Block oben Die Anzahl der zu stakenden Krypros muss mindesten %s betragen Nicht gestakte beanspruche Jährliche prozentuale Rendite @@ -659,12 +653,12 @@ Marktbewertung Metriken Mindestanforderungen - Keine Belohnungen zu beanspruchen. + Keine Belohnungen Belohnungen beanspruchen Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden. Belohnungszeitplan Dabei handelt es sich um einen Zeitplan, der festlegt, wann die Teilnehmer am Staking ihre Belohnungen erhalten. - Belohnungen, die du beanspruchen kannst: %s + Belohnungen %s Staking %s Entbindungsdauer Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. @@ -678,6 +672,7 @@ Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. + Bereit zum Abheben Erneut binden Erneut staken Belohnungen erneut staken @@ -698,6 +693,7 @@ Mehr staken Stake %s Staking beenden%s + Lösen der Bindungen Gelocktes unlocken Unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen @@ -862,7 +858,7 @@ Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s Die Gebühr %s kann nicht gedeckt werden Der zu erhaltende Betrag muss mindestens %s betragen - Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %@) + Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %s) Ausgewähltes Paar vorübergehend nicht verfügbar Service vorübergehend nicht verfügbar Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 581c5d1af7..188ec9511e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -101,7 +101,6 @@ Explorez Explorez l\'historique des transactions Explorateur - Commissions Les frais de réseau sont des charges que les utilisateurs paient pour traiter et confirmer les transactions. Le montant des frais peut être affecté par la congestion du réseau, la taille de la transaction et la priorité d\'exécution. %s Rapide Marché @@ -164,7 +163,7 @@ Adresse du contrat L\'adresse du contrat n\'est pas valide Veuillez sélectionner le réseau - Les décimales doivent être un entier valide, jusqu\'à %li + Les décimales doivent être un entier valide, jusqu\'à %d Dérivation personnalisée Par exemple m/00\'/0000\'/0\'/0/0 Entrez une dérivation personnalisée @@ -508,18 +507,11 @@ Les frais requis pour l\'utilisation de chaque sortie de transaction non dépensée (UTXO) dans le réseau Kaspa. Plus vous utilisez d’UTXO dans une transaction, plus les frais seront élevés. KAS pour UTXO %1$s, %2$s - Adresse ID de destination Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille - Tag invalide. Il ne sera pas ajouté à la transaction. Mémo invalide. Il ne sera pas ajouté à la transaction. - Tag Memo - Inclure les commissions - Bas - Normal - Priorité Vérifiez votre connexion réseau Informations sur les frais de réseau inaccessibles De @@ -534,7 +526,7 @@ Couverture des frais de réseau Fonds insuffisants pour le transfert, car le total des frais et du montant du transfert dépasse le solde existant Le total dépasse le solde - Le compte sera effacé de la blockchain si un solde descend en dessous du dépôt existentiel. Veuillez en laisser %s sur votre solde. + Un solde d\'au moins %s est requis pour conserver votre compte sur la blockchain afin d\'éviter les risques de sécurité. Ce montant restera sur votre solde et ne pourra pas être retiré. Dépôt existentiel Le montant de la commission est %s fois le montant recommandé. Assurez-vous que les paramètres personnalisés sont corrects. Les frais de douane sont élevés @@ -568,21 +560,16 @@ Envoyer %s Vous envoyez **%1$s** incluant des frais de réseau de %2$s Vous envoyez **%1$s** et %2$s - Envoi de %s - Total - Sera envoyé %1$s et %2$s - ≈ %1$s (incl. les commissions : %2$s) - Sera envoyé %s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. - Adresse incorrecte Transaction envoyée Scannez la carte que vous souhaitez configurer. Oublier le portefeuille Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Nom Actif - Afin d\'unstaker vos actifs, cliquez ici. + Afin d\'unstaker vos actifs, appuyez sur le bloc ci-dessus + Pourcentage de rendement annuel Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. APR Disponible @@ -604,6 +591,7 @@ Le temps imparti pour activer la participation au staking. Native staking Gagnez des récompenses de staking + Les récompenses cessent de s\'accumuler immédiatement après que vous ayez commencé le unstaking. Le processus de unstaking prend %s. Récompenses Stake verrouillé Staker plus @@ -761,7 +749,8 @@ Pour effectuer une transaction, vous devez déposer %1$s %2$s Impossible de couvrir %s frais Le montant à recevoir doit être d\'au moins %s - Cela peut se produire car le fournisseur n\'est actuellement pas en mesure d\'échanger la paire que vous avez sélectionnée. Veuillez patienter un instant et réessayer. (Code %@) + Cela peut se produire car le fournisseur n\'est actuellement pas en mesure d\'échanger la paire que vous avez sélectionnée. Veuillez patienter un instant et réessayer. (Code %s) + La paire sélectionnée est temporairement indisponible Service temporairement indisponible Le nombre de jetons à échanger ne doit pas dépasser %s Le montant à échanger doit être d\'au moins %s diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index e7caa7829e..900ef94aa5 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -8,7 +8,6 @@ Rimuovere Fatto Errore - Commissione Costi della rete OK Mantieni le modifiche @@ -35,21 +34,10 @@ Avvicina la carta Crea portafoglio Importo - Indirizzo L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio - Tag Memo - Includi commissione - Insufficiente - Normale - Prioritario Importo totale - Totale - Sarà inviato %1$s e %2$s - ≈ %1$s (inc. commissione: %2$s) - Sarà inviato %s La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo - Indirizzo non valido Tangem Twin WalletConnect L\'indirizzo è stato copiato con successo diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index f27b090b8a..025170ee41 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -39,7 +39,7 @@ 試行回数が多すぎます お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 バックアップ処理を開始する - 法定通貨カードまたは銀行口座から + 銀行カードまたは銀行口座から %d カード @@ -102,7 +102,6 @@ 移動する 取引履歴を調べる エクスプローラー - 手数料 ネットワーク手数料は、取引の処理と確認のためにユーザーが支払う料金です。手数料の額は、ネットワークの混雑さ、取引のサイズ、実行の優先度によって左右されます。 %s 速い マーケット @@ -112,6 +111,9 @@ プロバイダーへ移動 トークンへ移動 インポート + + %d日で + 後で ロックされています メインネットワーク @@ -167,7 +169,9 @@ コントラクトアドレス コントラクトアドレスが無効です ネットワークを選択してください - 小数は%liまでの有効な整数である必要があります + このトークンはすでにリストに追加されています + トークンはすでに存在します + 小数は%dまでの有効な整数である必要があります カスタム派生パス 例:m/00\'/0000\'/0\'/0/0 カスタム派生パスを入力 @@ -333,15 +337,17 @@ 賛成票を投じる ウォレットを選択 ウォレットは複数のネットワークをサポートしていません。 - このアセットの購入、交換、受け取りを開始するには、このトークンを少なくとも1つのネットワークに追加してください。 + このアセットの購入、交換、受け取りを開始するには、ポートフォリオに追加してください。 このアセットは利用できません ポートフォリオに追加 - トークンを追加 + 追加 利用可能なネットワーク 私のポートフォリオ マーケット 選択したネットワークのアドレスを生成するには、Tangemカードをスキャンする必要があります。 + このセクションのデータは、次のネットワークから取得されています: %s データを読み込めません… + データなし クイックアクション 結果 時価総額10万ドル以下のトークンを見る @@ -366,7 +372,7 @@ %d のレーティングに基づいて - ブロックチェーンサイト + ウェブサイト 買い圧力 買い手と売り手の取引量の差 循環供給量 @@ -447,7 +453,7 @@ あなたのシードフレーズ - %d 単語 + %d 単語 ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 シードフレーズを生成する @@ -498,7 +504,7 @@ カメラへのアクセスが拒否されました %3$sネットワーク上の%1$s ( %2$s ) このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。 - QRコードを表示するか、アドレスを共有します + QRコードを表示するか、アドレスを共有してください 参加する 紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。 紹介プログラムに関する情報を読み込めませんでした。エラー コード: %s 。しばらくしてからもう一度お試しください。 @@ -560,18 +566,11 @@ Kaspaネットワークで未使用の取引出力(UTXO)を使用するために必要な手数料です。取引で使用するUTXOが多ければ多いほど、手数料は高くなります。 UTXOあたりのKAS %1$s 、 %2$s - アドレス 宛先タグ アドレスを入力 アドレスはウォレットアドレスと同じです - 無効なタグです。取引には追加されません。 無効なメモです。取引には追加されません。 - タグ メモ - 手数料込み - 低い - 普通 - 優先 ネットワーク接続を確認してください ネットワーク手数料についての情報にアクセスできません より @@ -586,7 +585,7 @@ ネットワーク手数料のカバー 手数料と送金額の合計が残高を超えているため、送金に必要な資金が不足しています。 合計が残高を超えています - 残高が最低量を下回ると、当アカウントはブロックチェーンから消去されます。残高に%sを残しておいてください。 + セキュリティリスクを防ぐため、ブロックチェーン上にアカウントを維持するには、少なくとも%s の残高が必要です。この金額は残高に残り、引き出すことはできません。 アカウント維持に必要な最低残高 手数料額が推奨額の%s倍となっています。カスタム設定が正しいことを再度確認してください。 カスタム手数料が高くなっています @@ -620,21 +619,15 @@ %sを送金する **%1$s** を送金する (ネットワーク手数料%2$sを含む) **%1$s** と %2$s を送金しています。 - %sを送信しています - 合計 - %1$sと%2$sが送信されます - ≈ %1$s (%2$s: 手数料を含む) - %sが送信されます 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 - 無効なアドレス 取引が送信されました セットアップしたいカードをスキャンするために準備してください。 ウォレット削除 これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 名前 アクティブ - 資産のステーキングを解除するには、ここをクリックしてください。 + 資産のステーキングを解除するには、上のブロックをタップしてください ステーキング金額は %s 以上である必要があります ステーキング解除分を請求する 年率 @@ -647,25 +640,27 @@ 市場評価 指標 最低要件 - 請求できる報酬はありません + 報酬なし 請求中の報酬 - ステーキング報酬を受け取る方法。自動または手動で請求できます。 + ステーキング報酬の受け取り方法。\n自動であなたのアドレスに報酬が入金される方法と、手動で取引を生成して報酬を引き出す方法があります。 報酬スケジュール これは、ステーキングの参加者がいつ報酬を受け取るかを決定するスケジュールです。 - 受け取る報酬: %s + 報酬: %s ステーキング%s 解約完了までの期間 ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。 ウォームアップ期間 ステーキングへの参加を有効にするために割り当てられた時間。 + ロック中 移行 ネイティブステーキング ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 - ステーキングにより%1$sを獲得できます。ステーキング報酬は1時間ごとに受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎時間受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 - ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。 + ステーキング解除プロセスの開始後、報酬の獲得はすぐに停止します。ステーキング解除には%sかかります。 + 引き出し準備完了 再結束 再度ステーキングする 報酬をステーキングする @@ -686,8 +681,9 @@ もっとステーキングする %sをステーキングする %sのステーキング解除 + ステーキング解約中 ステーキング解除はロックされています - スタックされていない + ステーキングされていない 資産を請求するために、unstakedを確認してください ステーキング解除 バリデーター @@ -842,7 +838,7 @@ ネットワークには最低残高が必要です スワップは、%s の取引完了後に利用可能となります。 アクティブな取引があります - スワップの承認は現在進行中で、まもなく完了する予定です。 + スワップ承認は現在進行中で、まもなく完了する予定です。 承認が進行中 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 あなたのリストには、交換可能な %s トークンがありません。 @@ -850,7 +846,7 @@ 取引を行うには、 %1$s %2$sを入金する必要があります。 %s 手数料を支払えません 受け取る金額は、 %s 以上である必要があります。 - これは、選択したペアがプロバイダーでは現在交換できないために発生する可能性があります。しばらく待ってからもう一度お試しください。(コード%@ ) + これは、選択したペアがプロバイダーでは現在交換できないために発生する可能性があります。しばらく待ってからもう一度お試しください。(コード%s ) 選択されたペアは一時的に利用できません サービスは一時的に利用できません スワップするトークンの量は %s を超えないでください @@ -866,6 +862,8 @@ このカードには%sの署名のみが残っています。資金をすべて引き出す必要があります。 署名数が少ないです 異なるネットワーク上のトークンは、異なるアドレスを持つ場合があります。資金を送金する際には、アドレスがネットワークと一致していることを再確認してください。 + MATICはPOLに移行中です。ただし、期限は設定されておらず、MATICはまだ廃止されていません。MATICトークンを引き続き安全に使用することも、取引所でPOLに交換することもできます。 + MATICからPOLへの移行 %d ネットワークのアドレスを取得するために、カードを利用してください diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0a7ca9ff35..50e75be7a4 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -71,6 +71,7 @@ Доступ запрещен Все Разрешить + Аналитика Применить Одобрение Подтвердить @@ -177,7 +178,7 @@ Адрес контракта Адрес контракта некорректен Пожалуйста, выберите сеть - Десятичное число должно быть действительным целым числом, до %li + Десятичное число должно быть действительным целым числом, до %d Своя деривация Например m/00\'/0000\'/0\'/0/0 Введите свою деривацию @@ -353,6 +354,7 @@ Рынок Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту Tangem кошелька Данные раздела получены из следующих сетей: %s + Нет данных Быстрые действия Результат Токены с капитализацией меньше 100к @@ -647,7 +649,7 @@ Для завершения стейкинга нажмите на блок выше Сумма для стейкинга должна быть не менее %s Забрать средства - Годовая процентная ставка + Процентная ставка Годовой процентный доход, который вы можете получить от участия в стейкинге. APR Доступно @@ -668,6 +670,7 @@ Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. Период прогрева Время, необходимое для начала процесса стейкинга и активации процесса начисления наград + Заблокировано Переместить Нативный стейкинг Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый день. @@ -696,7 +699,7 @@ Застейкать %s Вывести %s Разблокировать - Выведено из стейкинга + Вывод из стейкинга Проверьте процесс завершения стейкинга, чтобы вывести свои средства. Завершение стейкинга Валидатор @@ -860,7 +863,7 @@ Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s Невозможно покрыть комиссию %s Сумма получения не может быть менее %s - Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %@) + Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s) Выбранная пара временно недоступна Cервис временно недоступен Сумма для обмена должна быть не более %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index d0ff9d566d..767c26f4fd 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -78,8 +78,8 @@ Увага Баланс: %s Баланс - біометрична автентифікація - біометрією + біометричну автентифікацію + біометрії Купити Перейдіть до %1$s Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності @@ -107,7 +107,6 @@ Оглядач Переглянути історію транзакцій Оглядач - Комісія Мережеві комісії – це збори, які користувачі сплачують за обробку та підтвердження транзакцій. На розмір комісії може впливати перевантаження мережі, розмір транзакції та пріоритет виконання. %s Швидко За ринком @@ -152,7 +151,7 @@ Застейкати Стейкінг Почати - Надіслати + Продовжити Успіх Підтримка Обмін @@ -172,7 +171,7 @@ Адреса контракту Адреса контракту недійсна Будь ласка, оберіть мережу - Десяткове число повинно бути дійсним цілим числом, до %li + Десяткове число повинно бути дійсним цілим числом, до %d Власна деривація Наприклад, m/00\'/0000\'/0\'/0/0 Введіть власну деривацію @@ -377,7 +376,7 @@ На основі %d оцінок На основі %d оцінок - Блокчейн сайт + Веб-сайт Давлення покупця Різниця між обсягом покупців та обсягом продавців Циркуляційний запас @@ -473,7 +472,7 @@ Використовувати seed-фразу Невірна seed-фраза. Будь ласка, перевірте порядок слів. Невірна seed-фраза. Будь ласка, перевірте правопис. - Застарілий + Застаріло Щоб перевірити чи правильно ви записали seed-фразу, введіть 2-е, 7-е та 11-те слова Отже, давайте перевіримо Щоб почати процес резервного копіювання, додайте одну або дві резервні картки. @@ -530,7 +529,7 @@ за %d гаманців за %d гаманців - Отримайте ^^%1$s^^ на вашу адресу в мережі %2$s%3$s ^^через 30 днів^^ за кожен гаманець, який придбає ваш друг + Отримаєте ^^%1$s^^ на вашу адресу в мережі %2$s %3$s ^^через 30 днів^^ за кожен гаманець, який придбає ваш друг Ви Отримає при купівлі гаманця на сайті tangem.com @@ -583,18 +582,11 @@ Комісія, необхідна за використання кожної невитраченої транзакції (UTXO) у мережі Kaspa. Чим більше UTXO ви використовуєте в транзакції, тим вищою буде комісія. KAS за UTXO %1$s, %2$s - Адреса Тег призначення Введіть адресу Адреса збігається з адресою гаманця - Недопустимий Tag. Він не буде доданий у транзакцію. Недопустимий Memo. Він не буде доданий до транзакції. - Tag Memo - Включаючи комісію - Низька - Нормальна - Пріоритетна Перевірте підключення до мережі Інформація щодо комісії в мережі недоступна Із @@ -609,7 +601,7 @@ Покриття мережевої комісії Недостатньо коштів для здійснення переказу, оскільки загальна сума комісії та переказу перевищує наявний баланс Сума перевищує баланс - Рахунок буде видалено з блокчейну, якщо баланс стане нижчим за екзистенційний депозит. Будь ласка, залиште %s на своєму балансі. + Для збереження вашого акаунту у блокчейні та захисту від можливих ризиків необхідний баланс не менше %s. Ця сума залишиться на вашому рахунку та не може бути знята. Екзистенційний депозит Сума комісії в %s разів перевищує рекомендовану. Переконайтеся, що користувацькі налаштування вірні. Встановлена комісія завелика @@ -643,23 +635,18 @@ Надіслати %s Ви надсилаєте **%1$s**, включно з комісію мережі %2$s Ви надсилаєте **%1$s** і %2$s - Надсилання %s - Всього - %1$s та %2$s буде надіслано - ≈ %1$s (вкл. комісію: %2$s ) - %s буде надіслано Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. - Недійсна адреса Трансакцію надіслано - Підготуйтеся до сканування картку, яку потрібно налаштувати. + Підготуйте до сканування картку, яку потрібно налаштувати. Забути гаманець Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. Ім\'я Активний - Щоб вивести активи зі стейкінгу, натисніть тут. + Для завершення стейкінгу натисніть на блок вище Сума для стейкінгу має бути не менше %s Зняти кошти + Процентна ставка Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. APR Доступно @@ -843,8 +830,6 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця - Затвердження обміну триває і незабаром буде завершено - Затвердження в процесі Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. @@ -862,13 +847,16 @@ Для роботи з мережею вимагається депозит Обмін буде доступний після завершення %s транзакції У вас є активна транзакція + Затвердження обміну триває і незабаром буде завершено + Затвердження в процесі Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s. У вашому списку немає доступних монет для обміну %s Немає доступних токенів для обміну Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s Неможливо покрити комісію %s Сума отримання не може бути меншою за %s - Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %@) + Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %s) + Обрана пара тимчасово недоступна Сервіс тимчасово недоступний Сума до обміну не повинна перевищувати %s Сума для обміну має бути не менше %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 3c2329daab..b860623796 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -57,7 +57,6 @@ 允許 啟用 錯誤 - 費用 導入 網路費 @@ -251,24 +250,11 @@ 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 準備好您的卡! 數量 - 地址 地址與錢包地址相同 - 標籤無效。它不會被添加到交易中 Memo無效。 它不會被添加到交易中 - Tag Memo - 包含費用 - - 正常 - 優先 最大值 - 發送 %s - 總計 - %1$s 和 %2$s 將被發送 - ≈ %1$s (費用包含: %2$s) - %s 將被發送 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 - 無效地址 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 創新式的硬體錢包 最多 3張實體卡片 到一個錢包 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index fce9968300..314a9a07d4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -70,6 +70,7 @@ Access denied All Allow + Analytics Apply Approval Approve @@ -82,6 +83,7 @@ Go to %1$s You have not given access to your camera, please adjust your privacy settings Cancel + Choose action Claim rewards Close Continue @@ -174,7 +176,7 @@ Please select the network This token has already been added to the list Token already exists - Decimal must be a valid integer, up to %li + Decimal must be a valid integer, up to %d Custom derivation E. g. m/00\'/0000\'/0\'/0/0 Enter custom derivation @@ -351,6 +353,7 @@ To generate addresses for selected networks, you must scan your Tangem Wallet card This section’s data is sourced from the following networks: %s Unable to load the data… + No data Quick actions Result See tokens under 100k market cap @@ -659,6 +662,7 @@ The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period The allocated time for activating participation in staking. + Locked Migrate Native staking Staking allows you to earn %1$s. Your staking rewards arrive every day. @@ -854,7 +858,7 @@ To make a transaction you need to deposit some %1$s %2$s Unable to cover %s fee The amount to receive must be at least %s - This may occur because the provider is currently unable to exchange your selected pair. Please wait a moment and try again. (Code %@) + This may occur because the provider is currently unable to exchange your selected pair. Please wait a moment and try again. (Code %s) Selected pair temporarily unavailable Service temporarily unavailable The amount of tokens to be swapped must not exceed %s diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 1eff068d9f..3ef8fefbc8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -43,9 +43,9 @@ import com.tangem.core.ui.res.TangemThemePreview */ @Composable fun InputRowDefault( - title: TextReference, text: TextReference, modifier: Modifier = Modifier, + title: TextReference? = null, titleColor: Color = TangemTheme.colors.text.secondary, textColor: Color = TangemTheme.colors.text.primary1, iconRes: Int? = null, @@ -66,16 +66,18 @@ fun InputRowDefault( modifier = Modifier .weight(1f), ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = titleColor, - ) + title?.let { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = titleColor, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8), + ) + } Text( text = text.resolveReference(), style = TangemTheme.typography.body2, color = textColor, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } iconRes?.let { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 0e08d89b1c..55e576c90f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -83,11 +83,10 @@ internal sealed class StakingStates { val innerState: InnerConfirmationStakingState, val feeState: FeeState, val validatorState: ValidatorState, - val pendingActions: ImmutableList, + val pendingAction: PendingAction?, val notifications: ImmutableList, val footerText: String, val transactionDoneState: TransactionDoneState, - val pendingActionInProgress: PendingAction? = null, val isApprovalNeeded: Boolean, ) : ConfirmationState() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt new file mode 100644 index 0000000000..873ba40422 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt @@ -0,0 +1,11 @@ +package com.tangem.features.staking.impl.presentation.state.bottomsheet + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.staking.model.stakekit.PendingAction + +internal data class StakingActionSelectionBottomSheetConfig( + val title: TextReference, + val actions: List, + val onActionSelect: (PendingAction) -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index f8a64a3695..ef4234d37c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -9,9 +9,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingNotification -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.ValidatorState import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -90,7 +87,7 @@ internal object ConfirmationStatePreviewData { ), ), transactionDoneState = TransactionDoneState.Empty, - pendingActions = persistentListOf(), + pendingAction = null, isApprovalNeeded = false, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 7220af1f41..8876bdabdb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -6,16 +6,15 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents -import kotlinx.collections.immutable.ImmutableList @Suppress("TooManyFunctions") internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList) {} + override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) {} - override fun onActionClick(pendingAction: PendingAction?) {} + override fun onActionClick() {} override fun onPrevClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 3f8b253474..6bd2c902fa 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -5,10 +5,9 @@ import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -22,7 +21,6 @@ internal class SetButtonsStateTransformer : Transformer { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), prevButton = getPrevButton(prevState), - secondaryButton = getSecondaryButton(prevState), extraButtons = getExtraButtons(prevState), txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, ) @@ -37,48 +35,22 @@ internal class SetButtonsStateTransformer : Transformer { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data val innerConfirmState = confirmState?.innerState - val isPrimaryInProgress = - confirmState?.pendingActions?.getPrimaryAction() == confirmState?.pendingActionInProgress val isConfirmation = prevState.currentStep == StakingStep.Confirmation val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED val isIconVisible = isConfirmation && !isCompleted - val isShowProgress = isInProgress && isPrimaryInProgress return NavigationButton( textReference = prevState.getButtonText(), iconRes = R.drawable.ic_tangem_24, isSecondary = false, isIconVisible = isIconVisible, - showProgress = isShowProgress, + showProgress = isInProgress, isEnabled = prevState.isButtonEnabled(), onClick = { prevState.onPrimaryClick() }, ) } - private fun getSecondaryButton(prevState: StakingUiState): NavigationButton? { - val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - val innerConfirmState = confirmState?.innerState - - val isConfirmation = prevState.currentStep == StakingStep.Confirmation - val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS - val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED - - return confirmState?.pendingActions?.getSecondaryAction()?.let { pendingAction -> - val isSecondaryInProgress = pendingAction == confirmState.pendingActionInProgress - val isShowProgress = isInProgress && isSecondaryInProgress - NavigationButton( - textReference = getPendingActionTitle(pendingAction.type), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = isShowProgress, - isEnabled = prevState.isButtonEnabled(), - onClick = { prevState.clickIntents.onActionClick(pendingAction) }, - ).takeIf { isConfirmation && !isCompleted } - } - } - private fun getPrevButton(prevState: StakingUiState): NavigationButton? { return NavigationButton( textReference = TextReference.EMPTY, @@ -114,10 +86,6 @@ internal class SetButtonsStateTransformer : Transformer { ) } - private fun List.getPrimaryAction(): PendingAction? = getOrNull(0) - - private fun List.getSecondaryAction(): PendingAction? = getOrNull(1) - private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) { StakingStep.RewardsValidators -> false else -> true @@ -158,7 +126,7 @@ internal class SetButtonsStateTransformer : Transformer { StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake) StakingActionCommonType.PENDING_OTHER, StakingActionCommonType.PENDING_REWARDS, - -> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type) + -> confirmationState.pendingAction?.type.getPendingActionTitle() } } } else { @@ -192,7 +160,7 @@ internal class SetButtonsStateTransformer : Transformer { if (isEnterAction && isApproveNeeded) { clickIntents.showApprovalBottomSheet() } else { - clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull()) + clickIntents.onActionClick() } } } else { @@ -219,25 +187,4 @@ internal class SetButtonsStateTransformer : Transformer { StakingStep.Validators -> true } } - - @Suppress("CyclomaticComplexMethod") - private fun getPendingActionTitle(type: StakingActionType?): TextReference = when (type) { - StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards) - StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards) - StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw) - StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake) - StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked) - StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked) - StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked) - StakingActionType.VOTE -> resourceReference(R.string.staking_vote) - StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke) - StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked) - StakingActionType.REVOTE -> resourceReference(R.string.staking_revote) - StakingActionType.REBOND -> resourceReference(R.string.staking_rebond) - StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate) - StakingActionType.STAKE -> resourceReference(R.string.common_stake) - StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake) - StakingActionType.UNKNOWN -> TextReference.EMPTY - null -> TextReference.EMPTY - } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 6a214fde9a..ee19ad787a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -10,13 +10,12 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, - private val pendingActionList: ImmutableList, + private val action: PendingAction?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -38,7 +37,7 @@ internal class SetConfirmationStateAssentTransformer( isFeeApproximate = false, ), validatorState = validatorState.copySealed(isClickable = true), - pendingActions = pendingActionList, + pendingAction = action, isPrimaryButtonEnabled = true, isApprovalNeeded = false, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt index 817ff94491..e17aad84b4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -1,14 +1,11 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer -internal class SetConfirmationStateInProgressTransformer( - private val pendingAction: PendingAction?, -) : Transformer { +internal class SetConfirmationStateInProgressTransformer : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( @@ -22,7 +19,6 @@ internal class SetConfirmationStateInProgressTransformer( isPrimaryButtonEnabled = false, innerState = InnerConfirmationStakingState.IN_PROGRESS, validatorState = validatorState.copySealed(isClickable = false), - pendingActionInProgress = pendingAction, ) } else { this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 07ba1f9fce..9e35b20442 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -32,8 +32,7 @@ internal class SetConfirmationStateLoadingTransformer( notifications = getNotifications(prevState), footerText = "", transactionDoneState = TransactionDoneState.Empty, - pendingActions = persistentListOf(), - pendingActionInProgress = null, + pendingAction = null, isApprovalNeeded = false, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 6402fc548e..19f1c44544 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -212,8 +212,7 @@ internal class SetInitialDataStateTransformer( notifications = persistentListOf(), footerText = "", transactionDoneState = TransactionDoneState.Empty, - pendingActions = persistentListOf(), - pendingActionInProgress = null, + pendingAction = null, isApprovalNeeded = isApprovalNeeded, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt new file mode 100644 index 0000000000..2c927652c7 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class ShowActionSelectorBottomSheetTransformer( + private val pendingActions: ImmutableList, + private val onActionSelect: (PendingAction) -> Unit, + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + content = StakingActionSelectionBottomSheetConfig( + title = resourceReference(R.string.common_choose_action), + actions = pendingActions, + onActionSelect = onActionSelect, + ), + onDismissRequest = onDismiss, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt new file mode 100644 index 0000000000..0116e0d6d5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -0,0 +1,27 @@ +package com.tangem.features.staking.impl.presentation.state.utils + +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.action.StakingActionType + +@Suppress("CyclomaticComplexMethod") +internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) { + StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards) + StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards) + StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw) + StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake) + StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked) + StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked) + StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked) + StakingActionType.VOTE -> resourceReference(R.string.staking_vote) + StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke) + StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked) + StakingActionType.REVOTE -> resourceReference(R.string.staking_revote) + StakingActionType.REBOND -> resourceReference(R.string.staking_rebond) + StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate) + StakingActionType.STAKE -> resourceReference(R.string.common_stake) + StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake) + StakingActionType.UNKNOWN -> TextReference.EMPTY + null -> TextReference.EMPTY +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 2bbb05b0c2..9fad6829f4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -24,7 +24,9 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig +import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingActionSelectorBottomSheet import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first @@ -74,6 +76,7 @@ fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { when (bottomSheetConfig.content) { is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig) is GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheet(bottomSheetConfig) + is StakingActionSelectionBottomSheetConfig -> StakingActionSelectorBottomSheet(bottomSheetConfig) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index e6ed596800..57ad4dd680 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -58,7 +58,7 @@ internal fun StakingValidatorListContent( InputRowImageSelector( subtitle = stringReference(item.name), caption = combinedReference( - resourceReference(R.string.staking_details_apr), + resourceReference(R.string.staking_details_annual_percentage_rate), annotatedReference { appendSpace() appendColored( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt new file mode 100644 index 0000000000..8f4434e496 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt @@ -0,0 +1,92 @@ +package com.tangem.features.staking.impl.presentation.ui.bottomsheet + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig +import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun StakingActionSelectorBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + title = { content -> + TangemBottomSheetTitle(title = content.title) + }, + containerColor = TangemTheme.colors.background.tertiary, + ) { content -> + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing32, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + content.actions.forEachIndexed { index, action -> + InputRowDefault( + text = action.type.getPendingActionTitle(), + textColor = TangemTheme.colors.text.primary1, + showDivider = index != content.actions.lastIndex, + modifier = Modifier + .fillMaxWidth() + .clickable { + content.onActionSelect(action) + }, + ) + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_StakingActionSelectorBottomSheet() { + TangemThemePreview { + StakingActionSelectorBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = StakingActionSelectionBottomSheetConfig( + title = resourceReference(R.string.common_select_action), + actions = persistentListOf( + PendingAction( + type = StakingActionType.CLAIM_REWARDS, + passthrough = "", + args = null, + ), + PendingAction( + type = StakingActionType.RESTAKE_REWARDS, + passthrough = "", + args = null, + ), + ), + onActionSelect = {}, + ), + ), + ) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index fee1e2115d..ad7b9ff1c6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -6,19 +6,14 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick( - actionType: StakingActionCommonType? = null, - pendingActions: ImmutableList = persistentListOf(), - ) + fun onNextClick(actionType: StakingActionCommonType? = null, pendingAction: PendingAction? = null) - fun onActionClick(pendingAction: PendingAction?) + fun onActionClick() fun onPrevClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 8dea58c5cd..523380111a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -63,7 +63,6 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -157,25 +156,25 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList) { + override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) { if (actionType != null) { stateController.update { it.copy(actionType = actionType) } } stakingStateRouter.onNextClick() if (isAssentState()) { - getFee(pendingActions) + getFee(pendingAction) } } - override fun onActionClick(pendingAction: PendingAction?) { - handleOnNextConfirmationClick(pendingAction) + override fun onActionClick() { + handleOnNextConfirmationClick() stakingStateRouter.onNextClick() } - private fun handleOnNextConfirmationClick(pendingAction: PendingAction?) { + private fun handleOnNextConfirmationClick() { if (isAssentState()) { viewModelScope.launch { - stateController.update(SetConfirmationStateInProgressTransformer(pendingAction)) + stateController.update(SetConfirmationStateInProgressTransformer()) val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") @@ -186,6 +185,7 @@ internal class StakingViewModel @Inject constructor( val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address") + val pendingAction = confirmationState.pendingAction val stakingTransactions = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -230,13 +230,13 @@ internal class StakingViewModel @Inject constructor( sendStakingTransaction( fullTransactionsData = fullTransactionsData, fee = fee, - pendingActionList = confirmationState.pendingActions, + pendingAction = confirmationState.pendingAction, ) } } } - private fun getFee(pendingActions: ImmutableList) { + private fun getFee(pendingAction: PendingAction?) { viewModelScope.launch { stateController.update( SetConfirmationStateLoadingTransformer( @@ -270,7 +270,7 @@ internal class StakingViewModel @Inject constructor( ) } else { estimateGas( - pendingActions = pendingActions, + pendingAction = pendingAction, amount = amount, sourceAddress = sourceAddress, validatorAddress = validatorAddress, @@ -278,7 +278,7 @@ internal class StakingViewModel @Inject constructor( } } else { estimateGas( - pendingActions = pendingActions, + pendingAction = pendingAction, amount = amount, sourceAddress = sourceAddress, validatorAddress = validatorAddress, @@ -288,12 +288,11 @@ internal class StakingViewModel @Inject constructor( } private suspend fun estimateGas( - pendingActions: ImmutableList, + pendingAction: PendingAction?, amount: BigDecimal, sourceAddress: String, validatorAddress: String, ) { - val pendingAction = pendingActions.firstOrNull() val gasEstimate = estimateGasUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, @@ -323,7 +322,7 @@ internal class StakingViewModel @Inject constructor( decimals = gasEstimate.token.decimals, ), ), - pendingActionList = pendingActions, + action = pendingAction, ), ) } @@ -397,14 +396,29 @@ internal class StakingViewModel @Inject constructor( } override fun onActiveStake(activeStake: BalanceState) { - val actionType = if (activeStake.pendingActions.isEmpty()) { - StakingActionCommonType.EXIT + if (activeStake.pendingActions.size > 1) { + stateController.update( + ShowActionSelectorBottomSheetTransformer( + pendingActions = activeStake.pendingActions, + onActionSelect = { + onNextClick(actionType = StakingActionCommonType.PENDING_OTHER, pendingAction = it) + stateController.update(DismissBottomSheetStateTransformer()) + }, + onDismiss = { + stateController.update(DismissBottomSheetStateTransformer()) + }, + ), + ) } else { - StakingActionCommonType.PENDING_OTHER + val actionType = if (activeStake.pendingActions.isEmpty()) { + StakingActionCommonType.EXIT + } else { + StakingActionCommonType.PENDING_OTHER + } + stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) + onNextClick(actionType, activeStake.pendingActions.firstOrNull()) } - stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - onNextClick(actionType, activeStake.pendingActions) } override fun showApprovalBottomSheet() { @@ -479,13 +493,13 @@ internal class StakingViewModel @Inject constructor( ifRight = { stateController.update(SetApprovalInProgressTransformer) stateController.update(DismissBottomSheetStateTransformer()) - awaitForAllowance(confirmationState.pendingActions) + awaitForAllowance(confirmationState.pendingAction) }, ) }.saveIn(approvalJobHolder) } - private fun awaitForAllowance(pendingActions: ImmutableList) { + private fun awaitForAllowance(pendingAction: PendingAction?) { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( scope = viewModelScope, @@ -504,7 +518,7 @@ internal class StakingViewModel @Inject constructor( val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") if (allowance >= amount) { - getFee(pendingActions) + getFee(pendingAction) allowanceTaskScheduler.cancelTask() } }, @@ -643,7 +657,7 @@ internal class StakingViewModel @Inject constructor( private suspend fun sendStakingTransaction( fullTransactionsData: List, fee: Fee, - pendingActionList: ImmutableList, + pendingAction: PendingAction?, ) { sendMultipleTransactionUseCase( txsData = fullTransactionsData.map { it.tangemTransaction }, @@ -657,7 +671,7 @@ internal class StakingViewModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = fee, - pendingActionList = pendingActionList, + action = pendingAction, ), ) stakingEventFactory.createSendTransactionErrorAlert(error) From 473a936863ef6a87ef7f8a99debdd16fddc5cbad Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 15:44:47 +0400 Subject: [PATCH 65/90] Updated on 2026-08-14 --- .../core/ui/components/rows/BlockchainRow.kt | 3 + .../components/rows/model/BlockchainRowUM.kt | 1 + .../managetokens/ui/ManageTokensScreen.kt | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 113 ++++++++++++++++++ .../impl/model/BlockchainRowUMConverter.kt | 38 ++++++ .../impl/model/PortfolioBSVisibilityModel.kt | 14 +++ .../impl/model/SelectNetworkUMConverter.kt | 32 +++++ .../impl/ui/AddToPortfolioBottomSheet.kt | 11 +- .../PreviewAddToPortfolioBSContentProvider.kt | 3 +- .../ui/state/AddToPortfolioBSContentUM.kt | 1 - .../com/tangem/lib/crypto/BlockchainUtils.kt | 37 +++++- 11 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index 035be68d2e..70150328ce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -137,6 +137,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid private class BlockchainRowParameterProvider : CollectionPreviewParameterProvider( collection = listOf( BlockchainRowUM( + id = "0", name = "BNB BEACON CHAIN", type = "BEP20", iconResId = R.drawable.img_bsc_22, @@ -144,6 +145,7 @@ private class BlockchainRowParameterProvider : CollectionPreviewParameterProvide isSelected = true, ), BlockchainRowUM( + id = "1", name = "1234567890111213141516171819", type = "BEP20", iconResId = R.drawable.ic_bsc_16, @@ -151,6 +153,7 @@ private class BlockchainRowParameterProvider : CollectionPreviewParameterProvide isSelected = false, ), BlockchainRowUM( + id = "2", name = "BNB BEACON CHAIN", type = "1234567890111213141516171819", iconResId = R.drawable.ic_bsc_16, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt index f8e756a9ae..700b4bc377 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable @Immutable data class BlockchainRowUM( + val id: String, val name: String, val type: String, val iconResId: Int, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index c8771d1fa6..430896ed3a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -348,6 +348,7 @@ private fun NetworksList( modifier = Modifier.padding(end = TangemTheme.dimens.spacing8), model = with(network) { BlockchainRowUM( + id = id, name = name, type = type, iconResId = iconResId, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt new file mode 100644 index 0000000000..9983cd1bb2 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -0,0 +1,113 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM +import kotlinx.collections.immutable.toImmutableList + +/** + * Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig] + * + * @property token token params + * @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed + * @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed + * @property onNetworkSwitchClick callback is invoked when network switch is clicked + * @property onWalletSelect callback is invoked when wallet is selected + * @property onContinueClick callback is invoked when continue button is clicked + * +[REDACTED_AUTHOR] + */ +internal class AddToPortfolioBSContentUMFactory( + private val token: TokenMarketParams, + private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, + private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, + private val onNetworkSwitchClick: (String, Boolean) -> Unit, + private val onWalletSelect: (UserWalletId) -> Unit, + private val onContinueClick: () -> Unit, +) { + + /** + * Create [TangemBottomSheetConfig] + * + * @param portfolioData portfolio data + * @param portfolioBSVisibilityModel portfolio bottom sheet visibility model + * @param selectedWallet selected wallet + * @param networksWithToggle networks with toggle + * @param isUserChangedNetworks flag indicates if user changed networks + */ + fun create( + portfolioData: PortfolioData, + portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + selectedWallet: UserWallet, + networksWithToggle: Map, + isUserChangedNetworks: Boolean, + ): TangemBottomSheetConfig { + return TangemBottomSheetConfig( + isShow = portfolioBSVisibilityModel.addToPortfolioBSVisibility, + onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, + content = AddToPortfolioBSContentUM( + selectedWallet = selectedWallet.toSelectedUserWalletItemUM(), + selectNetworkUM = SelectNetworkUMConverter( + networksWithToggle = networksWithToggle, + onNetworkSwitchClick = onNetworkSwitchClick, + ).convert(value = token), + isScanCardNotificationVisible = false, // TODO [REDACTED_JIRA] + continueButtonEnabled = isUserChangedNetworks, + onContinueButtonClick = onContinueClick, + walletSelectorConfig = crateWalletSelectorBSConfig( + isShow = portfolioBSVisibilityModel.walletSelectorBSVisibility, + portfolioData = portfolioData, + selectedWalletId = selectedWallet.walletId, + ), + ), + ) + } + + private fun UserWallet.toSelectedUserWalletItemUM(): UserWalletItemUM { + return UserWalletItemUMConverter( + onClick = { onWalletSelectorVisibilityChange(true) }, + endIcon = UserWalletItemUM.EndIcon.Arrow, + ).convert(value = this) + } + + private fun crateWalletSelectorBSConfig( + isShow: Boolean, + portfolioData: PortfolioData, + selectedWalletId: UserWalletId, + ): TangemBottomSheetConfig { + return TangemBottomSheetConfig( + isShow = isShow, + onDismissRequest = { onWalletSelectorVisibilityChange(false) }, + content = WalletSelectorBSContentUM( + userWallets = portfolioData.walletsWithCurrencyStatuses + .filterKeys(UserWallet::isMultiCurrency) + .map { it.key } + .map { userWallet -> + val balance = portfolioData.walletsWithBalance[userWallet.walletId] + + UserWalletItemUMConverter( + onClick = onWalletSelect, + appCurrency = portfolioData.appCurrency, + balance = balance?.getOrNull(), + isLoading = balance?.isLoading() == true, + isBalanceHidden = portfolioData.isBalanceHidden, + endIcon = if (userWallet.walletId == selectedWalletId) { + UserWalletItemUM.EndIcon.Checkmark + } else { + UserWalletItemUM.EndIcon.None + }, + ).convert(userWallet) + } + .toImmutableList(), + onBack = { onWalletSelectorVisibilityChange(false) }, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt new file mode 100644 index 0000000000..b80d71c2d1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt @@ -0,0 +1,38 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter + +/** + * Converter from [TokenMarketInfo.Network] to [BlockchainRowUM] + * +[REDACTED_AUTHOR] + */ +internal object BlockchainRowUMConverter : Converter, BlockchainRowUM> { + + override fun convert(value: Pair): BlockchainRowUM { + val (network, isSelected) = value + + val blockchainInfo = BlockchainUtils.getNetworkInfo(networkId = network.networkId) + ?: error("Can't find blockchain info for ${network.networkId}") + + val isMainNetwork = network.contractAddress == null + + return BlockchainRowUM( + id = network.networkId, + name = blockchainInfo.name, + type = if (isMainNetwork) "MAIN" else blockchainInfo.protocolName, + iconResId = if (isSelected) { + getActiveIconRes(blockchainInfo.blockchainId) + } else { + getGreyedOutIconRes(blockchainInfo.blockchainId) + }, + isMainNetwork = isMainNetwork, + isSelected = isSelected, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt new file mode 100644 index 0000000000..b2d43ea69a --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt @@ -0,0 +1,14 @@ +package com.tangem.features.markets.portfolio.impl.model + +/** + * Model for portfolio bottom sheet visibility + * + * @property addToPortfolioBSVisibility visibility of add to portfolio bottom sheet + * @property walletSelectorBSVisibility visibility of wallet selector bottom sheet + * +[REDACTED_AUTHOR] + */ +internal data class PortfolioBSVisibilityModel( + val addToPortfolioBSVisibility: Boolean = false, + val walletSelectorBSVisibility: Boolean = false, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt new file mode 100644 index 0000000000..ea02d5c805 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter from [TokenMarketParams] to [SelectNetworkUM] + * + * @property networksWithToggle map of networks with toggles + * @property onNetworkSwitchClick callback is called when network switch is clicked + * +[REDACTED_AUTHOR] + */ +internal class SelectNetworkUMConverter( + private val networksWithToggle: Map, + private val onNetworkSwitchClick: (String, Boolean) -> Unit, +) : Converter { + + override fun convert(value: TokenMarketParams): SelectNetworkUM { + return SelectNetworkUM( + tokenId = value.id, + iconUrl = value.imageUrl, + tokenName = value.name, + tokenCurrencySymbol = value.symbol, + networks = BlockchainRowUMConverter.convertList(networksWithToggle.toList()).toImmutableList(), + onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um.id, isChecked) }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 422bcdb678..315718d001 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -35,7 +35,6 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -214,15 +213,7 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi modifier = Modifier.padding( end = TangemTheme.dimens.spacing4, ), - model = with(network) { - BlockchainRowUM( - name = name, - type = type, - iconResId = iconResId, - isMainNetwork = isMainNetwork, - isSelected = isSelected, - ) - }, + model = network, action = { TangemSwitch( checked = network.isSelected, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index d218f4f8db..725f9518be 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -15,6 +15,7 @@ import kotlinx.collections.immutable.persistentListOf internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { private val blockchainRow = BlockchainRowUM( + id = "1", name = "Etherium 3", type = "TEST", iconResId = R.drawable.ic_eth_16, @@ -60,7 +61,6 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider onDismissRequest = {}, content = TangemBottomSheetConfigContent.Empty, ), - onWalletClick = {}, ), AddToPortfolioBSContentUM( selectedWallet = userWallet, @@ -88,7 +88,6 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider onDismissRequest = {}, content = TangemBottomSheetConfigContent.Empty, ), - onWalletClick = {}, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt index 7369b5cf69..ee30ab8615 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -10,6 +10,5 @@ internal data class AddToPortfolioBSContentUM( val isScanCardNotificationVisible: Boolean, val continueButtonEnabled: Boolean, val onContinueButtonClick: () -> Unit, - val onWalletClick: () -> Unit, val walletSelectorConfig: TangemBottomSheetConfig, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index dd253860fd..830677086d 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -35,12 +35,6 @@ object BlockchainUtils { return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } - /** If current [networkId] is Dogecoin */ - fun isDogecoin(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) - return blockchain == Blockchain.Dogecoin - } - /** If current [networkId] is Tezos */ fun isTezos(networkId: String): Boolean { val blockchain = Blockchain.fromId(networkId) @@ -61,4 +55,35 @@ object BlockchainUtils { fun isSupportedNetworkId(networkId: String): Boolean { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } + + data class BlockchainInfo( + val blockchainId: String, + val name: String, + val protocolName: String, + ) + + fun getNetworkInfo(networkId: String): BlockchainInfo? { + val blockchain = Blockchain.fromNetworkId(networkId) ?: return null + + return BlockchainInfo( + blockchainId = blockchain.id, + name = getNetworkNameWithoutTestnet(blockchain), + protocolName = getNetworkStandardName(blockchain), + ) + } + + private fun getNetworkStandardName(blockchain: Blockchain): String { + return when (blockchain) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20" + Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20" + Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2" + Blockchain.Tron, Blockchain.TronTestnet -> "TRC20" + Blockchain.TON -> "TON" + else -> "" + } + } + + private fun getNetworkNameWithoutTestnet(blockchain: Blockchain): String { + return blockchain.getNetworkName().replace(oldValue = " Testnet", newValue = "") + } } \ No newline at end of file From df938519a2324f579537619b732ea770e8ad6df3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 18:44:22 +0500 Subject: [PATCH 66/90] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 8 ++ .../tangemTech/models/UserTokensResponse.kt | 19 ++++- .../DefaultCustomTokensRepository.kt | 80 ++++++++++++++----- .../managetokens/di/ManageTokensDataModule.kt | 2 + .../utils/ManagedCryptoCurrencyFactory.kt | 1 + .../utils/TokenAddressesConverter.kt | 29 +++++++ .../repository/DefaultCurrenciesRepository.kt | 10 +-- .../model/ManagedCryptoCurrency.kt | 1 + ...emoveCustomManagedCryptoCurrencyUseCase.kt | 18 +++++ .../repository/CustomTokensRepository.kt | 5 +- .../utils/list/ManageTokensListManager.kt | 9 +++ .../utils/list/ManageTokensUiActions.kt | 2 + .../utils/list/ManageTokensUiManager.kt | 3 +- 13 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 1513f4d484..576da07163 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -48,6 +48,14 @@ internal object ManageTokensDomainModule { return CheckIsCurrencyNotAddedUseCase(customTokensRepository) } + @Provides + @Singleton + fun provideRemoveCustomManagedCryptoCurrencyUseCase( + customTokensRepository: CustomTokensRepository, + ): RemoveCustomManagedCryptoCurrencyUseCase { + return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository) + } + @Provides @Singleton fun provideSaveManagedTokensUseCase( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index c4b95bba88..dcd3107e06 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.tangem.common.extensions.calculateHashCode data class UserTokensResponse( @Json(name = "version") val version: Int = 0, @@ -17,7 +18,23 @@ data class UserTokensResponse( @Json(name = "symbol") val symbol: String, @Json(name = "decimals") val decimals: Int, @Json(name = "contractAddress") val contractAddress: String?, - ) + ) { + override fun equals(other: Any?): Boolean { + val otherToken = other as? Token ?: return false + + return otherToken.contractAddress == this.contractAddress && + otherToken.networkId == this.networkId && + otherToken.derivationPath == this.derivationPath && + otherToken.decimals == this.decimals + } + + override fun hashCode(): Int = calculateHashCode( + contractAddress.hashCode(), + networkId.hashCode(), + derivationPath.hashCode(), + decimals.hashCode(), + ) + } enum class GroupType { @Json(name = "none") diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index eabde921a1..0ec95b565f 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -1,37 +1,46 @@ package com.tangem.data.managetokens -import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter -import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.data.common.currency.getBlockchain +import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.getNetwork +import com.tangem.data.managetokens.utils.TokenAddressesConverter +import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, private val appPreferencesStore: AppPreferencesStore, + private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, ) : CustomTokensRepository { private val cryptoCurrencyFactory = CryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + private val tokenAddressConverter = TokenAddressesConverter() + private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean = withContext(dispatchers.io) { @@ -73,7 +82,7 @@ internal class DefaultCustomTokensRepository( val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") val network = getNetwork(networkId, derivationPath) - val tokenAddress = convertTokenAddress( + val tokenAddress = tokenAddressConverter.convertTokenAddress( networkId, contractAddress, symbol = null, @@ -112,10 +121,7 @@ internal class DefaultCustomTokensRepository( } } - override suspend fun createCoin( - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin { + override fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin { val network = getNetwork(networkId, derivationPath) return cryptoCurrencyFactory.createCoin(network) @@ -127,7 +133,7 @@ internal class DefaultCustomTokensRepository( formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { val network = getNetwork(networkId, derivationPath) - val tokenAddress = convertTokenAddress( + val tokenAddress = tokenAddressConverter.convertTokenAddress( networkId, formValues.contractAddress, formValues.symbol, @@ -143,20 +149,54 @@ internal class DefaultCustomTokensRepository( ) } - private fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { - val convertedAddress = when (getBlockchain(networkId)) { - Blockchain.Hedera, - Blockchain.HederaTestnet, - -> HederaTokenAddressConverter().convertToTokenId(contractAddress) - Blockchain.Cardano -> { - // TODO: [REDACTED_JIRA] - CardanoTokenAddressConverter().convertToFingerprint(contractAddress, symbol) + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) = + withContext(dispatchers.io) { + val cryptoCurrency = when (currency) { + is ManagedCryptoCurrency.Custom.Coin -> createCoin(currency.network.id, currency.network.derivationPath) + is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken( + network = currency.network, + rawId = currency.currencyId.rawCurrencyId, + name = currency.name, + symbol = currency.symbol, + decimals = currency.decimals, + contractAddress = currency.contractAddress, + ) + } + + val savedCurrencies = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, + ) + val token = userTokensResponseFactory.createResponseToken(cryptoCurrency) + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }), + ) + when (cryptoCurrency) { + is CryptoCurrency.Coin -> walletManagersFacade.remove(userWalletId, setOf(cryptoCurrency.network)) + is CryptoCurrency.Token -> walletManagersFacade.removeTokens(userWalletId, setOf(cryptoCurrency)) } - else -> contractAddress } - return requireNotNull(convertedAddress) { - "Token contract address is invalid" + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) + + pushTokens(userWalletId, response) + } + + private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) { + Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}") } } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 37d4a334fb..64317a96e8 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -52,12 +52,14 @@ internal object ManageTokensDataModule { tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, appPreferencesStore: AppPreferencesStore, + walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, ): CustomTokensRepository { return DefaultCustomTokensRepository( tangemTechApi, userWalletsStore, appPreferencesStore, + walletManagersFacade, dispatchers, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index c848b36699..5089f51294 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -77,6 +77,7 @@ internal class ManagedCryptoCurrencyFactory { iconUrl = token.id?.let { getIconUrl(it, imageHost) }, contractAddress = contractAddress, network = network, + decimals = token.decimals, ) } } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt new file mode 100644 index 0000000000..9503e4a6c7 --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.managetokens.utils + +import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter +import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.common.currency.getBlockchain +import com.tangem.domain.tokens.model.Network + +internal class TokenAddressesConverter { + private val hederaTokenAddressConverter = HederaTokenAddressConverter() + private val cardanoTokenAddressConverter = CardanoTokenAddressConverter() + + fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { + val convertedAddress = when (getBlockchain(networkId)) { + Blockchain.Hedera, + Blockchain.HederaTestnet, + -> hederaTokenAddressConverter.convertToTokenId(contractAddress) + Blockchain.Cardano -> { + // TODO: [REDACTED_JIRA] + cardanoTokenAddressConverter.convertToFingerprint(contractAddress, symbol) + } + else -> contractAddress + } + + return requireNotNull(convertedAddress) { + "Token contract address is invalid" + } + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 93ad36b66f..8fda73fff1 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -153,15 +153,7 @@ internal class DefaultCurrenciesRepository( val token = userTokensResponseFactory.createResponseToken(currency) storeAndPushTokens( userWalletId = userWalletId, - response = savedCurrencies.copy( - tokens = savedCurrencies.tokens.filterNot { - // it's better to compare by fields, to support renaming and etc - it.contractAddress == token.contractAddress && - it.networkId == token.networkId && - it.derivationPath == token.derivationPath && - it.decimals == token.decimals - }, - ), + response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }), ) } diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index 595a8e1077..848616a5b2 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -25,6 +25,7 @@ sealed class ManagedCryptoCurrency { override val iconUrl: String?, override val network: Network, val contractAddress: String, + val decimals: Int, ) : Custom() data class Coin( diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt new file mode 100644 index 0000000000..61e33c875c --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.wallets.models.UserWalletId + +class RemoveCustomManagedCryptoCurrencyUseCase(private val repository: CustomTokensRepository) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + customCurrency: ManagedCryptoCurrency.Custom, + ): Either { + return Either.catch { + repository.removeCurrency(userWalletId, customCurrency) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index b578627da0..5f84209200 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.managetokens.repository import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -23,11 +24,13 @@ interface CustomTokensRepository { derivationPath: Network.DerivationPath, ): CryptoCurrency.Token? - suspend fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin + fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin suspend fun createCustomToken( networkId: Network.ID, derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token + + suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 92dc53736c..12c248b792 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManageTokensUpdateAction @@ -29,6 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -36,6 +38,7 @@ import javax.inject.Inject internal class ManageTokensListManager @Inject constructor( private val getManagedTokensUseCase: GetManagedTokensUseCase, private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, + private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, ) : ManageTokensUiActions { @@ -160,6 +163,12 @@ internal class ManageTokensListManager @Inject constructor( sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) } + override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) { + scope.launch { + removeCustomCurrencyUseCase.invoke(userWalletId, currency) + } + } + override fun checkNeedToShowRemoveNetworkWarning( currency: ManagedCryptoCurrency.Token, network: Network, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt index 7b9ab64f3f..7b7548289a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -10,6 +10,8 @@ internal interface ManageTokensUiActions { fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) + fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) + fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 3ca2075789..78e3d1011b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -104,7 +104,8 @@ internal class ManageTokensUiManager( network = currency.network, isCoin = currency is ManagedCryptoCurrency.Custom.Coin, onConfirm = { - // TODO: [REDACTED_JIRA] + val userWalletId = requireNotNull(state.value.userWalletId) { "UserWalletId is null. Can not remove" } + actions.removeCustomCurrency(userWalletId = userWalletId, currency = currency) }, ) } From d18d87f41e98062c20bcdbb7866d0023ac770f64 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 18:09:23 +0400 Subject: [PATCH 67/90] Updated on 2026-08-14 --- .../entity/customtoken/CustomTokenFormUM.kt | 2 + .../managetokens/ui/CustomTokenFormContent.kt | 52 +++++++++++++++---- .../managetokens/ui/ManageTokensScreen.kt | 8 +++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index 902c7de226..efa5adecf9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -11,6 +11,7 @@ internal data class CustomTokenFormUM( val tokenForm: TokenFormUM?, val notifications: ImmutableList = persistentListOf(), val canAddToken: Boolean = false, + val isValidating: Boolean = false, val saveToken: () -> Unit, ) { @@ -32,6 +33,7 @@ internal data class TextInputFieldUM( val placeholder: TextReference, val value: String = "", val error: TextReference? = null, + val isEnabled: Boolean = true, val onValueChange: (String) -> Unit, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index 76ae892e76..3565f4c326 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -11,11 +11,11 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -82,6 +82,7 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier .fillMaxWidth(), text = stringResource(id = R.string.custom_token_add_token), enabled = model.canAddToken, + showProgress = model.isValidating, onClick = model.saveToken, ) } @@ -159,14 +160,25 @@ private fun TextField( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, ) { + var isFocused by remember { mutableStateOf(value = false) } + InformationBlock( modifier = modifier, title = { val color by animateColorAsState( - targetValue = if (model.error != null) { - TangemTheme.colors.text.warning - } else { - TangemTheme.colors.text.tertiary + targetValue = when { + !model.isEnabled -> { + TangemTheme.colors.text.disabled + } + model.error != null -> { + TangemTheme.colors.text.warning + } + model.value.isNotBlank() || isFocused -> { + TangemTheme.colors.text.tertiary + } + else -> { + TangemTheme.colors.text.disabled + } }, label = "Field label color", ) @@ -178,14 +190,27 @@ private fun TextField( ) }, content = { + val color by animateColorAsState( + targetValue = if (model.isEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.disabled + }, + label = "Field value color", + ) + SimpleTextField( modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing12) - .fillMaxWidth(), + .fillMaxWidth() + .onFocusChanged { + isFocused = it.isFocused + }, value = model.value, + color = color, onValueChange = model.onValueChange, - readOnly = false, placeholder = model.placeholder, + readOnly = !model.isEnabled && !isFocused, singleLine = true, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, @@ -236,7 +261,16 @@ private class PreviewCustomTokenFormComponentProvider : override val values: Sequence get() = sequenceOf( - PreviewCustomTokenFormComponent(), + PreviewCustomTokenFormComponent( + tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy( + contractAddress = TextInputFieldUM( + label = stringReference("Contract address"), + value = "0x1234567890", + placeholder = stringReference("0x1234567890"), + onValueChange = {}, + ), + ), + ), PreviewCustomTokenFormComponent( tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy( contractAddress = TextInputFieldUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 430896ed3a..9769590b55 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -44,8 +44,10 @@ import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero @@ -93,6 +95,12 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi state = state, ) }, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + hostState = LocalSnackbarHostState.current, + ) + }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { if (state is ManageTokensUM.ManageContent) { From 76300e781976a3a232909a1bf320af49d17a383a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 18:57:51 +0400 Subject: [PATCH 68/90] Updated on 2026-08-14 --- .../impl/DefaultCustomTokenFormComponent.kt | 129 +------- .../PreviewCustomTokenFormComponent.kt | 6 +- .../entity/customtoken/CustomTokenFormUM.kt | 4 +- .../customtoken/CustomTokenFormValues.kt | 14 + .../model/CustomTokenFormModel.kt | 311 ++++++++++++++++++ .../utils/CustomCurrencyValidator.kt | 214 ++++++++++++ .../utils/mapper/TokenFormMapper.kt | 13 + .../utils/ui/CustomCurrencyFormOperations.kt | 155 +++++++++ 8 files changed, 716 insertions(+), 130 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt index 2ca0b7e1a1..f2413c6b88 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt @@ -5,33 +5,24 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.managetokens.component.CustomTokenFormComponent -import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM -import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM -import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues -import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM -import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.model.CustomTokenFormModel import com.tangem.features.managetokens.ui.CustomTokenFormContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update internal class DefaultCustomTokenFormComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: CustomTokenFormComponent.Params, ) : CustomTokenFormComponent, AppComponentContext by context { - private val state: MutableStateFlow = MutableStateFlow( - value = getInitialState(), - ) + private val model: CustomTokenFormModel = getOrCreateModel(params) @Composable override fun Content(modifier: Modifier) { - val state by state.collectAsStateWithLifecycle() + val state by model.state.collectAsStateWithLifecycle() CustomTokenFormContent( modifier = modifier, @@ -39,114 +30,6 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor( ) } - private fun getInitialState(): CustomTokenFormUM { - return CustomTokenFormUM( - networkName = ClickableFieldUM( - label = resourceReference(R.string.custom_token_network_input_title), - value = params.network.name, - onClick = ::selectNetwork, - ), - tokenForm = getInitialTokenForm(), - derivationPath = ClickableFieldUM( - label = resourceReference(R.string.custom_token_derivation_path), - value = params.derivationPath.networkName, - onClick = ::selectDerivationPath, - ), - saveToken = { - // TODO: Save token: [REDACTED_JIRA] - }, - ) - } - - private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM { - val formValues = params.formValues - - val form = CustomTokenFormUM.TokenFormUM( - contractAddress = TextInputFieldUM( - label = resourceReference(R.string.custom_token_contract_address_input_title), - placeholder = stringReference(value = "0x000000000000000000000000000..."), - onValueChange = ::updateContractAddress, - ), - name = TextInputFieldUM( - label = resourceReference(R.string.custom_token_name_input_title), - placeholder = resourceReference(R.string.custom_token_name_input_placeholder), - onValueChange = ::updateTokenName, - ), - symbol = TextInputFieldUM( - label = resourceReference(R.string.custom_token_token_symbol_input_title), - placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder), - onValueChange = ::updateTokenSymbol, - ), - decimals = TextInputFieldUM( - label = resourceReference(R.string.custom_token_decimals_input_title), - placeholder = stringReference(value = "0"), - onValueChange = ::updateDecimals, - ), - ) - - return formValues.fillValues(form) - } - - private fun updateContractAddress(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - contractAddress = tokenForm.contractAddress.copy(value = value), - ), - ) - } - } - - private fun updateTokenName(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - name = tokenForm.name.copy(value = value), - ), - ) - } - } - - private fun updateTokenSymbol(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - symbol = tokenForm.symbol.copy(value = value), - ), - ) - } - } - - private fun updateDecimals(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - decimals = tokenForm.decimals.copy(value = value), - ), - ) - } - } - - private fun selectNetwork() { - params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) - } - - private fun selectDerivationPath() { - params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm)) - } - @AssistedFactory interface Factory : CustomTokenFormComponent.Factory { override fun create( @@ -154,8 +37,4 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor( params: CustomTokenFormComponent.Params, ): DefaultCustomTokenFormComponent } - - private companion object { - const val FORM_VALUES_KEY = "form_values" - } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt index 1c69d487de..fa909e5257 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt @@ -11,7 +11,7 @@ import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.CustomTokenFormContent -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf internal class PreviewCustomTokenFormComponent( @@ -19,7 +19,7 @@ internal class PreviewCustomTokenFormComponent( derivationPath: ClickableFieldUM = PreviewCustomTokenFormComponent.derivationPath, canAddToken: Boolean = false, tokenForm: CustomTokenFormUM.TokenFormUM? = PreviewCustomTokenFormComponent.tokenForm, - notifications: ImmutableList = PreviewCustomTokenFormComponent.notifications, + notifications: PersistentList = PreviewCustomTokenFormComponent.notifications, ) : CustomTokenFormComponent { private val previewState = CustomTokenFormUM( @@ -73,7 +73,7 @@ internal class PreviewCustomTokenFormComponent( onValueChange = {}, ), ) - val notifications: ImmutableList = persistentListOf( + val notifications: PersistentList = persistentListOf( CustomTokenFormUM.NotificationUM( id = "1", config = NotificationConfig( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index efa5adecf9..1fbd8e4de3 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -2,14 +2,14 @@ package com.tangem.features.managetokens.entity.customtoken import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf internal data class CustomTokenFormUM( val networkName: ClickableFieldUM, val derivationPath: ClickableFieldUM, val tokenForm: TokenFormUM?, - val notifications: ImmutableList = persistentListOf(), + val notifications: PersistentList = persistentListOf(), val canAddToken: Boolean = false, val isValidating: Boolean = false, val saveToken: () -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt index 1551b4bc30..983eb8e61a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt @@ -1,5 +1,6 @@ package com.tangem.features.managetokens.entity.customtoken +import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM import kotlinx.serialization.Serializable @@ -28,4 +29,17 @@ internal value class CustomTokenFormValues private constructor(private val value symbol = to.symbol.copy(value = values.getOrElse(index = 2) { "" }), decimals = to.decimals.copy(value = values.getOrElse(index = 3) { "" }), ) + + fun toDomainModel(): AddCustomTokenForm.Raw? { + return if (values.isEmpty()) { + null + } else { + AddCustomTokenForm.Raw( + contractAddress = values.getOrElse(index = 0) { "" }, + name = values.getOrElse(index = 1) { "" }, + symbol = values.getOrElse(index = 2) { "" }, + decimals = values.getOrElse(index = 3) { "" }, + ) + } + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt new file mode 100644 index 0000000000..0565adb15e --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -0,0 +1,311 @@ +package com.tangem.features.managetokens.model + +import androidx.compose.ui.res.stringResource +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.SimpleOkDialog +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.managetokens.component.CustomTokenFormComponent +import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.CustomCurrencyValidator +import com.tangem.features.managetokens.utils.mapper.mapToDomainModel +import com.tangem.features.managetokens.utils.ui.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class CustomTokenFormModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val customCurrencyValidator: CustomCurrencyValidator, + private val messageSender: UiMessageSender, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: CustomTokenFormComponent.Params = paramsContainer.require() + + val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + init { + observeValidatorUpdates() + + if (params.network.canHandleTokens) { + observeTokenFormUpdates() + } else { + modelScope.launch { + customCurrencyValidator.createCoin( + userWalletId = params.userWalletId, + networkId = params.network.id, + derivationPath = params.derivationPath.value, + ) + } + } + } + + private fun getInitialState(): CustomTokenFormUM { + return CustomTokenFormUM( + networkName = ClickableFieldUM( + label = resourceReference(R.string.custom_token_network_input_title), + value = params.network.name, + onClick = ::selectNetwork, + ), + tokenForm = if (params.network.canHandleTokens) { + getInitialTokenForm() + } else { + null + }, + derivationPath = ClickableFieldUM( + label = resourceReference(R.string.custom_token_derivation_path), + value = if (params.derivationPath.id == params.network.id) { + resourceReference(R.string.custom_token_derivation_path_default) + } else { + params.derivationPath.networkName + }, + onClick = ::selectDerivationPath, + ), + saveToken = { + // TODO: Save token: [REDACTED_JIRA] + val dialog = ContentMessage { onDismiss -> + SimpleOkDialog( + message = "Not yet implemented", + onDismissDialog = onDismiss, + ) + } + + messageSender.send(dialog) + }, + ) + } + + @OptIn(FlowPreview::class) + private fun observeTokenFormUpdates() { + state + .sample(periodMillis = 1_000) + .transform { + val values = it.tokenForm?.mapToDomainModel() + + if (values != null) { + emit(values) + } + } + .distinctUntilChanged() + .onEach { formValues -> + customCurrencyValidator.validateForm( + userWalletId = params.userWalletId, + networkId = params.network.id, + derivationPath = params.derivationPath.value, + formValues = formValues, + ) + } + .launchIn(modelScope) + } + + private fun observeValidatorUpdates() = modelScope.launch { + customCurrencyValidator.consumeUpdates { validatorState -> + when (validatorState) { + is CustomCurrencyValidator.State.NotStarted -> Unit + is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching() + is CustomCurrencyValidator.State.UnexpectedException -> showErrorDialog() + is CustomCurrencyValidator.State.FormValidationException -> updateStateWithExceptions( + exceptions = validatorState.exceptions, + ) + is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification() + is CustomCurrencyValidator.State.Validated -> updateStateWithCurrency( + currency = validatorState.currency, + fillForm = validatorState.fillForm, + isAlreadyAdded = validatorState.isAlreadyAdded, + isCustom = validatorState.isCustom, + ) + } + } + } + + private fun updateStateWithCurrency( + currency: CryptoCurrency, + fillForm: Boolean, + isAlreadyAdded: Boolean, + isCustom: Boolean, + ) { + state.update { state -> + var updatedState = state + .updateWithProgress( + showProgress = false, + canAddToken = !isAlreadyAdded, + clearNotifications = true, + clearFieldErrors = true, + disableSecondaryFields = !isCustom, + ) + + if (fillForm) { + updatedState = updatedState.updateWithCurrency(currency) + } + + if (isAlreadyAdded) { + updatedState = updatedState.updateWithCurrencyAlreadyAddedNotification() + } + + if (isCustom) { + updatedState = updatedState.updateWithCurrencyNotFoundNotification() + } + + updatedState + } + } + + private fun updateStateWithNotFoundNotification() { + state.update { state -> + state + .updateWithProgress( + showProgress = false, + canAddToken = false, + clearNotifications = true, + clearFieldErrors = true, + ) + .updateWithCurrencyNotFoundNotification() + } + } + + private fun updateStateWithSearching() { + state.update { state -> + state.updateWithProgress( + showProgress = true, + canAddToken = false, + clearNotifications = false, + clearFieldErrors = false, + ) + } + } + + private fun showErrorDialog() { + val dialog = ContentMessage { onDismiss -> + SimpleOkDialog( + message = stringResource(R.string.common_unknown_error), + onDismissDialog = onDismiss, + ) + } + + messageSender.send(dialog) + } + + private fun updateStateWithExceptions(exceptions: List) { + state.update { state -> + val validatedState = state + .updateWithProgress( + showProgress = false, + canAddToken = false, + clearNotifications = true, + clearFieldErrors = true, + ) + + exceptions.fold(validatedState) { stateAcc, exception -> + when (exception) { + is CustomTokenFormValidationException.ContractAddress -> { + stateAcc.updateWithContractAddressException(exception) + } + is CustomTokenFormValidationException.Decimals -> { + stateAcc.updateWithDecimalsException(exception) + } + is CustomTokenFormValidationException.EmptyName -> { + stateAcc // No need to display error + } + is CustomTokenFormValidationException.EmptySymbol -> { + stateAcc // No need to display error + } + is CustomTokenFormValidationException.DataError -> { + Timber.e(exception.cause, "Unable to validate custom currency") + stateAcc + } + } + } + } + } + + private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM { + val formValues = params.formValues + + val form = CustomTokenFormUM.TokenFormUM( + contractAddress = TextInputFieldUM( + label = resourceReference(R.string.custom_token_contract_address_input_title), + placeholder = stringReference(CONTRACT_ADDRESS_PLACEHOLDER), + onValueChange = ::updateContractAddress, + ), + name = TextInputFieldUM( + label = resourceReference(R.string.custom_token_name_input_title), + placeholder = resourceReference(R.string.custom_token_name_input_placeholder), + onValueChange = ::updateTokenName, + ), + symbol = TextInputFieldUM( + label = resourceReference(R.string.custom_token_token_symbol_input_title), + placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder), + onValueChange = ::updateTokenSymbol, + ), + decimals = TextInputFieldUM( + label = resourceReference(R.string.custom_token_decimals_input_title), + placeholder = stringReference(DECIMALS_PLACEHOLDER), + onValueChange = ::updateDecimals, + ), + ) + + return formValues.fillValues(form) + } + + private fun updateContractAddress(value: String) { + state.update { state -> + state.updateTokenForm { + copy(contractAddress = contractAddress.updateValue(value)) + } + } + } + + private fun updateTokenName(value: String) { + state.update { state -> + state.updateTokenForm { + copy(name = name.updateValue(value)) + } + } + } + + private fun updateTokenSymbol(value: String) { + state.update { state -> + state.updateTokenForm { + copy(symbol = symbol.updateValue(value)) + } + } + } + + private fun updateDecimals(value: String) { + state.update { state -> + state.updateTokenForm { + copy(decimals = decimals.updateValue(value)) + } + } + } + + private fun selectNetwork() { + params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) + } + + private fun selectDerivationPath() { + params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm)) + } + + private companion object { + const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..." + const val DECIMALS_PLACEHOLDER = "0" + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt new file mode 100644 index 0000000000..373b4640c7 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -0,0 +1,214 @@ +package com.tangem.features.managetokens.utils + +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase +import com.tangem.domain.managetokens.CreateCurrencyUseCase +import com.tangem.domain.managetokens.FindTokenUseCase +import com.tangem.domain.managetokens.ValidateTokenFormUseCase +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.managetokens.model.exceptoin.FindTokenException +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class CustomCurrencyValidator @Inject constructor( + private val validateTokenFormUseCase: ValidateTokenFormUseCase, + private val createCustomCurrencyUseCase: CreateCurrencyUseCase, + private val findTokenUseCase: FindTokenUseCase, + private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, +) { + + private val state: MutableStateFlow = MutableStateFlow(State.NotStarted) + + suspend fun consumeUpdates(block: suspend (State) -> Unit) { + state.collectLatest { s -> + block(s) + } + } + + suspend fun validateForm( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Raw, + ) { + val result = validateTokenFormUseCase( + networkId = networkId, + formValues = formValues, + ) + + val validatedForm = result.getOrElse { e -> + state.value = State.FormValidationException(e) + return + } + + when (validatedForm) { + is AddCustomTokenForm.Validated.All -> { + findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm) + } + is AddCustomTokenForm.Validated.ContractAddress -> { + findToken(userWalletId, networkId, derivationPath, validatedForm) + } + } + } + + suspend fun createCoin(userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath) { + createCurrency(userWalletId, networkId, derivationPath, validatedForm = null) + } + + private suspend fun findOrCreateCurrency( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + validatedForm: AddCustomTokenForm.Validated.All, + ) { + state.value = State.SearchingToken + + val foundToken = findTokenUseCase( + userWalletId = userWalletId, + contractAddress = validatedForm.contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ).getOrElse { e -> + when (e) { + is FindTokenException.DataError -> { + Timber.e(e.cause, "Unable to find custom currency") + state.value = State.UnexpectedException(e.cause) + return + } + is FindTokenException.NotFound -> { + null + } + } + } + + if (foundToken != null) { + updateStateToValidated(userWalletId, foundToken, fillForm = true, isCustom = false) + } else { + createCurrency(userWalletId, networkId, derivationPath, validatedForm) + } + } + + private suspend fun findToken( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + validatedForm: AddCustomTokenForm.Validated.ContractAddress, + ) { + state.value = State.SearchingToken + + val token = findTokenUseCase( + userWalletId = userWalletId, + contractAddress = validatedForm.contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ).getOrElse { e -> + state.value = when (e) { + is FindTokenException.DataError -> { + Timber.e(e.cause, "Unable to find custom currency") + State.UnexpectedException(e.cause) + } + is FindTokenException.NotFound -> { + State.TokenNotFound + } + } + + return + } + + updateStateToValidated(userWalletId, token, fillForm = true, isCustom = false) + } + + private suspend fun createCurrency( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + validatedForm: AddCustomTokenForm.Validated.All?, + ) { + val currency = createCustomCurrencyUseCase( + networkId = networkId, + derivationPath = derivationPath, + formValues = validatedForm, + ).getOrElse { e -> + Timber.e(e, "Unable to create custom currency") + state.value = State.UnexpectedException(e) + return + } + + updateStateToValidated(userWalletId, currency, fillForm = false, isCustom = validatedForm != null) + } + + private suspend fun updateStateToValidated( + userWalletId: UserWalletId, + currency: CryptoCurrency, + fillForm: Boolean, + isCustom: Boolean, + ) { + val isNotAdded = checkIsCurrencyNotAddedUseCase( + userWalletId = userWalletId, + networkId = currency.network.id, + derivationPath = currency.network.derivationPath, + contractAddress = when (currency) { + is CryptoCurrency.Coin -> null + is CryptoCurrency.Token -> currency.contractAddress + }, + ).getOrElse { e -> + Timber.e(e, "Unable to check if currency is already added") + state.value = State.UnexpectedException(e) + + return + } + + state.value = State.Validated( + currency = currency, + fillForm = fillForm, + isAlreadyAdded = !isNotAdded, + isCustom = isCustom, + ) + } + + sealed class State { + + abstract val isFinished: Boolean + + data object NotStarted : State() { + override val isFinished: Boolean = false + } + + data object SearchingToken : State() { + override val isFinished: Boolean = false + } + + data class Validated( + val currency: CryptoCurrency, + val fillForm: Boolean, + val isAlreadyAdded: Boolean, + val isCustom: Boolean, + ) : State() { + override val isFinished: Boolean = true + } + + data class FormValidationException( + val exceptions: List, + ) : State() { + override val isFinished: Boolean = true + } + + data object TokenNotFound : State() { + override val isFinished: Boolean = true + } + + data class UnexpectedException( + val cause: Throwable, + ) : State() { + override val isFinished: Boolean = true + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt new file mode 100644 index 0000000000..fd9a7bd140 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt @@ -0,0 +1,13 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM + +internal fun CustomTokenFormUM.TokenFormUM.mapToDomainModel(): AddCustomTokenForm.Raw { + return AddCustomTokenForm.Raw( + contractAddress = contractAddress.value, + symbol = symbol.value, + name = name.value, + decimals = decimals.value, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt new file mode 100644 index 0000000000..71ecd3cd8d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt @@ -0,0 +1,155 @@ +package com.tangem.features.managetokens.utils.ui + +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.domain.managetokens.ValidateTokenFormUseCase +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf + +internal fun CustomTokenFormUM.updateTokenForm( + block: CustomTokenFormUM.TokenFormUM.() -> CustomTokenFormUM.TokenFormUM, +): CustomTokenFormUM { + val form = tokenForm ?: return this + + val updatedForm = form.block() + + return copy(tokenForm = updatedForm) +} + +internal fun TextInputFieldUM.updateValue( + value: String = this.value, + error: TextReference? = this.error, + isEnabled: Boolean = this.isEnabled, + clearError: Boolean = false, +): TextInputFieldUM { + return copy( + value = value, + error = if (clearError) null else error, + isEnabled = isEnabled, + ) +} + +internal fun CustomTokenFormUM.updateWithProgress( + showProgress: Boolean, + canAddToken: Boolean = this.canAddToken, + clearNotifications: Boolean = false, + clearFieldErrors: Boolean = false, + disableSecondaryFields: Boolean = false, +): CustomTokenFormUM { + return copy( + isValidating = showProgress, + canAddToken = canAddToken, + notifications = if (clearNotifications) persistentListOf() else notifications, + ).updateTokenForm { + copy( + contractAddress = contractAddress.updateValue( + clearError = clearFieldErrors, + ), + name = name.updateValue( + isEnabled = !showProgress && !disableSecondaryFields, + clearError = clearFieldErrors, + ), + symbol = symbol.updateValue( + isEnabled = !showProgress && !disableSecondaryFields, + clearError = clearFieldErrors, + ), + decimals = decimals.updateValue( + isEnabled = !showProgress && !disableSecondaryFields, + clearError = clearFieldErrors, + ), + ) + } +} + +internal fun CustomTokenFormUM.updateWithCurrency(currency: CryptoCurrency): CustomTokenFormUM { + return updateTokenForm { + copy( + contractAddress = contractAddress.updateValue(error = null), + name = name.updateValue(currency.name), + symbol = symbol.updateValue(currency.symbol), + decimals = decimals.updateValue(currency.decimals.toString()), + ) + } +} + +internal fun CustomTokenFormUM.updateWithContractAddressException( + exception: CustomTokenFormValidationException.ContractAddress, +): CustomTokenFormUM { + return updateTokenForm { + copy( + contractAddress = contractAddress.updateValue( + error = when (exception) { + CustomTokenFormValidationException.ContractAddress.Empty -> { + null + } + CustomTokenFormValidationException.ContractAddress.Invalid -> { + resourceReference(R.string.custom_token_creation_error_invalid_contract_address) + } + }, + ), + ) + } +} + +internal fun CustomTokenFormUM.updateWithDecimalsException( + exception: CustomTokenFormValidationException.Decimals, +): CustomTokenFormUM { + return updateTokenForm { + copy( + decimals = decimals.updateValue( + error = when (exception) { + is CustomTokenFormValidationException.Decimals.Empty -> { + null + } + is CustomTokenFormValidationException.Decimals.Invalid -> { + resourceReference( + R.string.custom_token_creation_error_wrong_decimals, + wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), + ) + } + }, + ), + ) + } +} + +internal fun CustomTokenFormUM.updateWithCurrencyNotFoundNotification(): CustomTokenFormUM { + val notification = CustomTokenFormUM.NotificationUM( + id = "currency_not_found", + config = NotificationConfig( + title = resourceReference(R.string.custom_token_validation_error_not_found_title), + subtitle = resourceReference(R.string.custom_token_validation_error_not_found_description), + iconResId = R.drawable.img_attention_20, + ), + ) + + return copy( + notifications = notifications.mutate { + it.add(notification) + }, + ) +} + +internal fun CustomTokenFormUM.updateWithCurrencyAlreadyAddedNotification(): CustomTokenFormUM { + val notification = CustomTokenFormUM.NotificationUM( + id = "currency_already_added", + config = NotificationConfig( + title = resourceReference(R.string.custom_token_creation_error_token_already_exist_title), + subtitle = resourceReference(R.string.custom_token_creation_error_token_already_exist_message), + iconResId = R.drawable.img_attention_20, + ), + ) + + return copy( + notifications = notifications.mutate { + it.add(notification) + }, + ) +} \ No newline at end of file From 6cd6210a05d137805da29eab42c09ae15aa33190 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 16:16:48 +0400 Subject: [PATCH 69/90] Updated on 2026-08-14 --- .../DefaultMarketsTokenDetailsComponent.kt | 2 +- .../api/MarketsPortfolioComponent.kt | 3 +- .../impl/model/MarketsPortfolioModel.kt | 167 +++++++++++++----- .../impl/model/MyPortfolioUMMFactory.kt | 156 ++++++++++++---- .../impl/model/PortfolioTokenUMConverter.kt | 4 +- .../impl/model/TokensPortfolioUMConverter.kt | 52 +++--- .../markets/portfolio/impl/ui/MyPortfolio.kt | 6 +- .../preview/PreviewMyPortfolioUMProvider.kt | 22 +++ .../portfolio/impl/ui/state/MyPortfolioUM.kt | 9 +- 9 files changed, 292 insertions(+), 129 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index a1ea813365..0d758ce282 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -35,7 +35,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( private val portfolioComponent = portfolioComponentFactory.create( context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params(params.token.id), + params = MarketsPortfolioComponent.Params(params.token), ) init { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt index e23fee1426..c736781c03 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt @@ -4,13 +4,14 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams import kotlinx.serialization.Serializable @Stable interface MarketsPortfolioComponent : ComposableContentComponent { @Serializable - data class Params(val tokenId: String) + data class Params(val token: TokenMarketParams) fun setTokenNetworks(networks: List) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 83fd674f85..219da20190 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -5,13 +5,12 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList @@ -23,9 +22,8 @@ import javax.inject.Inject @ComponentScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, - getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val portfolioDataLoader: PortfolioDataLoader, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -34,59 +32,134 @@ internal class MarketsPortfolioModel @Inject constructor( private val params = paramsContainer.require() - private val availableNetworks = MutableStateFlow?>(value = null) + private val availableNetworksFlow = MutableStateFlow?>(value = null) - private val factory = MyPortfolioUMMFactory(::onAddClick, ::onTokenItemClick) + /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ + private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) + + /** Map of [UserWalletId] and network ids that user is changed */ + private val walletsWithChangedNetworksFlow = MutableStateFlow>>(value = emptyMap()) + + private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) + + private val factory = MyPortfolioUMMFactory( + onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) }, + onTokenItemClick = ::onTokenItemClick, + addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( + token = params.token, + onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, + onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, + onNetworkSwitchClick = ::onNetworkSwitchClick, + onWalletSelect = ::onWalletSelect, + onContinueClick = ::onContinueClick, + ), + ) init { - combine( - flow = getAllWalletsCryptoCurrencyStatusesUseCase(params.tokenId), - flow2 = availableNetworks, - flow3 = getSelectedAppCurrencyUseCase(), - flow4 = getBalanceHidingSettingsUseCase(), - ) { walletsWithCurrencyStatuses, networks, maybeAppCurrency, isBalanceHidden -> - val appCurrency = maybeAppCurrency.getOrElse { e -> - Timber.e("Failed to load app currency: $e") - AppCurrency.Default - } + // Subscribe on selected wallet flow to support actual selected wallet + subscribeOnSelectedMultiWalletUpdates() - factory.create( - walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, - availableNetworks = networks, - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden.isBalanceHidden, - ) - } + subscribeOnStateUpdates() + } + + fun setTokenNetworks(networks: List) { + availableNetworksFlow.value = networks + } + + fun setNoNetworksAvailable() { + availableNetworksFlow.value = emptyList() + } + + private fun subscribeOnSelectedMultiWalletUpdates() { + getSelectedWalletUseCase() + .getOrElse { e -> + Timber.e("Failed to load selected wallet: $e") + error("Failed to load selected wallet") + } + .onEach { + selectedMultiWalletIdFlow.value = it.takeIf { it.isMultiCurrency }?.walletId + } + .launchIn(modelScope) + } + + private fun subscribeOnStateUpdates() { + combine( + flow = portfolioDataLoader.load(params.token.id), + flow2 = portfolioBSVisibilityModelFlow, + flow3 = availableNetworksFlow, + flow4 = selectedMultiWalletIdFlow, + flow5 = walletsWithChangedNetworksFlow, + transform = factory::create, + ) .onEach { _state.value = it } .launchIn(modelScope) } - fun setTokenNetworks(networks: List) { - availableNetworks.value = networks - } - - fun setNoNetworksAvailable() { - availableNetworks.value = emptyList() - } - - private fun onAddClick() { - // TODO [REDACTED_JIRA] - } - - private fun onTokenItemClick(token: CryptoCurrencyStatus) { + private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) { _state.update { - (_state.value as? MyPortfolioUM.Tokens)?.let { state -> - state.copy( - tokens = state.tokens.map { tokenUM -> - if (tokenUM.tokenItemState.id == token.currency.id.value) { + val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it + + state.copy( + tokens = state.tokens + .mapIndexed { i, tokenUM -> + // check by index and id because id can be the same + if (index == i && id.value == tokenUM.tokenItemState.id) { tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown) } else { tokenUM } } - .toImmutableList(), - ) - } ?: it + .toImmutableList(), + ) + } + } + + private fun onNetworkSwitchClick(id: String, isChecked: Boolean) { + walletsWithChangedNetworksFlow.update { + val selectedWalletId = selectedMultiWalletIdFlow.value + + if (selectedWalletId == null) { + Timber.e("Impossible ti switch network when selected wallet is null") + return@update it + } + + it.toMutableMap().apply { + val networkIds = this[selectedWalletId] ?: emptyList() + + if (isChecked) { + this[selectedWalletId] = networkIds + id + } else { + this[selectedWalletId] = networkIds - id + } + } + } + } + + private fun onWalletSelect(userWalletId: UserWalletId) { + selectedMultiWalletIdFlow.update { prevUserWalletId -> + + // Clear user changes if user select another wallet + walletsWithChangedNetworksFlow.update { + it.toMutableMap().apply { remove(prevUserWalletId) } + } + + userWalletId + } + } + + private fun onContinueClick() { + // TODO [REDACTED_JIRA] + } + + private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { + portfolioBSVisibilityModelFlow.update { + it.copy(addToPortfolioBSVisibility = isShow, walletSelectorBSVisibility = false) + } + } + + private fun onWalletSelectorVisibilityChange(isShow: Boolean) { + portfolioBSVisibilityModelFlow.update { + it.copy(addToPortfolioBSVisibility = true, walletSelectorBSVisibility = isShow) } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt index bd9042cac0..91072c3906 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt @@ -1,11 +1,12 @@ package com.tangem.features.markets.portfolio.impl.model -import arrow.core.Either -import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM /** @@ -18,63 +19,142 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM */ internal class MyPortfolioUMMFactory( private val onAddClick: () -> Unit, - private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, + private val onTokenItemClick: (Int, CryptoCurrency.ID) -> Unit, + private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, ) { - /** - * Create [MyPortfolioUM] from data set - * - * @param walletsWithCurrencyStatuses wallets with currency statuses - * @param availableNetworks available networks - * @param appCurrency app currency - * @param isBalanceHidden flag that indicates if balance should be hidden - */ fun create( - walletsWithCurrencyStatuses: Map>>, + portfolioModel: PortfolioData, + portfolioBSVisibilityModel: PortfolioBSVisibilityModel, availableNetworks: List?, - appCurrency: AppCurrency, - isBalanceHidden: Boolean, + selectedWalletId: UserWalletId?, + walletsWithChangedNetworks: Map>, ): MyPortfolioUM { if (availableNetworks == null) return MyPortfolioUM.Loading if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable - val availableWalletsWithStatuses = walletsWithCurrencyStatuses - .filterAvailable(networks = availableNetworks) - .flatMapStatuses() + val walletsWithStatuses = portfolioModel.walletsWithCurrencyStatuses + .filterAvailableNetworks(networks = availableNetworks) - if (availableWalletsWithStatuses.isEmpty()) { - return MyPortfolioUM.AddFirstToken(onAddClick = onAddClick) + val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty() + if (isPortfolioEmpty) { + val hasMultiWallets = walletsWithStatuses.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() + + return if (hasMultiWallets) { + MyPortfolioUM.AddFirstToken( + bsConfig = createAddToPortfolioBSConfig( + portfolioModel = portfolioModel, + selectedWalletId = selectedWalletId, + availableNetworks = availableNetworks, + walletsWithChangedNetworks = walletsWithChangedNetworks, + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + ), + onAddClick = onAddClick, + ) + } else { + MyPortfolioUM.Unavailable + } } return TokensPortfolioUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - availableNetworks = availableNetworks, + appCurrency = portfolioModel.appCurrency, + isBalanceHidden = portfolioModel.isBalanceHidden, + isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks), + bsConfig = createAddToPortfolioBSConfig( + portfolioModel = portfolioModel, + selectedWalletId = selectedWalletId, + availableNetworks = availableNetworks, + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + walletsWithChangedNetworks = walletsWithChangedNetworks, + ), onAddClick = onAddClick, onTokenItemClick = onTokenItemClick, ) - .convert(availableWalletsWithStatuses) + .convert(walletsWithStatuses) } - private fun Map>>.filterAvailable( - networks: List, - ): Map> { - val networkIds = networks.map { it.networkId } + private fun createAddToPortfolioBSConfig( + portfolioModel: PortfolioData, + selectedWalletId: UserWalletId?, + availableNetworks: List, + portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + walletsWithChangedNetworks: Map>, + ): TangemBottomSheetConfig { + val walletId = requireNotNull(selectedWalletId) { + "Selected wallet must be not null in state with AddToPortfolio bottom sheet" + } - return mapValues { entry -> - entry.value.mapNotNull { maybeStatus -> - maybeStatus.getOrNull() - ?.takeIf { networkIds.contains(it.currency.network.backendId) } - } + val selectedWallet = portfolioModel.walletsWithCurrencyStatuses.keys + .firstOrNull { it.walletId == walletId } + ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") + + val changedNetworks = walletsWithChangedNetworks[selectedWalletId] + val alreadyAddedNetworks = requireNotNull( + value = portfolioModel.walletsWithCurrencyStatuses[selectedWallet], + lazyMessage = { + "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" + }, + ) + .filterAvailableNetworks(availableNetworks) + .map { it.currency.network.backendId } + + return addToPortfolioBSContentUMFactory.create( + portfolioData = portfolioModel, + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + selectedWallet = selectedWallet, + networksWithToggle = availableNetworks.associateWithToggle( + changedNetworks = changedNetworks, + alreadyAddedNetworks = alreadyAddedNetworks, + ), + isUserChangedNetworks = changedNetworks != null && alreadyAddedNetworks != changedNetworks, + ) + } + + private fun List.associateWithToggle( + changedNetworks: List?, + alreadyAddedNetworks: List, + ): Map { + // Use user choice or check already added networks + return associateWith { network -> + val isSelectedByUser = changedNetworks?.contains(network.networkId) + + if (isSelectedByUser != null) return@associateWith isSelectedByUser + + val isAlreadyAdded = alreadyAddedNetworks.any { it == network.networkId } + + isAlreadyAdded } } - private fun Map>.flatMapStatuses(): Map { + private fun Map>.isAllAvailableNetworksAdded( + availableNetworks: List, + ): Boolean { + val networkIds = availableNetworks.map { it.networkId } + return this - .flatMap { entry -> - entry.value.map { entry.key to it } - } - .toMap() + // User can add currencies only in multi-currency wallets + .filterKeys(UserWallet::isMultiCurrency) + .mapValues { it.value.map { it.currency.network.backendId } } + // Each wallets contains all available networks? + .all { it.value.containsAll(networkIds) } + } + + /** Filter map values by available networks [networks] */ + private fun Map>.filterAvailableNetworks( + networks: List, + ): Map> { + return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } + } + + /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ + private fun List.filterAvailableNetworks( + networks: List, + ): List { + val networkIds = networks.map(TokenMarketInfo.Network::networkId) + + return mapNotNull { + it.takeIf { networkIds.contains(it.currency.network.backendId) } + } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index d299172177..5e05440a85 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -17,9 +17,9 @@ internal class PortfolioTokenUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, -) : Converter, PortfolioTokenUM> { +) : Converter, PortfolioTokenUM> { - override fun convert(value: Map.Entry): PortfolioTokenUM { + override fun convert(value: Pair): PortfolioTokenUM { val (userWallet, cryptoCurrencyStatus) = value val tokenItemStateConverter = TokenItemStateConverter( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index aa3b90ea8f..e9e61f5618 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -1,7 +1,8 @@ package com.tangem.features.markets.portfolio.impl.model +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @@ -17,42 +18,29 @@ import kotlinx.collections.immutable.toImmutableList internal class TokensPortfolioUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, - private val availableNetworks: List, + private val isAllAvailableNetworksAdded: Boolean, + private val bsConfig: TangemBottomSheetConfig, private val onAddClick: () -> Unit, - private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, -) : Converter, MyPortfolioUM.Tokens> { + private val onTokenItemClick: (index: Int, id: CryptoCurrency.ID) -> Unit, +) : Converter>, MyPortfolioUM.Tokens> { - override fun convert(value: Map): MyPortfolioUM.Tokens { + override fun convert(value: Map>): MyPortfolioUM.Tokens { return MyPortfolioUM.Tokens( - tokens = PortfolioTokenUMConverter(appCurrency, isBalanceHidden, onTokenItemClick) - .convertList(value.entries) + tokens = value + .flatMap { entry -> + entry.value.map { entry.key to it } + } + .mapIndexed { index, status -> + PortfolioTokenUMConverter( + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden, + onTokenItemClick = { onTokenItemClick(index, it.currency.id) }, + ).convert(status) + } .toImmutableList(), - buttonState = getAddButtonState( - availableWalletsWithStatuses = value, - availableNetworks = availableNetworks, - ), + buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available, + bsConfig = bsConfig, onAddClick = onAddClick, ) } - - private fun getAddButtonState( - availableWalletsWithStatuses: Map, - availableNetworks: List, - ): AddButtonState { - val networkIds = availableNetworks.map { it.networkId } - - val allNetworksAdded = availableWalletsWithStatuses - // User can add currencies only in multi-currency wallets - .filterKeys(UserWallet::isMultiCurrency) - .entries - .groupBy( - keySelector = { it.key }, - valueTransform = { it.value.currency.network.backendId }, - ) - .mapValues { it.value.toSet() } - // Each wallets contains all available networks? - .all { it.value.containsAll(networkIds) } - - return if (allNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available - } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 6f5c6f01db..8c3b6118fd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -53,7 +53,11 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { } } - AddToPortfolioBottomSheet(config = state.addToPortfolioBS) + when (state) { + is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.bsConfig) + is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.bsConfig) + else -> Unit + } } @Composable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 1f21b46f5b..02339b5257 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -1,6 +1,8 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @@ -14,19 +16,39 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider, val buttonState: AddButtonState, + val bsConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() { @@ -28,6 +22,7 @@ internal sealed class MyPortfolioUM { } data class AddFirstToken( + val bsConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() From ab4ed3efc6c6e79e402481603e5302da3bcb7c47 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 21:13:32 +0400 Subject: [PATCH 70/90] Updated on 2026-08-14 --- .../block/information/InformationBlock.kt | 4 +- .../ui/CustomTokenSelectorContent.kt | 84 +++++++++---------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 4565c520d9..25bac7feca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R @@ -32,12 +33,13 @@ fun InformationBlock( title: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12, + shape: Shape = TangemTheme.shapes.roundedCornersXMedium, action: (@Composable BoxScope.() -> Unit)? = null, content: (@Composable InformationBlockContentScope.() -> Unit)? = null, ) { Column( modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) + .clip(shape) .background(color = TangemTheme.colors.background.action), horizontalAlignment = Alignment.Start, ) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 67c77ff303..5984b55cf3 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.ChainRowUM @@ -34,7 +35,6 @@ import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath -import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.entity.item.DerivationPathUM import com.tangem.features.managetokens.impl.R @@ -46,7 +46,9 @@ internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier: val lastIndex = model.items.lastIndex LazyColumn( - modifier = modifier, + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + ), contentPadding = PaddingValues( bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, ), @@ -178,38 +180,47 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) @Composable private fun CustomDerivationButton(onClick: () -> Unit, modifier: Modifier = Modifier) { - // TODO: Implement in [REDACTED_JIRA] - Box( - modifier = modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size56) - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .background(color = TangemTheme.colors.background.primary) - .clickable(onClick = onClick) - .padding(all = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Text( - text = "Custom", - color = TangemTheme.colors.text.primary1, - ) - } + InformationBlock( + modifier = modifier.clickable(onClick = onClick), + title = { + Text( + text = stringResource(id = R.string.custom_token_custom_derivation), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + }, + content = { + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = stringResource(id = R.string.custom_token_custom_derivation_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + }, + ) } @Composable private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Modifier) { - // TODO: Implement in [REDACTED_JIRA] - Box( - modifier = modifier - .heightIn(TangemTheme.dimens.size56) - .padding(all = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Text( - text = "${model.networkName.resolveReference()}: ${model.value} ${if (model.isSelected) "<" else ""}", - color = TangemTheme.colors.text.primary1, - ) - } + InformationBlock( + modifier = modifier, + shape = RectangleShape, + title = { + Text( + text = model.networkName.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + }, + content = { + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = model.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + }, + ) } // region Preview @@ -229,19 +240,6 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - PreviewCustomTokenSelectorComponent(), - PreviewCustomTokenSelectorComponent( - params = CustomTokenSelectorComponent.Params.NetworkSelector( - userWalletId = UserWalletId(stringValue = "321"), - selectedNetwork = SelectedNetwork( - id = Network.ID(value = "0"), - name = stringReference(""), - derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), - canHandleTokens = false, - ), - onNetworkSelected = {}, - ), - ), PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = UserWalletId(stringValue = "321"), From 80e0e5988e6aac57e339a6ec1da2e0e500e632d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 21:14:07 +0400 Subject: [PATCH 71/90] Updated on 2026-08-14 --- .../CustomTokenDerivationInputComponent.kt | 16 +++++ ...viewCustomTokenDerivationInputComponent.kt | 34 +++++++++ .../customtoken/CustomDerivationInputUM.kt | 12 ++++ .../ui/dialog/CustomDerivationInputDialog.kt | 71 +++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt new file mode 100644 index 0000000000..1530c3d205 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.managetokens.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.domain.wallets.models.UserWalletId + +internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent { + + data class Params( + val userWalletId: UserWalletId, + val onConfirm: (String) -> Unit, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt new file mode 100644 index 0000000000..a91caf00e0 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent +import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM +import com.tangem.features.managetokens.ui.dialog.CustomDerivationInputDialog + +internal class PreviewCustomTokenDerivationInputComponent( + private val value: String = "", + private val error: String? = null, +) : CustomTokenDerivationInputComponent { + + override fun dismiss() { + /* no-op */ + } + + @Composable + override fun Dialog() { + val model = CustomDerivationInputUM( + value = TextFieldValue(value), + error = error?.let(::stringReference), + updateValue = {}, + isConfirmEnabled = false, + onConfirm = {}, + ) + + CustomDerivationInputDialog( + model = model, + onDismiss = ::dismiss, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt new file mode 100644 index 0000000000..0a0e28431a --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.managetokens.entity.customtoken + +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.extensions.TextReference + +internal data class CustomDerivationInputUM( + val value: TextFieldValue, + val error: TextReference? = null, + val updateValue: (value: TextFieldValue) -> Unit, + val isConfirmEnabled: Boolean, + val onConfirm: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt new file mode 100644 index 0000000000..be41894576 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt @@ -0,0 +1,71 @@ +package com.tangem.features.managetokens.ui.dialog + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.AdditionalTextInputDialogUM +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent +import com.tangem.features.managetokens.component.preview.PreviewCustomTokenDerivationInputComponent +import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun CustomDerivationInputDialog(model: CustomDerivationInputUM, onDismiss: () -> Unit) { + val value by rememberUpdatedState(newValue = model.value) + + TextInputDialog( + title = stringResource(id = R.string.custom_token_custom_derivation_title), + fieldValue = value, + confirmButton = DialogButtonUM( + title = stringResource(id = R.string.common_ok), + enabled = model.isConfirmEnabled, + onClick = model.onConfirm, + ), + dismissButton = DialogButtonUM( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + onValueChange = model.updateValue, + textFieldParams = AdditionalTextInputDialogUM( + label = model.error?.resolveReference() ?: stringResource(id = R.string.custom_token_derivation_path), + isError = model.error != null, + ), + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CustomDerivationInputDialog( + @PreviewParameter(ComponentPreviewProvider::class) component: CustomTokenDerivationInputComponent, +) { + TangemThemePreview { + component.Dialog() + } +} + +private class ComponentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewCustomTokenDerivationInputComponent(), + PreviewCustomTokenDerivationInputComponent( + value = "m/44'/60'/0'/0/0", + ), + PreviewCustomTokenDerivationInputComponent( + value = "m/44'/60'/0'/0/0", + error = "Invalid derivation path", + ), + ) +} +// endregion Preview \ No newline at end of file From c10f747de8d8ec3247d86a47f301822a89e242c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 18:05:46 +0500 Subject: [PATCH 72/90] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../common/ui/notifications/NotificationUM.kt | 251 ++++++++++++ .../ui/notifications/NotificationsFactory.kt | 362 ++++++++++++++++++ .../domain/transaction/error/GetFeeError.kt | 0 .../ui/block/NotificationsBlock.kt | 12 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 + 6 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt rename domain/transaction/{src/main/java => models/src/main/kotlin}/com/tangem/domain/transaction/error/GetFeeError.kt (100%) diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 9380922c04..315574334e 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt new file mode 100644 index 0000000000..20dad94e6b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -0,0 +1,251 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.ui.R +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.core.ui.utils.BigDecimalFormatter +import java.math.BigDecimal + +sealed class NotificationUM(val config: NotificationConfig) { + + open class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonState, + onCloseClick = onCloseClick, + ), + ) { + + data object TotalExceedsBalance : Error( + title = resourceReference(R.string.send_notification_exceed_balance_title), + subtitle = resourceReference(R.string.send_notification_exceed_balance_text), + ) + + data object InvalidAmount : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference(R.string.send_notification_invalid_amount_text), + ) + + data class MinimumAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference( + R.string.send_notification_invalid_minimum_amount_text, + wrappedList(amount, amount), + ), + ) + + data class TransactionLimitError( + val cryptoCurrency: String, + val utxoLimit: String, + val amountLimit: String, + val onConfirmClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.send_notification_transaction_limit_title), + subtitle = resourceReference( + R.string.send_notification_transaction_limit_text, + wrappedList(cryptoCurrency, utxoLimit, amountLimit), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)), + onClick = onConfirmClick, + ), + ) + + data class ExceedsBalance( + val networkIconId: Int, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val mergeFeeNetworkName: Boolean = false, + val onClick: (() -> Unit)? = null, + ) : Error( + title = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_title, + wrappedList(feeName), + ), + subtitle = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), + ), + iconResId = networkIconId, + buttonState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference( + R.string.common_buy_currency, + wrappedList( + if (mergeFeeNetworkName) { + "$currencyName ($feeSymbol)" + } else { + feeName + }, + ), + ), + onClick = onClick, + ) + }, + ) + + data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error( + title = resourceReference(R.string.send_notification_existential_deposit_title), + subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), + onClick = onConfirmClick, + ), + ) + + data class ReserveAmount(val amount: String) : Error( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + wrappedList(amount), + ), + subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), + ) + } + + open class Warning( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.img_attention_20, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ), + ) { + data class HighFeeError( + val currencyName: String, + val amount: String, + val onConfirmClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), + onClick = onConfirmClick, + ), + onCloseClick = onCloseClick, + ) + + data object FeeTooLow : Warning( + title = resourceReference(id = R.string.send_notification_transaction_delay_title), + subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), + ) + + data class TooHigh( + val value: String, + ) : Warning( + title = resourceReference(id = R.string.send_notification_fee_too_high_title), + subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), + ) + + 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, + ), + ) + + data class TronAccountNotActivated(val tokenName: String) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference( + R.string.send_tron_account_activation_error, + wrappedList(tokenName), + ), + ) + + data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( + title = resourceReference(R.string.send_network_fee_warning_title), + subtitle = resourceReference( + R.string.common_network_fee_warning_content, + wrappedList(cryptoAmount, fiatAmount), + ), + ) + } + + sealed interface Cardano { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + ) + + data object InsufficientBalanceToTransferCoin : Error( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + ) + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Error( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + ) + } + + sealed interface Koinos { + data class InsufficientRecoverableMana( + val mana: BigDecimal, + val maxMana: BigDecimal, + ) : Error( + title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title), + subtitle = resourceReference( + R.string.koinos_insufficient_mana_to_send_koin_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()), + BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()), + ), + ), + ) + + data object InsufficientBalance : Error( + title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title), + subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description), + ) + + data class ManaExceedsBalance( + val availableKoinForTransfer: BigDecimal, + val onReduceClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title), + subtitle = resourceReference( + R.string.koinos_mana_exceeds_koin_balance_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmount( + availableKoinForTransfer, + Blockchain.Koinos.currency, + Blockchain.Koinos.decimals(), + ), + ), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)), + onClick = onReduceClick, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt new file mode 100644 index 0000000000..526007b989 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -0,0 +1,362 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.error.GetFeeError +import java.math.BigDecimal + +@Suppress("LargeClass") +object NotificationsFactory { + + fun MutableList.addFeeUnreachableNotification( + feeError: GetFeeError, + tokenName: String, + onReload: () -> Unit, + ) { + when (feeError) { + is GetFeeError.BlockchainErrors.TronActivationError -> add( + NotificationUM.Warning.TronAccountNotActivated(tokenName), + ) + is GetFeeError.DataError, + is GetFeeError.UnknownError, + -> add( + NotificationUM.Warning.NetworkFeeUnreachable(onReload), + ) + else -> { + /* do nothing */ + } + } + } + + fun MutableList.addExceedBalanceNotification( + feeAmount: BigDecimal, + sendingAmount: BigDecimal, + isSubtractionAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + + if (!isSubtractionAvailable) return + + val showNotification = sendingAmount + feeAmount > balance + if (showNotification) { + add(NotificationUM.Error.TotalExceedsBalance) + } + } + + fun MutableList.addReserveAmountErrorNotification( + reserveAmount: BigDecimal?, + sendingAmount: BigDecimal, + cryptoCurrency: CryptoCurrency, + isAccountFunded: Boolean, + ) { + if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) { + add( + NotificationUM.Error.ReserveAmount( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } + + fun MutableList.addTransactionLimitErrorNotification( + utxoLimit: UtxoAmountLimit?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + if (utxoLimit != null) { + add( + NotificationUM.Error.TransactionLimitError( + cryptoCurrency = cryptoCurrency.name, + utxoLimit = utxoLimit.maxLimit.toPlainString(), + amountLimit = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = utxoLimit.maxAmount, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + utxoLimit.maxAmount, + NotificationUM.Error.TransactionLimitError::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addExistentialWarningNotification( + existentialDeposit: BigDecimal?, + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + onReduceClick: ( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) -> Unit, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val balance = cryptoCurrencyStatus.value.amount ?: return + val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { + feeAmount + } else { + receivedAmount + } + val diff = balance.minus(spendingAmount) + if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) { + add( + NotificationUM.Error.ExistentialDeposit( + deposit = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = existentialDeposit, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + existentialDeposit, + existentialDeposit.minus(diff), + NotificationUM.Error.ExistentialDeposit::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addFeeCoverageNotification( + isFeeCoverage: Boolean, + amountField: AmountFieldModel, + sendingValue: BigDecimal, + appCurrency: AppCurrency, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val amountValue = amountField.cryptoAmount.value ?: return + + val cryptoDiff = amountValue.minus(sendingValue) + if (isFeeCoverage) { + add( + NotificationUM.Warning.FeeCoverageNotification( + cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = cryptoDiff, + cryptoCurrency = cryptoCurrency, + ), + fiatAmount = getFiatString( + value = cryptoDiff, + rate = fiatRate, + appCurrency = appCurrency, + ), + ), + ) + } + } + + fun MutableList.addDustWarningNotification( + dustValue: BigDecimal?, + feeValue: BigDecimal, + sendingAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ) { + if (dustValue == null) return + val isExceedsLimit = checkDustLimits( + feeAmount = feeValue, + receivedAmount = sendingAmount, + dustValue = dustValue, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + ) + if (isExceedsLimit) { + add( + NotificationUM.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + ), + ) + } + } + + fun MutableList.addExceedsBalanceNotification( + cryptoCurrencyWarning: CryptoCurrencyWarning?, + cryptoCurrencyStatus: CryptoCurrencyStatus, + shouldMergeFeeNetworkName: Boolean, + onClick: (CryptoCurrency) -> Unit, + onAnalyticsEvent: (CryptoCurrency) -> Unit, + ) { + when (cryptoCurrencyWarning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId, + networkName = cryptoCurrencyWarning.coinCurrency.name, + currencyName = cryptoCurrencyStatus.currency.name, + feeName = cryptoCurrencyWarning.coinCurrency.name, + feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + onClick(cryptoCurrencyWarning.coinCurrency) + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyStatus.currency) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = cryptoCurrencyWarning.feeCurrency + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, + currencyName = cryptoCurrencyWarning.currency.name, + feeName = cryptoCurrencyWarning.feeCurrencyName, + feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol, + networkName = cryptoCurrencyWarning.networkName, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + currency?.let { + onClick(currency) + } + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyWarning.currency) + } + else -> Unit + } + } + + fun MutableList.addValidateTransactionNotifications( + dustValue: BigDecimal, + fee: Fee?, + validationError: Throwable?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (validationError) { + is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError( + error = validationError, + sendingCurrency = cryptoCurrency, + dustValue = dustValue, + ) + is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError( + error = validationError, + onReduceClick = onReduceClick, + ) + null -> (fee as? Fee.CardanoToken)?.let { + add( + NotificationUM.Cardano.MinAdaValueCharged( + tokenName = cryptoCurrency.name, + minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals), + ), + ) + } + else -> return + } + } + + private fun MutableList.addCardanoTransactionValidationError( + error: BlockchainSdkError.Cardano, + sendingCurrency: CryptoCurrency, + dustValue: BigDecimal?, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (sendingCurrency) { + is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name) + } + }.let(::add) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + dustValue?.let { + add( + NotificationUM.Error.MinimumAmountError( + amount = it.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + } + } + } + + private fun MutableList.addKoinosTransactionValidationError( + error: BlockchainSdkError.Koinos, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (error) { + is BlockchainSdkError.Koinos.InsufficientBalance -> { + add(NotificationUM.Koinos.InsufficientBalance) + } + is BlockchainSdkError.Koinos.InsufficientMana -> { + add( + NotificationUM.Koinos.InsufficientRecoverableMana( + mana = error.manaBalance ?: BigDecimal.ZERO, + maxMana = error.maxMana ?: BigDecimal.ZERO, + ), + ) + } + is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> { + add( + NotificationUM.Koinos.ManaExceedsBalance( + availableKoinForTransfer = error.availableKoinForTransfer, + onReduceClick = { + onReduceClick( + error.availableKoinForTransfer, + NotificationUM.Koinos.InsufficientRecoverableMana::class.java, + ) + }, + ), + ) + } + else -> {} + } + } + + private fun checkDustLimits( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + dustValue: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + val change = when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + balance - (feeAmount + receivedAmount) + } + is CryptoCurrency.Token -> { + val balance = feeCurrencyStatus?.value?.amount ?: BigDecimal.ZERO + balance - feeAmount + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + return receivedAmount < dustValue || isChangeLowerThanDust + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt similarity index 100% rename from domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt rename to domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index a9dcad67cf..dca4eefe62 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.CardWithIcon import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference @@ -13,7 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingNotification import kotlinx.collections.immutable.ImmutableList @Composable -internal fun NotificationsBlock(notifications: ImmutableList) { +internal fun NotificationsBlock(notifications: ImmutableList) { notifications.forEach { notification -> key(notification) { if (notification is StakingNotification.Warning.TransactionInProgress) { @@ -32,8 +33,15 @@ internal fun NotificationsBlock(notifications: ImmutableList TangemTheme.colors.icon.warning is StakingNotification.Warning -> TangemTheme.colors.icon.accent + + is NotificationUM.Error.ExceedsBalance, + is NotificationUM.Warning, + -> null + + is StakingNotification.Error, + is NotificationUM.Error, + -> TangemTheme.colors.icon.warning }, ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 830677086d..89f796a6b7 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -56,6 +56,11 @@ object BlockchainUtils { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } + fun isArbitrum(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Arbitrum + } + data class BlockchainInfo( val blockchainId: String, val name: String, From 52ea08c979029df6bd3ed651e225a8313ea6e272 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 18:08:43 +0500 Subject: [PATCH 73/90] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 6 + .../common/ui/feeScreen/FeeCalculation.kt | 86 ++++++++ .../model/warnings/CryptoCurrencyCheck.kt | 11 ++ .../domain/tokens/GetCurrencyCheckUseCase.kt | 41 ++++ .../staking/impl/di/StakingRouterModule.kt | 6 +- .../impl/navigation/DefaultStakingRouter.kt | 18 ++ .../impl/navigation/InnerStakingRouter.kt | 4 + .../AddStakingNotificationsTransformer.kt | 186 ++++++++++++++++++ .../amount/AmountReduceByStateTransformer.kt | 19 ++ .../amount/AmountReduceToStateTransformer.kt | 18 ++ 10 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.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 078e47c07e..5169a4b83a 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 @@ -436,4 +436,10 @@ internal object TokensDomainModule { fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase { return CheckHasLinkedTokensUseCase(currenciesRepository) } + + @Provides + @Singleton + fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase { + return GetCurrencyCheckUseCase(currencyChecksRepository) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt new file mode 100644 index 0000000000..d1c3719c0a --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt @@ -0,0 +1,86 @@ +package com.tangem.common.ui.feeScreen + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Check and calculates subtracted amount + */ +fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, +): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } +} + +/** + * Checks if sending amount with fee is greater than balance + */ +fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, +): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue +} + +/** + * Check if custom fee is too low + */ +fun checkIfFeeTooLow(fee: TransactionFee, customValue: BigDecimal, isCustomSelected: Boolean): Boolean { + val multipleFees = fee as? TransactionFee.Choosable ?: return false + val minimumValue = multipleFees.minimum.amount.value ?: return false + + return isCustomSelected && minimumValue > customValue +} + +/** + * Check if custom fee is too high + */ +fun checkIfFeeTooHigh( + fee: TransactionFee, + customValue: BigDecimal, + isCustomSelected: Boolean, + onShow: (String) -> Unit, +): Boolean { + val multipleFees = fee as? TransactionFee.Choosable ?: return false + val highValue = multipleFees.priority.amount.value ?: return false + + val diff = customValue / highValue + val isShow = isCustomSelected && diff > FEE_MAX_DIFF + if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP)) + return isShow +} + +/** + * Checks if fee exceeds fee paid currency balance + */ +fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { + return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance +} + +private val FEE_MAX_DIFF = BigDecimal("5") +private const val ZERO_DECIMALS = 0 \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt new file mode 100644 index 0000000000..04018ed4aa --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens.model.warnings + +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import java.math.BigDecimal + +data class CryptoCurrencyCheck( + val dustValue: BigDecimal?, + val reserveAmount: BigDecimal?, + val existentialDeposit: BigDecimal?, + val utxoAmountLimit: UtxoAmountLimit?, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt new file mode 100644 index 0000000000..4e74a21591 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetCurrencyCheckUseCase( + private val currencyChecksRepository: CurrencyChecksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + amount: BigDecimal?, + fee: BigDecimal?, + ): CryptoCurrencyCheck { + val network = currencyStatus.currency.network + val dustValue = currencyChecksRepository.getDustValue(userWalletId, network) + val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network) + val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network) + val utxoAmountLimit = if (amount != null && fee != null) { + currencyChecksRepository.checkUtxoAmountLimit( + userWalletId = userWalletId, + network = network, + amount = amount, + fee = fee, + ) + } else { + null + } + + return CryptoCurrencyCheck( + dustValue = dustValue, + reserveAmount = reserveAmount, + existentialDeposit = existentialDeposit, + utxoAmountLimit = utxoAmountLimit, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt index c64b0dd4f2..1e01cf0b3d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt @@ -1,8 +1,9 @@ package com.tangem.features.staking.impl.di +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,9 +19,10 @@ internal object StakingRouterModule { @Provides @ActivityScoped - fun provideStakingRouter(urlOpener: UrlOpener): StakingRouter { + fun provideStakingRouter(urlOpener: UrlOpener, router: AppRouter): StakingRouter { return DefaultStakingRouter( urlOpener = urlOpener, + router = router, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 464d28694e..6acad0658b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,15 +1,33 @@ package com.tangem.features.staking.impl.navigation import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.impl.presentation.StakingFragment internal class DefaultStakingRouter( private val urlOpener: UrlOpener, + private val router: AppRouter, ) : InnerStakingRouter { override fun getEntryFragment(): Fragment = StakingFragment.create() override fun openUrl(url: String) { urlOpener.openUrl(url) } + + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { + router.pop { isSuccess -> + if (isSuccess) { + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index d4197489fb..e1fec4bab1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -1,8 +1,12 @@ package com.tangem.features.staking.impl.navigation +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.navigation.StakingRouter interface InnerStakingRouter : StakingRouter { fun openUrl(url: String) + + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt new file mode 100644 index 0000000000..2776bf6869 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -0,0 +1,186 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.feeScreen.checkAndCalculateSubtractedAmount +import com.tangem.common.ui.feeScreen.checkFeeCoverage +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class AddStakingNotificationsTransformer( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + private val currencyWarning: CryptoCurrencyWarning?, + private val validatorError: Throwable?, + private val feeError: GetFeeError?, + private val currencyCheck: CryptoCurrencyCheck, + private val isSubtractAvailable: Boolean, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val balance = cryptoCurrencyStatus.value.amount.orZero() + + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val amountState = prevState.amountState as? AmountState.Data ?: return prevState + val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState + + val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() + val feeValue = feeState.fee?.amount?.value.orZero() + val reduceAmountBy = confirmationState.reduceAmountBy.orZero() + + val isFeeCoverage = checkFeeCoverage( + amountValue = amountValue, + feeValue = feeValue, + balance = balance, + isSubtractAvailable = isSubtractAvailable, + reduceAmountBy = reduceAmountBy, + ) + val sendingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + val notifications = buildList { + // errors + addErrorNotifications( + prevState = prevState, + feeError = feeError, + sendingAmount = sendingAmount, + onReload = { prevState.clickIntents.loadFee(confirmationState.pendingActions) }, + feeValue = feeValue, + ) + // warnings + addWarningNotifications( + prevState = prevState, + amountState = amountState, + feeState = feeState, + sendingAmount = sendingAmount, + isFeeCoverage = isFeeCoverage, + ) + + addAll(confirmationState.notifications) + }.toImmutableList() + + return prevState.copy( + confirmationState = confirmationState.copy( + notifications = notifications.toImmutableList(), + ), + ) + } + + private fun MutableList.addErrorNotifications( + prevState: StakingUiState, + onReload: () -> Unit, + feeError: GetFeeError?, + sendingAmount: BigDecimal, + feeValue: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + val network = cryptoCurrency.network + + if (feeError != null) { + addFeeUnreachableNotification( + feeError = feeError, + tokenName = cryptoCurrencyStatusProvider().currency.name, + onReload = onReload, + ) + } + addExceedBalanceNotification( + feeAmount = feeValue, + sendingAmount = sendingAmount, + isSubtractionAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + addExceedsBalanceNotification( + cryptoCurrencyWarning = currencyWarning, + cryptoCurrencyStatus = cryptoCurrencyStatus, + shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), + onClick = prevState.clickIntents::openTokenDetails, + onAnalyticsEvent = { /* [REDACTED_TODO_COMMENT] */ }, + ) + if (!BlockchainUtils.isCardano(network.id.value)) { + addDustWarningNotification( + dustValue = currencyCheck.dustValue, + feeValue = feeValue, + sendingAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + addTransactionLimitErrorNotification( + utxoLimit = currencyCheck.utxoAmountLimit, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + addReserveAmountErrorNotification( + reserveAmount = currencyCheck.reserveAmount, + sendingAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + isAccountFunded = false, + ) + } + + private fun MutableList.addWarningNotifications( + prevState: StakingUiState, + amountState: AmountState.Data, + feeState: FeeState.Content, + sendingAmount: BigDecimal, + isFeeCoverage: Boolean, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + addExistentialWarningNotification( + existentialDeposit = currencyCheck.existentialDeposit, + feeAmount = feeState.fee?.amount?.value.orZero(), + receivedAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + onReduceClick = prevState.clickIntents::onAmountReduceByClick, + ) + addFeeCoverageNotification( + isFeeCoverage = isFeeCoverage, + amountField = amountState.amountTextField, + sendingValue = sendingAmount, + appCurrency = appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + + // blockchain specific + addValidateTransactionNotifications( + dustValue = currencyCheck.dustValue.orZero(), + fee = feeState.fee, + validationError = validatorError, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt new file mode 100644 index 0000000000..91468a21bd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountReduceByStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: ReduceByData, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceByTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt new file mode 100644 index 0000000000..b826aae59e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +internal class AmountReduceToStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: BigDecimal, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceToTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file From 33e5b99d978d76c123341d669aecdcb79741fb7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 19:14:03 +0500 Subject: [PATCH 74/90] Updated on 2026-08-14 --- .../common/ui/feeScreen/FeeCalculation.kt | 86 ------- .../presentation/state/StakingNotification.kt | 43 ++-- .../impl/presentation/state/StakingUiState.kt | 5 +- .../ConfirmationStatePreviewData.kt | 1 + .../state/stub/StakingClickIntentsStub.kt | 17 ++ .../AddStakingErrorTransformer.kt | 17 +- .../AddStakingNotificationsTransformer.kt | 25 +- ...issStakingNotificationsStateTransformer.kt | 25 ++ .../SetConfirmationStateLoadingTransformer.kt | 4 +- .../SetInitialDataStateTransformer.kt | 1 + .../state/utils/FeeCalculation.kt | 44 ++++ .../viewmodel/StakingClickIntents.kt | 18 ++ .../viewmodel/StakingViewModel.kt | 239 ++++++++++++------ 13 files changed, 327 insertions(+), 198 deletions(-) delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt deleted file mode 100644 index d1c3719c0a..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.common.ui.feeScreen - -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import java.math.BigDecimal -import java.math.RoundingMode - -/** - * Check and calculates subtracted amount - */ -fun checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable: Boolean, - cryptoCurrencyStatus: CryptoCurrencyStatus, - amountValue: BigDecimal, - feeValue: BigDecimal, - reduceAmountBy: BigDecimal, -): BigDecimal { - val balance = cryptoCurrencyStatus.value.amount ?: return amountValue - val isFeeCoverage = checkFeeCoverage( - isSubtractAvailable = isAmountSubtractAvailable, - balance = balance, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = reduceAmountBy, - ) - return if (isFeeCoverage) { - balance.minus(reduceAmountBy).minus(feeValue) - } else { - amountValue - } -} - -/** - * Checks if sending amount with fee is greater than balance - */ -fun checkFeeCoverage( - isSubtractAvailable: Boolean, - balance: BigDecimal, - amountValue: BigDecimal, - feeValue: BigDecimal, - reduceAmountBy: BigDecimal?, -): Boolean { - if (!isSubtractAvailable) return false - val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) - return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue -} - -/** - * Check if custom fee is too low - */ -fun checkIfFeeTooLow(fee: TransactionFee, customValue: BigDecimal, isCustomSelected: Boolean): Boolean { - val multipleFees = fee as? TransactionFee.Choosable ?: return false - val minimumValue = multipleFees.minimum.amount.value ?: return false - - return isCustomSelected && minimumValue > customValue -} - -/** - * Check if custom fee is too high - */ -fun checkIfFeeTooHigh( - fee: TransactionFee, - customValue: BigDecimal, - isCustomSelected: Boolean, - onShow: (String) -> Unit, -): Boolean { - val multipleFees = fee as? TransactionFee.Choosable ?: return false - val highValue = multipleFees.priority.amount.value ?: return false - - val diff = customValue / highValue - val isShow = isCustomSelected && diff > FEE_MAX_DIFF - if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP)) - return isShow -} - -/** - * Checks if fee exceeds fee paid currency balance - */ -fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { - return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance -} - -private val FEE_MAX_DIFF = BigDecimal("5") -private const val ZERO_DECIMALS = 0 \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index f9da881678..682b777b25 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -1,61 +1,56 @@ package com.tangem.features.staking.impl.presentation.state +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.* import com.tangem.features.staking.impl.R -internal sealed class StakingNotification(val config: NotificationConfig) { +internal object StakingNotification { sealed class Error( title: TextReference, subtitle: TextReference, - iconResId: Int = R.drawable.ic_alert_24, buttonState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, - ) : StakingNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = iconResId, - buttonsState = buttonState, - onCloseClick = onCloseClick, - ), + ) : NotificationUM.Error( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_24, + buttonState = buttonState, + onCloseClick = onCloseClick, ) { - data class StakedPositionNotFoundError(val message: String) : Error( + data class StakedPositionNotFoundError(val message: String) : StakingNotification.Error( title = stringReference(message), subtitle = stringReference(message), ) - data class Common(val subtitle: TextReference) : Error( + data class Common(val subtitle: TextReference) : StakingNotification.Error( title = resourceReference(R.string.common_error), subtitle = subtitle, ) } - sealed class Warning( title: TextReference, subtitle: TextReference, buttonsState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, - ) : StakingNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = buttonsState, - onCloseClick = onCloseClick, - ), + ) : NotificationUM.Warning( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = buttonsState, + onCloseClick = onCloseClick, ) { data class EarnRewards( val subtitleText: TextReference, - ) : Warning( + ) : StakingNotification.Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), subtitle = subtitleText, ) data class Unstake( val cooldownPeriodDays: Int, - ) : Warning( + ) : StakingNotification.Warning( title = resourceReference(R.string.common_unstake), subtitle = resourceReference( R.string.staking_notification_unstake_text, @@ -72,6 +67,6 @@ internal sealed class StakingNotification(val config: NotificationConfig) { data class TransactionInProgress( val title: TextReference, val description: TextReference, - ) : Warning(title = title, subtitle = description) + ) : StakingNotification.Warning(title = title, subtitle = description) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 55e576c90f..d06f0e1507 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent @@ -13,6 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal /** * Ui states of the staking screen @@ -84,10 +86,11 @@ internal sealed class StakingStates { val feeState: FeeState, val validatorState: ValidatorState, val pendingAction: PendingAction?, - val notifications: ImmutableList, + val notifications: ImmutableList, val footerText: String, val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, + val reduceAmountBy: BigDecimal?, ) : ConfirmationState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index ef4234d37c..25879ce343 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -89,5 +89,6 @@ internal object ConfirmationStatePreviewData { transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = false, + reduceAmountBy = null, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 8876bdabdb..b2edad1892 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,11 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.stub +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import java.math.BigDecimal @Suppress("TooManyFunctions") internal object StakingClickIntentsStub : StakingClickIntents { @@ -49,4 +52,18 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onFailedTxEmailClick(errorMessage: String) {} override fun onActiveStake(activeStake: BalanceState) {} + + override fun getFee(pendingAction: PendingAction?) {} + + override fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) {} + + override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) {} + + override fun onNotificationCancel(notification: Class) {} + + override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt index 69278afeb6..5acae90ea3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt @@ -1,28 +1,37 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList internal class AddStakingErrorTransformer( - private val error: StakingError, + private val error: StakingError? = null, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val notifications = buildList { + addAll(confirmationState.notifications) + error?.let { add(convertToNotification(it)) } + }.toPersistentList() + return prevState.copy( confirmationState = confirmationState.copy( - notifications = (confirmationState.notifications + convertToNotification(error)).toPersistentList(), + notifications = notifications, feeState = FeeState.Error, ), ) } - private fun convertToNotification(error: StakingError): StakingNotification { + private fun convertToNotification(error: StakingError): NotificationUM { return when (error) { is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError( message = error.toString(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 2776bf6869..fd4fb47e43 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -1,8 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.feeScreen.checkAndCalculateSubtractedAmount -import com.tangem.common.ui.feeScreen.checkFeeCoverage import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification @@ -14,13 +12,17 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountE import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount +import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero @@ -41,17 +43,17 @@ internal class AddStakingNotificationsTransformer( ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val appCurrency = appCurrencyProvider() val balance = cryptoCurrencyStatus.value.amount.orZero() val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState val amountState = prevState.amountState as? AmountState.Data ?: return prevState - val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState + val feeState = confirmationState.feeState as? FeeState.Content val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() - val feeValue = feeState.fee?.amount?.value.orZero() + val feeValue = feeState?.fee?.amount?.value.orZero() val reduceAmountBy = confirmationState.reduceAmountBy.orZero() + val isEnterAction = prevState.actionType == StakingActionCommonType.ENTER val isFeeCoverage = checkFeeCoverage( amountValue = amountValue, feeValue = feeValue, @@ -61,7 +63,7 @@ internal class AddStakingNotificationsTransformer( ) val sendingAmount = checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isSubtractAvailable, - cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, reduceAmountBy = reduceAmountBy, @@ -73,7 +75,7 @@ internal class AddStakingNotificationsTransformer( prevState = prevState, feeError = feeError, sendingAmount = sendingAmount, - onReload = { prevState.clickIntents.loadFee(confirmationState.pendingActions) }, + onReload = { prevState.clickIntents.getFee(confirmationState.pendingAction) }, feeValue = feeValue, ) // warnings @@ -82,7 +84,7 @@ internal class AddStakingNotificationsTransformer( amountState = amountState, feeState = feeState, sendingAmount = sendingAmount, - isFeeCoverage = isFeeCoverage, + isFeeCoverage = isFeeCoverage && isEnterAction, ) addAll(confirmationState.notifications) @@ -91,6 +93,7 @@ internal class AddStakingNotificationsTransformer( return prevState.copy( confirmationState = confirmationState.copy( notifications = notifications.toImmutableList(), + isPrimaryButtonEnabled = !notifications.any { it is StakingNotification.Error }, ), ) } @@ -151,7 +154,7 @@ internal class AddStakingNotificationsTransformer( private fun MutableList.addWarningNotifications( prevState: StakingUiState, amountState: AmountState.Data, - feeState: FeeState.Content, + feeState: FeeState.Content?, sendingAmount: BigDecimal, isFeeCoverage: Boolean, ) { @@ -161,7 +164,7 @@ internal class AddStakingNotificationsTransformer( addExistentialWarningNotification( existentialDeposit = currencyCheck.existentialDeposit, - feeAmount = feeState.fee?.amount?.value.orZero(), + feeAmount = feeState?.fee?.amount?.value.orZero(), receivedAmount = sendingAmount, cryptoCurrencyStatus = cryptoCurrencyStatus, onReduceClick = prevState.clickIntents::onAmountReduceByClick, @@ -177,7 +180,7 @@ internal class AddStakingNotificationsTransformer( // blockchain specific addValidateTransactionNotifications( dustValue = currencyCheck.dustValue.orZero(), - fee = feeState.fee, + fee = feeState?.fee, validationError = validatorError, cryptoCurrency = cryptoCurrency, onReduceClick = prevState.clickIntents::onAmountReduceToClick, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt new file mode 100644 index 0000000000..a5fc4d9bc5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class DismissStakingNotificationsStateTransformer( + private val notification: Class, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val updatedNotifications = confirmationState?.notifications + ?.filterNot { it::class == notification }?.toPersistentList() + ?: persistentListOf() + + return prevState.copy( + confirmationState = confirmationState?.copy( + notifications = updatedNotifications, + ) ?: StakingStates.ConfirmationState.Empty(), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 9e35b20442..e158860e89 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.Yield @@ -34,11 +35,12 @@ internal class SetConfirmationStateLoadingTransformer( transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = false, + reduceAmountBy = null, ), ) } - private fun getNotifications(prevState: StakingUiState): ImmutableList { + private fun getNotifications(prevState: StakingUiState): ImmutableList { return persistentListOf( if (prevState.actionType == StakingActionCommonType.EXIT) { StakingNotification.Warning.Unstake( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 19f1c44544..7d873f5da1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -214,6 +214,7 @@ internal class SetInitialDataStateTransformer( transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = isApprovalNeeded, + reduceAmountBy = null, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt new file mode 100644 index 0000000000..21e95ed465 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt @@ -0,0 +1,44 @@ +package com.tangem.features.staking.impl.presentation.state.utils + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import java.math.BigDecimal + +/** + * Check and calculates subtracted amount + */ +internal fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, +): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } +} + +/** + * Checks if sending amount with fee is greater than balance + */ +internal fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, +): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index ad7b9ff1c6..11910f51df 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,12 +1,16 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType +import java.math.BigDecimal +@Suppress("TooManyFunctions") internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() @@ -21,6 +25,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onInfoClick(infoType: InfoType) + fun getFee(pendingAction: PendingAction?) + override fun onAmountNext() = onNextClick(actionType = null) fun openValidators() @@ -35,9 +41,21 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onApprovalClick() + fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) + + fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) + + fun onNotificationCancel(notification: Class) + fun onExploreClick() fun onShareClick() fun onFailedTxEmailClick(errorMessage: String) + + fun openTokenDetails(cryptoCurrency: CryptoCurrency) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 523380111a..1c18173837 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -12,7 +12,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager @@ -32,16 +34,15 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType -import com.tangem.domain.tokens.FetchPendingTransactionsUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -50,10 +51,7 @@ import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.* import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalInProgressTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer @@ -103,6 +101,10 @@ internal class StakingViewModel @Inject constructor( private val feedbackManager: FeedbackManager, private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val validateTransactionUseCase: ValidateTransactionUseCase, + private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -140,6 +142,7 @@ internal class StakingViewModel @Inject constructor( ) private var stakingApproval: StakingApproval = StakingApproval.Empty + private var isAmountSubtractAvailable: Boolean = false private val allowanceTaskScheduler = SingleTaskScheduler() private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() @@ -171,6 +174,57 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onNextClick() } + override fun getFee(pendingAction: PendingAction?) { + viewModelScope.launch { + stateController.update( + SetConfirmationStateLoadingTransformer( + yield = yield, + ), + ) + val cryptoCurrencyValue = cryptoCurrencyStatus.value + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + + val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value + ?: error("No available address") + val validatorAddress = validatorState.chosenValidator.address + + val approval = stakingApproval as? StakingApproval.Needed + if (approval != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + + if (allowance < amount) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + ) + } else { + estimateGas( + pendingAction = pendingAction, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } else { + estimateGas( + pendingAction = pendingAction, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } + } + private fun handleOnNextConfirmationClick() { if (isAssentState()) { viewModelScope.launch { @@ -236,57 +290,6 @@ internal class StakingViewModel @Inject constructor( } } - private fun getFee(pendingAction: PendingAction?) { - viewModelScope.launch { - stateController.update( - SetConfirmationStateLoadingTransformer( - yield = yield, - ), - ) - val cryptoCurrencyValue = cryptoCurrencyStatus.value - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content - ?: error("No validator provided") - - val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided") - val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value - ?: error("No available address") - val validatorAddress = validatorState.chosenValidator.address - - val approval = stakingApproval as? StakingApproval.Needed - if (approval != null) { - val allowance = getAllowanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - - if (allowance < amount) { - getApproveFee( - amount = amount, - validatorAddress = validatorAddress, - ) - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } - } - } - private suspend fun estimateGas( pendingAction: PendingAction?, amount: BigDecimal, @@ -310,7 +313,6 @@ internal class StakingViewModel @Inject constructor( stateController.update(AddStakingErrorTransformer(it)) return } - stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -325,24 +327,30 @@ internal class StakingViewModel @Inject constructor( action = pendingAction, ), ) + updateNotifications() } private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) { - val approvalFee = getFeeUseCase( + getFeeUseCase( amount = amount, destination = validatorAddress, userWallet = userWallet, cryptoCurrency = cryptoCurrencyStatus.currency, - ).getOrElse { - return stakingEventFactory.createGenericErrorAlert(it.toString()) - } - - stateController.update( - SetConfirmationStateAssentApprovalTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = approvalFee, - ), + ).fold( + ifRight = { fee -> + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = fee, + ), + ) + updateNotifications() + }, + ifLeft = { + stateController.update(AddStakingErrorTransformer()) + updateNotifications(it) + }, ) } @@ -499,6 +507,85 @@ internal class StakingViewModel @Inject constructor( }.saveIn(approvalJobHolder) } + private fun updateNotifications(feeError: GetFeeError? = null) { + viewModelScope.launch { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + val feeState = confirmationState?.feeState as? FeeState.Content + val amountState = value.amountState as? AmountState.Data + + val amount = amountState?.amountTextField?.cryptoAmount?.value + val fee = feeState?.fee?.amount?.value + val currencyWarning = if (feeCryptoCurrencyStatus != null && fee != null) { + getBalanceNotEnoughForFeeWarningUseCase( + fee = fee, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, + ).getOrNull() + } else { + null + } + val validation = amount?.let { + validateTransactionUseCase( + userWalletId = userWalletId, + amount = amount.convertToSdkAmount(cryptoCurrencyStatus.currency), + fee = feeState?.fee, + memo = null, + destination = "", + network = cryptoCurrencyStatus.currency.network, + ).leftOrNull() + } + + val currencyStatus = getCurrencyCheckUseCase( + userWalletId = userWalletId, + currencyStatus = cryptoCurrencyStatus, + amount = amount, + fee = fee, + ) + stateController.update( + AddStakingNotificationsTransformer( + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + currencyWarning = currencyWarning, + validatorError = validation, + currencyCheck = currencyStatus, + isSubtractAvailable = isAmountSubtractAvailable, + feeError = feeError, + ), + ) + } + } + + override fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) { + AmountReduceByStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = AmountReduceByTransformer.ReduceByData( + reduceAmountBy = reduceAmountBy, + reduceAmountByDiff = reduceAmountByDiff, + ), + ) + onNotificationCancel(notification) + } + + override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) { + stateController.update( + AmountReduceToStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = reduceAmountTo, + ), + ) + onNotificationCancel(notification) + } + + override fun onNotificationCancel(notification: Class) { + stateController.update(DismissStakingNotificationsStateTransformer(notification)) + } + private fun awaitForAllowance(pendingAction: PendingAction?) { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( @@ -586,6 +673,10 @@ internal class StakingViewModel @Inject constructor( } } + override fun openTokenDetails(cryptoCurrency: CryptoCurrency) { + innerRouter.openTokenDetails(userWalletId, cryptoCurrency) + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -612,6 +703,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatus = it setupApprovalNeeded() + checkIfSubtractAvailable() stateController.update( transformer = SetInitialDataStateTransformer( @@ -779,6 +871,11 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } + private suspend fun checkIfSubtractAvailable() { + isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrencyStatus.currency) + .getOrElse { false } + } + private data class FullTransactionData( val stakeKitTransaction: StakingTransaction, val tangemTransaction: TransactionData.Compiled, From 4a25e38ccc6b72ef7a5cdddcbeb4668b5e2a2e4b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 18:25:34 +0400 Subject: [PATCH 75/90] Updated on 2026-08-14 --- .../com/tangem/core/decompose/model/Model.kt | 26 ++++++- .../java/com/tangem/utils/extensions/List.kt | 21 ++++-- .../component/AddCustomTokenComponent.kt | 1 + features/manage-tokens/impl/build.gradle.kts | 1 + .../component/CustomTokenFormComponent.kt | 1 + .../impl/DefaultAddCustomTokenComponent.kt | 6 ++ .../impl/DefaultManageTokensComponent.kt | 1 + .../entity/managetokens/ManageTokensUM.kt | 8 +++ .../model/CustomTokenFormModel.kt | 70 ++++++++++++++----- .../managetokens/model/ManageTokensModel.kt | 25 +++++++ .../managetokens/ui/ManageTokensScreen.kt | 11 ++- .../utils/list/ManageTokensUiManager.kt | 8 +-- 12 files changed, 151 insertions(+), 28 deletions(-) diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt index c845658265..b1337c72f9 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt @@ -46,14 +46,36 @@ abstract class Model : InstanceKeeper.Instance { progressFlow: MutableSharedFlow, dispatcher: CoroutineDispatcher = dispatchers.mainImmediate, crossinline block: suspend () -> Unit, + ): Job = resource( + acquire = { progressFlow.emit(true) }, + release = { progressFlow.emit(false) }, + dispatcher = dispatcher, + block = block, + ) + + /** + * Launches [block] in the model's scope and acquires a resource before executing the block and releases it after. + * + * @param acquire The block of code to acquire the resource. + * @param release The block of code to release the resource. + * @param dispatcher The [CoroutineDispatcher] to launch the coroutine. Default is [Dispatchers.Main.immediate]. + * @param block The block of code to execute. + * + * @return The [Job] of the launched coroutine. + * */ + protected inline fun resource( + crossinline acquire: suspend () -> Unit, + crossinline release: suspend () -> Unit, + dispatcher: CoroutineDispatcher = dispatchers.mainImmediate, + crossinline block: suspend () -> Unit, ): Job = modelScope.launch(dispatcher) { - progressFlow.emit(value = true) + acquire() try { block() } finally { withContext(NonCancellable) { - progressFlow.emit(value = false) + release() } } } diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt index e0fbdb5d90..86bdfe2e6e 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt @@ -44,11 +44,24 @@ inline fun MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boo */ inline fun List.addOrReplace(item: T, predicate: (T) -> Boolean): List { val mutableList = this.toMutableList() - val isReplaced = mutableList.replaceBy(item, predicate) - if (!isReplaced) { - mutableList.add(item) - } + mutableList.addOrReplace(item, predicate) return mutableList +} + +/** + * Adds the specified element to the mutable list or replaces an existing element. + * + * !!!This function is not thread-safe!!! + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + */ +inline fun MutableList.addOrReplace(item: T, predicate: (T) -> Boolean) { + val isReplaced = replaceBy(item, predicate) + + if (!isReplaced) { + add(item) + } } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt index 63a0f48e26..4539527ce5 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt @@ -9,6 +9,7 @@ interface AddCustomTokenComponent : ComposableBottomSheetComponent { data class Params( val userWalletId: UserWalletId, val onDismiss: () -> Unit, + val onCurrencyAdded: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index a8d50aa900..a506043ad2 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.manageTokens) + implementation(projects.domain.card) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index 3f6d401d30..e9fc3c448f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -16,6 +16,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { val formValues: CustomTokenFormValues, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, + val onCurrencyAdded: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 48e7a1b01f..3679e6f929 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -131,6 +131,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( formValues = config.formValues, onSelectNetworkClick = ::showNetworkSelector, onSelectDerivationPathClick = ::showDerivationPathSelector, + onCurrencyAdded = ::dismissAndNotify, ), ) } @@ -170,6 +171,11 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( navigation.replaceAll(config) } + private fun dismissAndNotify() { + dismiss() + params.onCurrencyAdded() + } + @AssistedFactory interface Factory : AddCustomTokenComponent.Factory { override fun create( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index 9d90bf8789..98df4a4346 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -62,6 +62,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( params = AddCustomTokenComponent.Params( userWalletId = config.userWalletId, onDismiss = model.bottomSheetNavigation::dismiss, + onCurrencyAdded = model::reloadList, ), ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index 33f1524452..8a0bf34154 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -2,6 +2,8 @@ package com.tangem.features.managetokens.entity.managetokens import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.features.managetokens.entity.item.CurrencyItemUM import kotlinx.collections.immutable.ImmutableList @@ -15,6 +17,7 @@ internal sealed class ManageTokensUM { abstract val topBar: ManageTokensTopBarUM abstract val search: SearchBarUM abstract val loadMore: () -> Boolean + abstract val scrollToTop: StateEvent data class ReadContent( override val popBack: () -> Unit, @@ -24,6 +27,7 @@ internal sealed class ManageTokensUM { override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, override val loadMore: () -> Boolean, + override val scrollToTop: StateEvent = consumedEvent(), ) : ManageTokensUM() data class ManageContent( @@ -34,6 +38,7 @@ internal sealed class ManageTokensUM { override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, override val loadMore: () -> Boolean, + override val scrollToTop: StateEvent = consumedEvent(), val saveChanges: () -> Unit, val hasChanges: Boolean, val isSavingInProgress: Boolean, @@ -46,6 +51,7 @@ internal sealed class ManageTokensUM { isInitialBatchLoading: Boolean = this.isInitialBatchLoading, isNextBatchLoading: Boolean = this.isNextBatchLoading, isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, + scrollToTop: StateEvent = this.scrollToTop, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -55,12 +61,14 @@ internal sealed class ManageTokensUM { isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, isSavingInProgress = isSavingInProgress, + scrollToTop = scrollToTop, ) is ReadContent -> copy( search = search, items = items, isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, + scrollToTop = scrollToTop, ) } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 0565adb15e..25fbf11d48 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.managetokens.model import androidx.compose.ui.res.stringResource +import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,7 +10,9 @@ import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM @@ -31,11 +34,14 @@ import javax.inject.Inject internal class CustomTokenFormModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val customCurrencyValidator: CustomCurrencyValidator, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, private val messageSender: UiMessageSender, paramsContainer: ParamsContainer, ) : Model() { private val params: CustomTokenFormComponent.Params = paramsContainer.require() + private var createdCurrency: CryptoCurrency? = null val state: MutableStateFlow = MutableStateFlow( value = getInitialState(), @@ -78,17 +84,7 @@ internal class CustomTokenFormModel @Inject constructor( }, onClick = ::selectDerivationPath, ), - saveToken = { - // TODO: Save token: [REDACTED_JIRA] - val dialog = ContentMessage { onDismiss -> - SimpleOkDialog( - message = "Not yet implemented", - onDismissDialog = onDismiss, - ) - } - - messageSender.send(dialog) - }, + saveToken = ::addCurrency, ) } @@ -117,6 +113,8 @@ internal class CustomTokenFormModel @Inject constructor( private fun observeValidatorUpdates() = modelScope.launch { customCurrencyValidator.consumeUpdates { validatorState -> + createdCurrency = null + when (validatorState) { is CustomCurrencyValidator.State.NotStarted -> Unit is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching() @@ -125,12 +123,16 @@ internal class CustomTokenFormModel @Inject constructor( exceptions = validatorState.exceptions, ) is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification() - is CustomCurrencyValidator.State.Validated -> updateStateWithCurrency( - currency = validatorState.currency, - fillForm = validatorState.fillForm, - isAlreadyAdded = validatorState.isAlreadyAdded, - isCustom = validatorState.isCustom, - ) + is CustomCurrencyValidator.State.Validated -> { + createdCurrency = validatorState.currency + + updateStateWithCurrency( + currency = validatorState.currency, + fillForm = validatorState.fillForm, + isAlreadyAdded = validatorState.isAlreadyAdded, + isCustom = validatorState.isCustom, + ) + } } } } @@ -296,6 +298,40 @@ internal class CustomTokenFormModel @Inject constructor( } } + private fun addCurrency() = resource( + acquire = { + state.update { state -> + state.updateWithProgress(showProgress = true) + } + }, + release = { + state.update { state -> + state.updateWithProgress(showProgress = false) + } + }, + ) { + val currency = createdCurrency + if (currency == null) { + Timber.e("Trying to add currency without validation") + showErrorDialog() + return@resource + } + + derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse { + Timber.e(it, "Failed to derive public keys") + showErrorDialog() + return@resource + } + + addCryptoCurrenciesUseCase(params.userWalletId, currency).getOrElse { + Timber.e(it, "Failed to add currency") + showErrorDialog() + return@resource + } + + params.onCurrencyAdded() + } + private fun selectNetwork() { params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 87e6636653..b5a235ee3a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -9,6 +9,8 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage @@ -67,6 +69,12 @@ internal class ManageTokensModel @Inject constructor( } } + fun reloadList() { + modelScope.launch { + manageTokensListManager.reload(params.userWalletId) + } + } + private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { return if (userWalletId == null) { createReadContentModel() @@ -166,9 +174,18 @@ internal class ManageTokensModel @Inject constructor( (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> Timber.e(fetchError.throwable) } + state.copySealed( isInitialBatchLoading = false, isNextBatchLoading = false, + scrollToTop = if (state.isInitialBatchLoading && state.items.isNotEmpty()) { + triggeredEvent( + data = Unit, + onConsume = ::consumeScrollToTopEvent, + ) + } else { + state.scrollToTop + }, ) } is PaginationStatus.EndOfPagination -> state.copySealed( @@ -179,6 +196,14 @@ internal class ManageTokensModel @Inject constructor( } } + private fun consumeScrollToTopEvent() { + this.state.update { state -> + state.copySealed( + scrollToTop = consumedEvent(), + ) + } + } + private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { state.update { state -> state.copySealed( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 9769590b55..1cff9a28cb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.CircularProgressIndicator @@ -45,6 +46,7 @@ import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalSnackbarHostState @@ -166,9 +168,12 @@ private fun SaveChangesButton( @Composable private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + Box(modifier = modifier) { Currencies( modifier = Modifier.fillMaxSize(), + listState = listState, items = state.items, showLoadingItem = state.isNextBatchLoading, onLoadMore = state.loadMore, @@ -185,10 +190,15 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { ) } } + + EventEffect(event = state.scrollToTop) { + listState.animateScrollToItem(index = 0) + } } @Composable private fun Currencies( + listState: LazyListState, items: ImmutableList, showLoadingItem: Boolean, isEditable: Boolean, @@ -198,7 +208,6 @@ private fun Currencies( val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(density = this).toDp() } - val listState = rememberLazyListState() LazyColumn( modifier = modifier, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 78e3d1011b..1cc07f7286 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -13,6 +13,7 @@ import com.tangem.features.managetokens.utils.ui.update import com.tangem.pagination.Batch import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -49,8 +50,9 @@ internal class ManageTokensUiManager( newCurrencyBatches.forEach { (key, data) -> val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key } + val currencyBatch = state.value.currencyBatches.getOrNull(indexToUpdate) - if (indexToUpdate == -1) { + if (indexToUpdate == -1 || currencyBatch == null) { val newBatch = Batch( key = key, data = data.map { item -> @@ -62,7 +64,7 @@ internal class ManageTokensUiManager( }, ) - batches.add(newBatch) + batches.addOrReplace(newBatch) { it.key == key } } else { val uiBatchToUpdate = currentUiBatches[indexToUpdate] @@ -70,8 +72,6 @@ internal class ManageTokensUiManager( return@forEach } - val currentCurrencyBatches = state.value.currencyBatches - val currencyBatch = currentCurrencyBatches[indexToUpdate] val updatedBatch = uiBatchToUpdate.copy( data = data.mapIndexed { index, item -> if (item == currencyBatch.data[index]) { From 248dab7303634ccd7c66c1f40df8da2ec83a290d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 14:21:17 +0500 Subject: [PATCH 76/90] Updated on 2026-08-14 --- .../managetokens/utils/list/ManageTokensListManager.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 12c248b792..565bb8de58 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -92,6 +92,7 @@ internal class ManageTokensListManager @Inject constructor( } suspend fun reload(userWalletId: UserWalletId?) { + state.value = ManageTokensListState() actionsFlow.emit( BatchAction.Reload( requestParams = ManageTokensListConfig(userWalletId, searchText = null), @@ -166,6 +167,8 @@ internal class ManageTokensListManager @Inject constructor( override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) { scope.launch { removeCustomCurrencyUseCase.invoke(userWalletId, currency) + .onRight { reload(userWalletId) } + .onLeft { Timber.e(it) } } } From 681fe664c632c7eb3592525e0fe6c71a6bb09c05 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:23:16 +0500 Subject: [PATCH 77/90] Updated on 2026-08-14 --- .../transformers/SetConfirmationStateCompletedTransformer.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 27e6593bd0..d2e2435917 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -4,11 +4,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateCompletedTransformer( private val appCurrencyProvider: Provider, @@ -39,6 +37,7 @@ internal class SetConfirmationStateCompletedTransformer( validatorState = validatorState.copySealed( isClickable = false, ), + notifications = persistentListOf(), transactionDoneState = TransactionDoneState.Content( timestamp = System.currentTimeMillis(), txUrl = txUrl, From 01b655feb7a6012a0c1ed8f08d5350ba5cf57bd2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:23:48 +0500 Subject: [PATCH 78/90] Updated on 2026-08-14 --- .../presentation/state/StakingStateRouter.kt | 4 +--- .../transformers/SetButtonsStateTransformer.kt | 2 +- .../ui/StakingConfirmationContent.kt | 11 ++++++++--- .../presentation/viewmodel/StakingViewModel.kt | 16 ++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 03cc906908..5dd1f3de27 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -26,9 +26,7 @@ internal class StakingStateRouter( StakingStep.Validators, StakingStep.Amount, -> showConfirmation() - StakingStep.Confirmation -> { - // TODO staking handle - } + StakingStep.Confirmation -> showInitial() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 6bd2c902fa..b18c96f147 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -152,7 +152,7 @@ internal class SetButtonsStateTransformer : Transformer { private fun StakingUiState.onConfirmationClick() { if (confirmationState is StakingStates.ConfirmationState.Data) { if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - clickIntents.onBackClick() + clickIntents.onNextClick() } else { val isEnterAction = actionType == StakingActionCommonType.ENTER val isApproveNeeded = confirmationState.isApprovalNeeded diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index ae5079b938..d7a9431621 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -3,11 +3,15 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -45,11 +49,12 @@ internal fun StakingConfirmationContent( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { + val doneState = state.transactionDoneState AnimatedVisibility( - visible = state.transactionDoneState is TransactionDoneState.Content, + visible = doneState is TransactionDoneState.Content, modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), ) { - val transactionDoneStateContent = state.transactionDoneState as TransactionDoneState.Content + val transactionDoneStateContent = remember(this) { doneState as TransactionDoneState.Content } TransactionDoneTitle( titleRes = R.string.sent_transaction_sent_title, date = transactionDoneStateContent.timestamp, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 1c18173837..4eba6fabde 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -164,23 +164,19 @@ internal class StakingViewModel @Inject constructor( stateController.update { it.copy(actionType = actionType) } } stakingStateRouter.onNextClick() - if (isAssentState()) { - getFee(pendingAction) + when { + isInitState() -> stateController.update(SetConfirmationStateLoadingTransformer(yield)) + isAssentState() -> getFee(pendingAction) } } override fun onActionClick() { handleOnNextConfirmationClick() - stakingStateRouter.onNextClick() } override fun getFee(pendingAction: PendingAction?) { viewModelScope.launch { - stateController.update( - SetConfirmationStateLoadingTransformer( - yield = yield, - ), - ) + stateController.update(SetConfirmationStateLoadingTransformer(yield)) val cryptoCurrencyValue = cryptoCurrencyStatus.value val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") @@ -871,6 +867,10 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } + private fun isInitState(): Boolean { + return value.currentStep == StakingStep.InitialInfo + } + private suspend fun checkIfSubtractAvailable() { isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrencyStatus.currency) .getOrElse { false } From a809fa07b6a4a10d4a14742e1d1630fab7085a88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:24:18 +0500 Subject: [PATCH 79/90] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../staking/model/stakekit/YieldBalance.kt | 25 ++++++++++++++--- .../state/InnerYieldBalanceState.kt | 1 + .../RewardsValidatorStateConverter.kt | 27 ++++++++++++------- .../converters/YieldBalancesConverter.kt | 21 ++++----------- .../previewdata/InitialStakingStatePreview.kt | 6 ++++- .../ui/StakingInitialInfoContent.kt | 14 +++++++++- 7 files changed, 63 insertions(+), 32 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 314a9a07d4..7b30804e92 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -671,6 +671,7 @@ Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. + Preparing Ready to withdraw Rebond Restake diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index adf85b9751..1c7c27ac54 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -78,11 +78,28 @@ data class PendingAction( enum class BalanceType { AVAILABLE, STAKED, - UNSTAKING, - UNSTAKED, PREPARING, - REWARDS, - LOCKED, + UNSTAKING, UNLOCKING, + UNSTAKED, + LOCKED, + REWARDS, UNKNOWN, + ; + + companion object { + fun BalanceType.isClickable() = when (this) { + STAKED, + UNSTAKED, + -> true + AVAILABLE, + UNSTAKING, + PREPARING, + REWARDS, + LOCKED, + UNLOCKING, + UNKNOWN, + -> false + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index ef7e4e8fec..c2ed6f2d62 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -39,5 +39,6 @@ data class BalanceState( val fiatAmount: TextReference, val rawCurrencyId: String?, val unbondingPeriod: TextReference, + val warmupPeriod: TextReference, val pendingActions: ImmutableList, ) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index a1c35482de..b27bdf6c41 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -1,6 +1,9 @@ package com.tangem.features.staking.impl.presentation.state.converters -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -46,16 +49,11 @@ internal class RewardsValidatorStateConverter( } val cryptoValue = balance.amount val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) - val unbondingPeriod = yield.metadata.cooldownPeriod.days + validator?.toBalanceState( cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoValue = cryptoValue, fiatValue = fiatValue, - unbondingPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(unbondingPeriod), - ), pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -64,12 +62,12 @@ internal class RewardsValidatorStateConverter( cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoValue: BigDecimal, fiatValue: BigDecimal?, - unbondingPeriod: TextReference, pendingActions: ImmutableList, ): BalanceState { val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency - + val unbondingPeriod = yield.metadata.cooldownPeriod.days + val warmupPeriod = yield.metadata.warmupPeriod.days val cryptoAmount = stringReference( BigDecimalFormatter.formatCryptoAmount( cryptoAmount = cryptoValue, @@ -94,7 +92,16 @@ internal class RewardsValidatorStateConverter( cryptoAmount = cryptoAmount, fiatAmount = fiatAmount, rawCurrencyId = cryptoCurrency.id.rawCurrencyId, - unbondingPeriod = unbondingPeriod, + unbondingPeriod = pluralReference( + id = R.plurals.common_days, + count = unbondingPeriod, + formatArgs = wrappedList(unbondingPeriod), + ), + warmupPeriod = pluralReference( + id = R.plurals.common_days, + count = unbondingPeriod, + formatArgs = wrappedList(warmupPeriod), + ), pendingActions = pendingActions, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 9be3086028..17c6e659d7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState @@ -61,7 +62,7 @@ internal class YieldBalancesConverter( .groupBy { it.type.toGroup() } .mapNotNull { item -> val (title, footer) = getGroupTitle(item.key) - val isClickable = getClickableType(item.key) + val isClickable = item.key.isClickable() title?.let { BalanceGroupedState( items = item.value.mapBalances().toPersistentList(), @@ -88,6 +89,7 @@ internal class YieldBalancesConverter( val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) val unbonding = getUnbondingDate(balance.date) + val warmupPeriod = yield.metadata.warmupPeriod.days validator?.let { BalanceState( validator = validator, @@ -108,6 +110,7 @@ internal class YieldBalancesConverter( ), rawCurrencyId = balance.rawCurrencyId, unbondingPeriod = unbonding, + warmupPeriod = pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)), pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -128,27 +131,13 @@ internal class YieldBalancesConverter( resourceReference(R.string.staking_unstaked_footer) BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null BalanceType.AVAILABLE -> null to null - BalanceType.PREPARING -> null to null + BalanceType.PREPARING -> resourceReference(R.string.staking_preparing) to null BalanceType.REWARDS -> null to null BalanceType.LOCKED -> null to null BalanceType.UNLOCKING -> null to null BalanceType.UNKNOWN -> null to null } - private fun getClickableType(type: BalanceType) = when (type) { - BalanceType.STAKED, - BalanceType.UNSTAKED, - -> true - BalanceType.AVAILABLE, - BalanceType.UNSTAKING, - BalanceType.PREPARING, - BalanceType.REWARDS, - BalanceType.LOCKED, - BalanceType.UNLOCKING, - BalanceType.UNKNOWN, - -> false - } - private fun getUnbondingDate(date: DateTime?): TextReference { val now = DateTime.now().millis val nowCalendar = Calendar.getInstance() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 66d92d8f89..15efd12337 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -6,7 +6,10 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { @@ -87,6 +90,7 @@ internal object InitialStakingStatePreview { ), unbondingPeriod = stringReference("3 days"), pendingActions = persistentListOf(), + warmupPeriod = stringReference("3 days"), ), ), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 8bd4543778..c84e8247e3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -209,7 +209,7 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli InputRowImageInfo( subtitle = stringReference(balance.validator.name), caption = getCaption(group.type, balance), - isGrayscaleImage = group.type == BalanceType.UNSTAKING, + isGrayscaleImage = !group.isClickable, infoTitle = balance.fiatAmount, infoSubtitle = balance.cryptoAmount, imageUrl = balance.validator.image.orEmpty(), @@ -248,6 +248,18 @@ private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextRef BalanceType.UNSTAKED -> { resourceReference(R.string.staking_ready_to_withdraw) } + BalanceType.PREPARING -> { + combinedReference( + resourceReference(R.string.staking_details_warmup_period), + annotatedReference { + appendSpace() + appendColored( + text = balance.warmupPeriod.resolveReference(), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } else -> { combinedReference( resourceReference(R.string.staking_details_apr), From 653cd7e72cb0009f513a618dbf20b7789ddf15cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:15:07 +0300 Subject: [PATCH 80/90] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 5 - .../com/tangem/common/routing/AppRoute.kt | 4 +- .../domain/tokens/legacy/TradeCryptoAction.kt | 2 - .../feature/swap/domain/SwapInteractor.kt | 8 +- .../feature/swap/domain/SwapInteractorImpl.kt | 129 +++++++----------- .../swap/domain/di/SwapDomainModule.kt | 124 ++--------------- .../feature/swap/viewmodels/SwapViewModel.kt | 58 ++++---- .../viewmodels/TokenDetailsViewModel.kt | 4 +- .../MultiWalletCurrencyActionsConverter.kt | 8 +- .../WalletCurrencyActionsClickIntents.kt | 17 ++- 10 files changed, 121 insertions(+), 238 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index cfb77e6180..9f5d8aecb2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -48,7 +48,6 @@ object TradeCryptoMiddleware { is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.Buy -> proceedBuyAction(state, action) is TradeCryptoAction.Sell -> proceedSellAction(action) - is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency) is TradeCryptoAction.Stake -> openStaking( userWalletId = action.userWalletId, cryptoCurrencyId = action.cryptoCurrencyId, @@ -141,10 +140,6 @@ object TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openSwap(currency: CryptoCurrency) { - store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) } - } - private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) { store.dispatchNavigationAction { push( diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index bae031de6d..d8e383a18e 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -219,12 +219,14 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Swap( val currency: CryptoCurrency, - ) : AppRoute(path = "/swap/${currency.id.value}"), RouteBundleParams { + val userWalletId: UserWalletId, + ) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) companion object { const val CURRENCY_BUNDLE_KEY = "currency" + const val USER_WALLET_ID_KEY = "userWalletId" } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 0b04bf321b..706493a6ea 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -40,8 +40,6 @@ sealed class TradeCryptoAction : Action { val transactionInfo: TransactionInfo? = null, ) : TradeCryptoAction() - data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() - data class Stake( val userWalletId: UserWalletId, val cryptoCurrencyId: CryptoCurrency.ID, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index dafaff9a51..8c6f6a8de7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount import com.tangem.feature.swap.domain.models.domain.PermissionOptions @@ -79,12 +79,14 @@ interface SwapInteractor { */ fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount - fun getSelectedWallet(): UserWallet? - suspend fun selectInitialCurrencyToSwap( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, ): CryptoCurrencyStatus? fun getNativeToken(networkId: String): CryptoCurrency + + interface Factory { + fun create(selectedWalletId: UserWalletId): SwapInteractor + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index de0468d9ff..72b9def69e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -14,7 +14,6 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.* @@ -29,7 +28,7 @@ import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.DataError @@ -43,21 +42,22 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFee import com.tangem.lib.crypto.models.ProxyFees +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode -import javax.inject.Inject @Suppress("LargeClass", "LongParameterList") -internal class SwapInteractorImpl @Inject constructor( +internal class SwapInteractorImpl @AssistedInject constructor( private val transactionManager: TransactionManager, private val userWalletManager: UserWalletManager, private val repository: SwapRepository, private val allowPermissionsHandler: AllowPermissionsHandler, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, @@ -69,9 +69,10 @@ internal class SwapInteractorImpl @Inject constructor( private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, - private val demoConfig: DemoConfig, private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) { @@ -82,15 +83,13 @@ internal class SwapInteractorImpl @Inject constructor( private val amountFormatter = AmountFormatter() private val hundredPercent = BigInteger("100") + private val userWallet + get() = getUserWalletUseCase(userWalletId).getOrElse { + error("Failed to get user wallet") + } + override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { - val selectedWallet = getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { it }, - ) - - requireNotNull(selectedWallet) { "No selected wallet" } - - val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) + val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(userWalletId) .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses @@ -133,10 +132,6 @@ internal class SwapInteractorImpl @Inject constructor( ) } - override fun getSelectedWallet(): UserWallet? { - return getSelectedWalletSyncUseCase().getOrNull() - } - private fun getToCurrenciesGroup( currency: CryptoCurrency, leastPairs: List, @@ -224,7 +219,7 @@ internal class SwapInteractorImpl @Inject constructor( memo = null, destination = getTokenAddress(permissionOptions.fromToken), network = permissionOptions.fromToken.network, - userWalletId = requireNotNull(getSelectedWallet()).walletId, + userWalletId = userWalletId, txExtras = createDexTxExtras( dataToSign, permissionOptions.fromToken.network, @@ -237,7 +232,7 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( txData = approveTransaction, - userWallet = requireNotNull(getSelectedWallet()), + userWallet = userWallet, network = permissionOptions.fromToken.network, ) return result.fold( @@ -403,7 +398,6 @@ internal class SwapInteractorImpl @Inject constructor( minAdaValue: BigDecimal?, ): List { val fromToken = fromTokenStatus.currency - val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() val warnings = mutableListOf() manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken, feeState) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) @@ -597,7 +591,8 @@ internal class SwapInteractorImpl @Inject constructor( """.trimIndent(), ) - val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return SwapTransactionState.UnknownError + val cardId = userWallet.scanResponse.card.cardId if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode return when (swapProvider.type) { @@ -615,7 +610,6 @@ internal class SwapInteractorImpl @Inject constructor( amount = amountToSwapWithFee, txFee = fee, swapProvider = swapProvider, - userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { @@ -627,7 +621,6 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGetStatus = currencyToGet, fee = fee, amountToSwap = amountToSwap, - userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } } @@ -679,7 +672,6 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGetStatus: CryptoCurrencyStatus, amountToSwap: String, fee: TxFee, - userWalletId: UserWalletId, ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) @@ -706,7 +698,7 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( txData = txData, - userWallet = requireNotNull(getSelectedWallet()), + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return SwapTransactionState.UnknownError }, network = currencyToSendStatus.currency.network, ) return result.fold( @@ -766,7 +758,6 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, txFee: TxFee, swapProvider: SwapProvider, - userWalletId: UserWalletId, ): SwapTransactionState { val exchangeData = repository.getExchangeData( fromContractAddress = currencyToSend.currency.getContractAddress(), @@ -787,8 +778,9 @@ internal class SwapInteractorImpl @Inject constructor( val exchangeDataCex = exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError - val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError - if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.UnknownError + val cardId = userWallet.scanResponse.card.cardId + + if (isDemoCardUseCase(cardId)) return SwapTransactionState.UnknownError val txData = createTransactionUseCase( amount = amount.value.convertToSdkAmount(currencyToSend.currency), @@ -811,7 +803,7 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( txData = txData, - userWallet = requireNotNull(getSelectedWallet()), + userWallet = userWallet, network = currencyToSend.currency.network, ) @@ -941,9 +933,8 @@ internal class SwapInteractorImpl @Inject constructor( txExternalUrl: String? = null, txExternalId: String? = null, ) { - val selectedWallet = getSelectedWallet() ?: return swapTransactionRepository.storeTransaction( - userWalletId = selectedWallet.walletId, + userWalletId = userWalletId, fromCryptoCurrency = currencyToSend.currency, toCryptoCurrency = currencyToGet.currency, transaction = SavedSwapTransactionModel( @@ -994,23 +985,16 @@ internal class SwapInteractorImpl @Inject constructor( spenderAddress: String, ): Boolean { if (fromToken is CryptoCurrency.Coin) return true - return getSelectedWalletSyncUseCase().fold( - ifRight = { userWallet -> - val allowance = repository.getAllowance( - userWalletId = userWallet.walletId, - networkId = networkId, - derivationPath = fromToken.network.derivationPath.value, - tokenDecimalCount = fromToken.decimals, - tokenAddress = getTokenAddress(fromToken), - spenderAddress = spenderAddress, - ) - allowance >= amount.value - }, - ifLeft = { - Timber.e("Swap Error on isAllowedToSpend") - false - }, + + val allowance = repository.getAllowance( + userWalletId = userWallet.walletId, + networkId = networkId, + derivationPath = fromToken.network.derivationPath.value, + tokenDecimalCount = fromToken.decimals, + tokenAddress = getTokenAddress(fromToken), + spenderAddress = spenderAddress, ) + return allowance >= amount.value } private suspend fun createEmptyAmountState(): SwapState { @@ -1040,13 +1024,11 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { - val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.let { userWallet -> - getUnhandledFee( - amount = amount.value, - userWallet = userWallet, - cryptoCurrency = fromToken, - ) - } + val txFeeResult = getUnhandledFee( + amount = amount.value, + userWallet = userWallet, + cryptoCurrency = fromToken, + ) val txFee = if (provider.type == ExchangeProviderType.CEX) { getFeeForCex(txFeeResult, fromTokenStatus) @@ -1196,7 +1178,6 @@ internal class SwapInteractorImpl @Inject constructor( is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee } val feePaidCurrency = getFeePaidCurrency( - userWalletId = requireNotNull(getSelectedWallet()).walletId, currency = fromToken, ) @@ -1256,7 +1237,6 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getFormattedFiatFees(fromToken: CryptoCurrency, vararg fees: BigDecimal): List { val appCurrency = getSelectedAppCurrencyUseCase.unwrap() val feePaidCurrency = getFeePaidCurrency( - userWalletId = requireNotNull(getSelectedWallet()).walletId, currency = fromToken, ) val feeCurrencyId: CryptoCurrency.ID = when (feePaidCurrency) { @@ -1308,7 +1288,6 @@ internal class SwapInteractorImpl @Inject constructor( val otherNativeFee = transaction.otherNativeFeeWei ?.movePointLeft(nativeCoinDecimals) ?: BigDecimal.ZERO - val userWallet = getSelectedWallet() val txFeeState = when ( val feeData = getFeeDataForDexSwap( networkId = networkId, @@ -1509,7 +1488,6 @@ internal class SwapInteractorImpl @Inject constructor( swapAmount = swapAmount, spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, ) - val userWallet = getSelectedWallet() val cardId = userWallet?.scanResponse?.card?.cardId val feeData = if (cardId != null && isDemoCardUseCase(cardId)) { getDemoFees(fromTokenStatus.currency) @@ -1846,7 +1824,6 @@ internal class SwapInteractorImpl @Inject constructor( ): Boolean { val tokenBalance = getTokenBalance(fromToken).value val feePaidCurrency = getFeePaidCurrency( - userWalletId = requireNotNull(getSelectedWallet()).walletId, currency = fromToken.currency, ) return when (feePaidCurrency) { @@ -1861,7 +1838,7 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency { + private suspend fun getFeePaidCurrency(currency: CryptoCurrency): FeePaidCurrency { return currenciesRepository.getFeePaidCurrency( userWalletId = userWalletId, currency = currency, @@ -1894,13 +1871,12 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromTokenStatus: CryptoCurrencyStatus, ): SwapFeeState { - val userWalletId = requireNotNull(getSelectedWallet()).walletId if (fee == null) { return SwapFeeState.NotEnough() } val percentsToFeeIncrease = BigDecimal.ONE - return when (val feePaidCurrency = getFeePaidCurrency(userWalletId, fromTokenStatus.currency)) { + return when (val feePaidCurrency = getFeePaidCurrency(fromTokenStatus.currency)) { FeePaidCurrency.Coin -> { val nativeTokenBalance = userWalletManager.getNativeTokenBalance( networkId, @@ -1994,21 +1970,13 @@ internal class SwapInteractorImpl @Inject constructor( swapAmount: SwapAmount? = null, spenderAddress: String, ): String { - return getSelectedWalletSyncUseCase().fold( - ifRight = { userWallet -> - repository.getApproveData( - userWalletId = userWallet.walletId, - networkId = networkId, - derivationPath = derivationPath, - currency = fromToken, - amount = swapAmount?.value, - spenderAddress = spenderAddress, - ) - }, - ifLeft = { - Timber.e("Swap Error on getApproveData") - error("Swap Error on getApproveData") - }, + return repository.getApproveData( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + currency = fromToken, + amount = swapAmount?.value, + spenderAddress = spenderAddress, ) } @@ -2059,4 +2027,9 @@ internal class SwapInteractorImpl @Inject constructor( private val normalDemoFee = "0.0002".toBigDecimal() private val priorityDemoFee = "0.0003".toBigDecimal() } + + @AssistedFactory + interface Factory : SwapInteractor.Factory { + override fun create(selectedWalletId: UserWalletId): SwapInteractorImpl + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 3f20bc09bc..9f95b3e0c6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,79 +1,34 @@ package com.tangem.feature.swap.domain.di -import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.CurrencyChecksRepository 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.* -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* -import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.lib.crypto.TransactionManager -import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Qualifier import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class SwapDomainModule { +internal class SwapDomainModule { + + @Provides + fun provideAllowPermissionsHandler(): AllowPermissionsHandler { + return AllowPermissionsHandlerImpl() + } @Provides @Singleton - fun provideSwapInteractor( - swapRepository: SwapRepository, - userWalletManager: UserWalletManager, - transactionManager: TransactionManager, - @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, - @SwapScope sendTransactionUseCase: SendTransactionUseCase, - @SwapScope createTransactionUseCase: CreateTransactionUseCase, - createTransactionDataExtrasUseCase: CreateTransactionDataExtrasUseCase, - isDemoCardUseCase: IsDemoCardUseCase, - quotesRepository: QuotesRepository, - swapTransactionRepository: SwapTransactionRepository, - appCurrencyRepository: AppCurrencyRepository, - currencyChecksRepository: CurrencyChecksRepository, - initialToCurrencyResolver: InitialToCurrencyResolver, - currenciesRepository: CurrenciesRepository, - validateTransactionUseCase: ValidateTransactionUseCase, - estimateFeeUseCase: EstimateFeeUseCase, - ): SwapInteractor { - return SwapInteractorImpl( - transactionManager = transactionManager, - userWalletManager = userWalletManager, - repository = swapRepository, - allowPermissionsHandler = AllowPermissionsHandlerImpl(), - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, - sendTransactionUseCase = sendTransactionUseCase, - createTransactionUseCase = createTransactionUseCase, - createTransactionExtrasUseCase = createTransactionDataExtrasUseCase, - isDemoCardUseCase = isDemoCardUseCase, - quotesRepository = quotesRepository, - swapTransactionRepository = swapTransactionRepository, - appCurrencyRepository = appCurrencyRepository, - currencyChecksRepository = currencyChecksRepository, - currenciesRepository = currenciesRepository, - initialToCurrencyResolver = initialToCurrencyResolver, - demoConfig = DemoConfig(), - validateTransactionUseCase = validateTransactionUseCase, - estimateFeeUseCase = estimateFeeUseCase, - ) + fun provideSwapInteractorFactory(factory: SwapInteractorImpl.Factory): SwapInteractor.Factory { + return factory } @Provides @@ -84,13 +39,6 @@ class SwapDomainModule { ) } - @SwapScope - @Provides - @Singleton - fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) - } - @Provides @Singleton fun providesGetCryptoCurrencyStatusUseCase( @@ -109,58 +57,10 @@ class SwapDomainModule { ) } - @SwapScope - @Provides - @Singleton - fun providesGetCardTokensListUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, - ): GetCardTokensListUseCase { - return GetCardTokensListUseCase( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } - - @SwapScope - @Provides - fun provideDemoCardUseCase(): IsDemoCardUseCase { - return IsDemoCardUseCase(config = DemoConfig()) - } - - @SwapScope - @Provides - @Singleton - fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { - return CreateTransactionUseCase( - transactionRepository = transactionRepository, - ) - } - - @SwapScope - @Provides - @Singleton - fun provideSendTransactionUseCase( - cardSdkConfigRepository: CardSdkConfigRepository, - transactionRepository: TransactionRepository, - walletManagersFacade: WalletManagersFacade, - ): SendTransactionUseCase { - return SendTransactionUseCase( - demoConfig = DemoConfig(), - cardSdkConfigRepository = cardSdkConfigRepository, - transactionRepository = transactionRepository, - walletManagersFacade = walletManagersFacade, - ) - } - @Provides @Singleton fun provideInitialToCurrencyResolver( - @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, swapTransactionRepository: SwapTransactionRepository, ): InitialToCurrencyResolver { return DefaultInitialToCurrencyResolver( @@ -168,8 +68,4 @@ class SwapDomainModule { swapTransactionRepository = swapTransactionRepository, ) } -} - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class SwapScope \ No newline at end of file +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 8361ef85c9..1958445de2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -25,7 +25,6 @@ import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor @@ -65,7 +64,7 @@ typealias SuccessLoadedSwapData = Map @Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class SwapViewModel @Inject constructor( - private val swapInteractor: SwapInteractor, + private val swapInteractorFactory: SwapInteractor.Factory, private val blockchainInteractor: BlockchainInteractor, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -79,7 +78,13 @@ internal class SwapViewModel @Inject constructor( private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Swap.CURRENCY_BUNDLE_KEY) ?.unbundle(CryptoCurrency.serializer()) - ?: error("no expected parameter CryptoCurrency found`") + ?: error("no expected parameter CryptoCurrency found") + + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Swap.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) + ?: error("no expected parameter UserWalletId found") + + private val swapInteractor = swapInteractorFactory.create(userWalletId) private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus @@ -126,15 +131,13 @@ internal class SwapViewModel @Inject constructor( init { viewModelScope.launch(dispatchers.io) { - swapInteractor.getSelectedWallet()?.let { - val cryptoCurrencyStatus = - getCryptoCurrencyStatusUseCase(it.walletId, initialCryptoCurrency.id).getOrNull() - if (cryptoCurrencyStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back) - } else { - initialCryptoCurrencyStatus = cryptoCurrencyStatus - initTokens() - } + val cryptoCurrencyStatus = + getCryptoCurrencyStatusUseCase(userWalletId, initialCryptoCurrency.id).getOrNull() + if (cryptoCurrencyStatus == null) { + uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back) + } else { + initialCryptoCurrencyStatus = cryptoCurrencyStatus + initTokens() } } } @@ -191,7 +194,6 @@ internal class SwapViewModel @Inject constructor( ), ) - val userWalletId = swapInteractor.getSelectedWallet()?.walletId ?: return@launch (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { subscribeToCoinBalanceUpdates( userWalletId = userWalletId, @@ -707,8 +709,6 @@ internal class SwapViewModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it)) } - val userWalletId = swapInteractor.getSelectedWallet()?.walletId - if (foundToken != null) { val fromToken: CryptoCurrencyStatus val toToken: CryptoCurrencyStatus @@ -717,7 +717,7 @@ internal class SwapViewModel @Inject constructor( toToken = initialCryptoCurrencyStatus val newToken = fromToken.currency as? CryptoCurrency.Coin - if (userWalletId != null && newToken != null) { + if (newToken != null) { subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = newToken, @@ -729,7 +729,7 @@ internal class SwapViewModel @Inject constructor( toToken = foundToken.currencyStatus val newToken = toToken.currency as? CryptoCurrency.Coin - if (userWalletId != null && newToken != null) { + if (newToken != null) { subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = newToken, @@ -985,12 +985,10 @@ internal class SwapViewModel @Inject constructor( } }, onBuyClick = { currency -> - swapInteractor.getSelectedWallet()?.let { userWallet -> - swapRouter.openTokenDetails( - userWalletId = userWallet.walletId, - currency = currency, - ) - } + swapRouter.openTokenDetails( + userWalletId = userWalletId, + currency = currency, + ) }, onRetryClick = { startLoadingQuotesFromLastState() @@ -1162,20 +1160,18 @@ internal class SwapViewModel @Inject constructor( } private fun updateWalletBalance() { - swapInteractor.getSelectedWallet()?.let { userWallet -> - dataState.fromCryptoCurrency?.currency?.network?.let { network -> - viewModelScope.launch { - withContext(NonCancellable) { - updateForBalance(userWallet, network) - } + dataState.fromCryptoCurrency?.currency?.network?.let { network -> + viewModelScope.launch { + withContext(NonCancellable) { + updateForBalance(userWalletId, network) } } } } - private suspend fun updateForBalance(userWallet: UserWallet, network: Network) { + private suspend fun updateForBalance(userWalletId: UserWalletId, network: Network) { updateDelayedCurrencyStatusUseCase( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, network = network, delayMillis = UPDATE_BALANCE_DELAY_MILLIS, refresh = true, 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 d6c7da21d3..f959f9632a 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 @@ -6,6 +6,7 @@ import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -126,6 +127,7 @@ internal class TokenDetailsViewModel @Inject constructor( tokenDetailsFeatureToggles: TokenDetailsFeatureToggles, deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, + private val appRouter: AppRouter, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.CurrencyDetails.USER_WALLET_ID_KEY) @@ -616,7 +618,7 @@ internal class TokenDetailsViewModel @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason)) return - reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrency)) + appRouter.push(AppRoute.Swap(currency = cryptoCurrency, userWalletId = userWalletId)) } override fun onDismissDialog() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index d924dabe10..95f595916e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -77,7 +77,13 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Swap -> { title = resourceReference(R.string.swapping_swap_action) icon = R.drawable.ic_exchange_horizontal_24 - action = { clickIntents.onSwapClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) } + action = { + clickIntents.onSwapClick( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = userWallet.walletId, + unavailabilityReason = ScenarioUnavailabilityReason.None, + ) + } } is TokenActionsState.ActionState.CopyAddress -> { title = resourceReference(R.string.common_copy_address) 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 cb24122b08..f00e79aa55 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 @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -57,7 +59,11 @@ interface WalletCurrencyActionsClickIntents { fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) - fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) + fun onSwapClick( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWalletId: UserWalletId, + unavailabilityReason: ScenarioUnavailabilityReason, + ) fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) @@ -95,6 +101,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val getYieldUseCase: GetYieldUseCase, + private val appRouter: AppRouter, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -352,6 +359,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, + userWalletId: UserWalletId, unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( @@ -360,7 +368,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason)) return - reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrencyStatus.currency)) + appRouter.push( + AppRoute.Swap( + currency = cryptoCurrencyStatus.currency, + userWalletId = userWalletId, + ), + ) } override fun onExploreClick() { From 5cd0d0b878f23a06c498f9523a4c7c1442ad17c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 12:17:42 +0400 Subject: [PATCH 81/90] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 18 +++++++++-- .../DefaultCustomTokensRepository.kt | 30 +++++++++++++++++++ .../GetSupportedNetworksUseCase.kt | 27 +++++++++++++++++ .../ValidateDerivationPathUseCase.kt | 24 +++++++++++++++ .../DerivationPathValidationException.kt | 8 +++++ .../exceptoin/SupportedBlockchainException.kt | 8 +++++ .../repository/CustomTokensRepository.kt | 4 +++ 7 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 576da07163..02bb0f9562 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,8 +1,6 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.domain.managetokens.SaveManagedTokensUseCase -import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -67,4 +65,20 @@ internal object ManageTokensDomainModule { derivationsRepository = derivationsRepository, ) } + + @Provides + @Singleton + fun provideGetSupportedNetworksUseCase( + customTokensRepository: CustomTokensRepository, + ): GetSupportedNetworksUseCase { + return GetSupportedNetworksUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideValidateDerivationPathUseCase( + customTokensRepository: CustomTokensRepository, + ): ValidateDerivationPathUseCase { + return ValidateDerivationPathUseCase(customTokensRepository) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 0ec95b565f..752cef4833 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.managetokens import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.UserTokensResponseFactory @@ -16,8 +17,10 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.canHandleBlockchain import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository @@ -178,6 +181,33 @@ internal class DefaultCustomTokensRepository( } } + override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + ?: error("User wallet not found") + val scanResponse = userWallet.scanResponse + + Blockchain.entries + .mapNotNull { blockchain -> + if (scanResponse.card.canHandleBlockchain(blockchain, scanResponse.cardTypesResolver)) { + getNetwork( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = scanResponse.derivationStyleProvider, + ) + } else { + null + } + } + } + + override fun createDerivationPath(rawPath: String): Network.DerivationPath { + val sdkPath = DerivationPath(rawPath) + + return Network.DerivationPath.Custom( + value = sdkPath.rawPath, + ) + } + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) appPreferencesStore.storeObject( diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt new file mode 100644 index 0000000000..1074a21b02 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.tangem.domain.managetokens.model.exceptoin.SupportedBlockchainException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class GetSupportedNetworksUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either> { + return either { + val networks = catch({ repository.getSupportedNetworks(userWalletId) }) { + raise(SupportedBlockchainException.DataError(it)) + } + + ensureNotNull(networks.takeIf { it.isNotEmpty() }) { + SupportedBlockchainException.EmptyList + } + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt new file mode 100644 index 0000000000..deaa42edf9 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network + +class ValidateDerivationPathUseCase( + private val repository: CustomTokensRepository, +) { + + operator fun invoke(rawValue: String): Either { + return either { + ensure(rawValue.isNotEmpty()) { DerivationPathValidationException.Empty } + + catch({ repository.createDerivationPath(rawValue) }) { + raise(DerivationPathValidationException.Invalid) + } + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt new file mode 100644 index 0000000000..fdda23776c --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class DerivationPathValidationException { + + data object Empty : DerivationPathValidationException() + + data object Invalid : DerivationPathValidationException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt new file mode 100644 index 0000000000..946e3cd9b4 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class SupportedBlockchainException { + + data object EmptyList : SupportedBlockchainException() + + data class DataError(val cause: Throwable) : SupportedBlockchainException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index 5f84209200..c0f898207e 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -33,4 +33,8 @@ interface CustomTokensRepository { ): CryptoCurrency.Token suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) + + suspend fun getSupportedNetworks(userWalletId: UserWalletId): List + + fun createDerivationPath(rawPath: String): Network.DerivationPath } \ No newline at end of file From 3cf222aa32e516f80a7d0c2d366be2e9e83be267 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 12:53:14 +0400 Subject: [PATCH 82/90] Updated on 2026-08-14 --- .../CustomTokenDerivationInputComponent.kt | 3 +- ...aultCustomTokenDerivationInputComponent.kt | 127 ++++++++++++++++++ .../impl/DefaultManageTokensComponent.kt | 8 +- .../managetokens/di/ComponentModule.kt | 16 +-- .../CustomTokenSelectorDialogConfig.kt | 13 ++ ...ig.kt => ManageTokensBottomSheetConfig.kt} | 4 +- .../managetokens/model/ManageTokensModel.kt | 6 +- 7 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt rename features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/{BottomSheetConfig.kt => ManageTokensBottomSheetConfig.kt} (73%) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt index 1530c3d205..383d94f073 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt @@ -3,12 +3,13 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent { data class Params( val userWalletId: UserWalletId, - val onConfirm: (String) -> Unit, + val onConfirm: (SelectedDerivationPath) -> Unit, val onDismiss: () -> Unit, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt new file mode 100644 index 0000000000..0695290569 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt @@ -0,0 +1,127 @@ +package com.tangem.features.managetokens.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import arrow.core.getOrElse +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.managetokens.ValidateDerivationPathUseCase +import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent +import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.dialog.CustomDerivationInputDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* + +internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: CustomTokenDerivationInputComponent.Params, + private val validateDerivationPathUseCase: ValidateDerivationPathUseCase, +) : CustomTokenDerivationInputComponent, AppComponentContext by context { + + private val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + init { + observeValueUpdates() + } + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun Dialog() { + val state by state.collectAsStateWithLifecycle() + + CustomDerivationInputDialog( + model = state, + onDismiss = ::dismiss, + ) + } + + @OptIn(FlowPreview::class) + private fun observeValueUpdates() { + state + .map { it.value.text } + .distinctUntilChanged() + .sample(periodMillis = 1_000) + .onEach(::validateValue) + .launchIn(componentScope) + } + + private fun getInitialState(): CustomDerivationInputUM = CustomDerivationInputUM( + value = TextFieldValue(), + error = null, + updateValue = ::updateValue, + isConfirmEnabled = false, + onConfirm = ::confirm, + ) + + private fun validateValue(value: String) { + validateDerivationPathUseCase(value).getOrElse { e -> + updateWithValidationError(e) + return + } + + state.update { state -> + state.copy( + error = null, + isConfirmEnabled = true, + ) + } + } + + private fun updateWithValidationError(e: DerivationPathValidationException) { + state.update { state -> + state.copy( + error = when (e) { + DerivationPathValidationException.Empty -> null + DerivationPathValidationException.Invalid -> { + resourceReference(R.string.custom_token_invalid_derivation_path) + } + }, + isConfirmEnabled = false, + ) + } + } + + private fun updateValue(value: TextFieldValue) { + state.update { state -> + state.copy(value = value) + } + } + + private fun confirm() { + if (state.value.error != null && !state.value.isConfirmEnabled) { + return + } + + val value = state.value.value.text + val model = SelectedDerivationPath( + id = null, + value = Network.DerivationPath.Custom(value), + networkName = stringReference(value = value), + ) + + params.onConfirm(model) + } + + @AssistedFactory + interface Factory : CustomTokenDerivationInputComponent.Factory { + override fun create( + context: AppComponentContext, + params: CustomTokenDerivationInputComponent.Params, + ): DefaultCustomTokenDerivationInputComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index 98df4a4346..2b513d23cb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -15,7 +15,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig +import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.model.ManageTokensModel import com.tangem.features.managetokens.ui.ManageTokensScreen import dagger.assisted.Assisted @@ -32,7 +32,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, - serializer = BottomSheetConfig.serializer(), + serializer = ManageTokensBottomSheetConfig.serializer(), handleBackButton = false, childFactory = ::bottomSheetChild, ) @@ -53,10 +53,10 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( } private fun bottomSheetChild( - config: BottomSheetConfig, + config: ManageTokensBottomSheetConfig, componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { - is BottomSheetConfig.AddCustomToken -> { + is ManageTokensBottomSheetConfig.AddCustomToken -> { addCustomTokenComponentFactory.create( context = childByContext(componentContext), params = AddCustomTokenComponent.Params( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt index 9de5afa38c..ea21e92af1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt @@ -1,13 +1,7 @@ package com.tangem.features.managetokens.di -import com.tangem.features.managetokens.component.AddCustomTokenComponent -import com.tangem.features.managetokens.component.CustomTokenFormComponent -import com.tangem.features.managetokens.component.CustomTokenSelectorComponent -import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.component.impl.DefaultAddCustomTokenComponent -import com.tangem.features.managetokens.component.impl.DefaultCustomTokenFormComponent -import com.tangem.features.managetokens.component.impl.DefaultCustomTokenSelectorComponent -import com.tangem.features.managetokens.component.impl.DefaultManageTokensComponent +import com.tangem.features.managetokens.component.* +import com.tangem.features.managetokens.component.impl.* import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -39,4 +33,10 @@ internal interface ComponentModule { fun bindCustomTokenFormComponentFactory( factory: DefaultCustomTokenFormComponent.Factory, ): CustomTokenFormComponent.Factory + + @Binds + @Singleton + fun bindCustomTokenDerivationInputComponentFactory( + factory: DefaultCustomTokenDerivationInputComponent.Factory, + ): CustomTokenDerivationInputComponent.Factory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt new file mode 100644 index 0000000000..19b090720b --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.features.managetokens.entity.customtoken + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class CustomTokenSelectorDialogConfig { + + @Serializable + data class CustomDerivationInput( + val userWalletId: UserWalletId, + ) : CustomTokenSelectorDialogConfig() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt similarity index 73% rename from features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt rename to features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt index 102140a57c..96f7553288 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt @@ -4,10 +4,10 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @Serializable -internal sealed class BottomSheetConfig { +internal sealed class ManageTokensBottomSheetConfig { @Serializable data class AddCustomToken( val userWalletId: UserWalletId, - ) : BottomSheetConfig() + ) : ManageTokensBottomSheetConfig() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index b5a235ee3a..581f40fc1b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -18,7 +18,7 @@ import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM -import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig +import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R @@ -47,7 +47,7 @@ internal class ManageTokensModel @Inject constructor( private val params: ManageTokensComponent.Params = paramsContainer.require() val state: MutableStateFlow = MutableStateFlow(getInitialState(params.userWalletId)) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { manageTokensListManager.uiItems @@ -228,7 +228,7 @@ internal class ManageTokensModel @Inject constructor( private fun navigateToAddCustomToken() { params.userWalletId?.let { - bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it)) + bottomSheetNavigation.activate(ManageTokensBottomSheetConfig.AddCustomToken(it)) } } From a99b3372700504a4e2dfc35f6ccd22c4f0e28043 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 13:07:15 +0400 Subject: [PATCH 83/90] Updated on 2026-08-14 --- .../component/CustomTokenFormComponent.kt | 2 +- .../component/CustomTokenSelectorComponent.kt | 1 + .../impl/DefaultAddCustomTokenComponent.kt | 15 +- .../DefaultCustomTokenSelectorComponent.kt | 40 +++- .../preview/PreviewAddCustomTokenComponent.kt | 3 +- .../PreviewCustomTokenSelectorComponent.kt | 7 +- .../features/managetokens/di/ModelModule.kt | 12 ++ .../customtoken/AddCustomTokenConfig.kt | 2 +- .../customtoken/CustomTokenSelectorUM.kt | 1 + .../model/CustomTokenFormModel.kt | 11 +- .../model/CustomTokenSelectorModel.kt | 172 ++++++++++++++++++ .../utils/mapper/CurrencyNetworksMapper.kt | 20 +- .../utils/mapper/DerivationPathMapper.kt | 34 ++++ 13 files changed, 296 insertions(+), 24 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index e9fc3c448f..b7c219e560 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -12,7 +12,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val network: SelectedNetwork, - val derivationPath: SelectedDerivationPath, + val derivationPath: SelectedDerivationPath?, val formValues: CustomTokenFormValues, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt index b3e4a159a0..d1f5e8aaf6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt @@ -18,6 +18,7 @@ internal interface CustomTokenSelectorComponent : ComposableContentComponent { data class DerivationPathSelector( val userWalletId: UserWalletId, + val selectedNetwork: SelectedNetwork, val selectedDerivationPath: SelectedDerivationPath?, val onDerivationPathSelected: (SelectedDerivationPath) -> Unit, ) : Params() diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 3679e6f929..bde22e12c6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -12,7 +12,6 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.component.CustomTokenSelectorComponent @@ -20,7 +19,6 @@ import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork -import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -110,6 +108,9 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( context = childByContext(componentContext), params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = config.userWalletId, + selectedNetwork = requireNotNull(config.selectedNetwork) { + "Network is not selected" + }, selectedDerivationPath = config.selectedDerivationPath, onDerivationPathSelected = { derivationPath -> showForm(derivationPath = derivationPath) @@ -122,12 +123,10 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( context = childByContext(componentContext), params = CustomTokenFormComponent.Params( userWalletId = config.userWalletId, - network = config.selectedNetwork ?: error("Network is not selected"), - derivationPath = config.selectedDerivationPath ?: SelectedDerivationPath( - id = config.selectedNetwork.id, - value = config.selectedNetwork.derivationPath, - networkName = resourceReference(R.string.custom_token_derivation_path_default), - ), + network = requireNotNull(config.selectedNetwork) { + "Network is not selected" + }, + derivationPath = config.selectedDerivationPath, formValues = config.formValues, onSelectNetworkClick = ::showNetworkSelector, onSelectDerivationPathClick = ::showDerivationPathSelector, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt index c748d7363f..2d690a862c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt @@ -4,33 +4,61 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent import com.tangem.features.managetokens.component.CustomTokenSelectorComponent -import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent -import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig +import com.tangem.features.managetokens.model.CustomTokenSelectorModel import com.tangem.features.managetokens.ui.CustomTokenSelectorContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.MutableStateFlow internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted params: CustomTokenSelectorComponent.Params, + private val customTokenDerivationInputComponentFactory: CustomTokenDerivationInputComponent.Factory, ) : CustomTokenSelectorComponent, AppComponentContext by context { - private val state: MutableStateFlow = MutableStateFlow( - value = PreviewCustomTokenSelectorComponent(params = params).previewState, + private val model: CustomTokenSelectorModel = getOrCreateModel(params) + private val dialogSlot = childSlot( + source = model.dialogNavigation, + serializer = CustomTokenSelectorDialogConfig.serializer(), + childFactory = ::createDialog, ) + private fun createDialog( + config: CustomTokenSelectorDialogConfig, + context: ComponentContext, + ): ComposableDialogComponent = when (config) { + is CustomTokenSelectorDialogConfig.CustomDerivationInput -> customTokenDerivationInputComponentFactory.create( + context = childByContext(context), + params = CustomTokenDerivationInputComponent.Params( + userWalletId = config.userWalletId, + onConfirm = model::selectCustomDerivationPath, + onDismiss = model.dialogNavigation::dismiss, + ), + ) + } + @Composable override fun Content(modifier: Modifier) { - val state by state.collectAsStateWithLifecycle() + val state by model.state.collectAsStateWithLifecycle() + val dialog by dialogSlot.subscribeAsState() CustomTokenSelectorContent( modifier = modifier, model = state, ) + + dialog.child?.instance?.Dialog() } @AssistedFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index a09d7b2b93..79d2f3bdcd 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -60,7 +60,8 @@ internal class PreviewAddCustomTokenComponent( PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = state.userWalletId, - selectedDerivationPath = state.selectedDerivationPath, + selectedNetwork = state.selectedNetwork!!, + selectedDerivationPath = state.selectedDerivationPath!!, onDerivationPathSelected = {}, ), ).Content(modifier) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 1ebcff8b5d..0a37116d7d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -35,7 +35,7 @@ internal class PreviewCustomTokenSelectorComponent( ) DerivationPathUM( - id = d.id.value, + id = d.id?.value ?: "", value = d.value.value.orEmpty(), networkName = d.networkName, isSelected = d.value == params.selectedDerivationPath?.value, @@ -75,7 +75,10 @@ internal class PreviewCustomTokenSelectorComponent( val previewState = CustomTokenSelectorUM( header = when (params) { - is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton({}) + is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton( + value = null, + onClick = {}, + ) is Params.NetworkSelector -> if (params.selectedNetwork == null) { CustomTokenSelectorUM.HeaderUM.Description } else { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt index 572c2624cb..0f33d5994f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt @@ -2,6 +2,8 @@ package com.tangem.features.managetokens.di import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.managetokens.model.CustomTokenFormModel +import com.tangem.features.managetokens.model.CustomTokenSelectorModel import com.tangem.features.managetokens.model.ManageTokensModel import dagger.Binds import dagger.Module @@ -17,4 +19,14 @@ internal interface ModelModule { @IntoMap @ClassKey(ManageTokensModel::class) fun provideManageTokensModel(model: ManageTokensModel): Model + + @Binds + @IntoMap + @ClassKey(CustomTokenFormModel::class) + fun provideCustomTokenFormModel(model: CustomTokenFormModel): Model + + @Binds + @IntoMap + @ClassKey(CustomTokenSelectorModel::class) + fun provideCustomTokenSelectorModel(model: CustomTokenSelectorModel): Model } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt index 6a43cdc40d..1c5de5c702 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt @@ -34,7 +34,7 @@ internal data class SelectedNetwork( @Serializable internal data class SelectedDerivationPath( - val id: Network.ID, + val id: Network.ID?, val value: Network.DerivationPath, val networkName: TextReference, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt index 25c08fac10..d607a8b959 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt @@ -17,6 +17,7 @@ internal data class CustomTokenSelectorUM( data object Description : HeaderUM() data class CustomDerivationButton( + val value: String?, val onClick: () -> Unit, ) : HeaderUM() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 25fbf11d48..2fa77b8e92 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -14,6 +14,7 @@ import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM @@ -57,7 +58,7 @@ internal class CustomTokenFormModel @Inject constructor( customCurrencyValidator.createCoin( userWalletId = params.userWalletId, networkId = params.network.id, - derivationPath = params.derivationPath.value, + derivationPath = getDerivationPath(), ) } } @@ -77,7 +78,7 @@ internal class CustomTokenFormModel @Inject constructor( }, derivationPath = ClickableFieldUM( label = resourceReference(R.string.custom_token_derivation_path), - value = if (params.derivationPath.id == params.network.id) { + value = if (params.derivationPath == null || params.derivationPath.id == params.network.id) { resourceReference(R.string.custom_token_derivation_path_default) } else { params.derivationPath.networkName @@ -104,7 +105,7 @@ internal class CustomTokenFormModel @Inject constructor( customCurrencyValidator.validateForm( userWalletId = params.userWalletId, networkId = params.network.id, - derivationPath = params.derivationPath.value, + derivationPath = getDerivationPath(), formValues = formValues, ) } @@ -266,6 +267,10 @@ internal class CustomTokenFormModel @Inject constructor( return formValues.fillValues(form) } + private fun getDerivationPath(): Network.DerivationPath { + return params.derivationPath?.value ?: params.network.derivationPath + } + private fun updateContractAddress(value: String) { state.update { state -> state.updateTokenForm { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt new file mode 100644 index 0000000000..611c6a1fe9 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -0,0 +1,172 @@ +package com.tangem.features.managetokens.model + +import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.managetokens.GetSupportedNetworksUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.DerivationPathSelector +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.NetworkSelector +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM.HeaderUM +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork +import com.tangem.features.managetokens.entity.item.DerivationPathUM +import com.tangem.features.managetokens.entity.item.SelectableItemUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel +import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class CustomTokenSelectorModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase, + private val messageSender: UiMessageSender, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: CustomTokenSelectorComponent.Params = paramsContainer.require() + + val dialogNavigation: SlotNavigation = SlotNavigation() + + val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + init { + loadItems() + } + + private fun getInitialState(): CustomTokenSelectorUM = when (params) { + is NetworkSelector -> CustomTokenSelectorUM( + header = if (params.selectedNetwork == null) { + HeaderUM.Description + } else { + HeaderUM.None + }, + items = persistentListOf(), + ) + is DerivationPathSelector -> CustomTokenSelectorUM( + header = HeaderUM.CustomDerivationButton( + value = (params.selectedDerivationPath?.value as? Network.DerivationPath.Custom)?.value, + onClick = ::showCustomDerivationInput, + ), + items = persistentListOf(), + ) + } + + private fun loadItems() = modelScope.launch { + val items = when (params) { + is NetworkSelector -> loadNetworks(params).toImmutableList() + is DerivationPathSelector -> loadDerivationPaths(params).toImmutableList() + } + + state.update { state -> + state.copy(items = items) + } + } + + private suspend fun loadNetworks(selector: NetworkSelector): List { + return getSupportedNetworks(selector.userWalletId).map { network -> + network.toCurrencyNetworkModel( + isSelected = network.id == selector.selectedNetwork?.id, + onSelectedStateChange = { + val model = SelectedNetwork( + id = network.id, + name = stringReference(network.name), + derivationPath = network.derivationPath, + canHandleTokens = network.canHandleTokens, + ) + + selector.onNetworkSelected(model) + }, + ) + } + } + + private suspend fun loadDerivationPaths(selector: DerivationPathSelector): List { + val derivationPaths = mutableListOf() + val defaultPath = selector.selectedNetwork.let { network -> + network.toDerivationPathModel( + isSelected = network.id == selector.selectedDerivationPath?.id, + onSelectedStateChange = { + val model = SelectedDerivationPath( + id = network.id, + networkName = resourceReference(R.string.custom_token_derivation_path_default), + value = network.derivationPath, + ) + + selector.onDerivationPathSelected(model) + }, + ) + } + + if (defaultPath != null) { + derivationPaths.add(defaultPath) + } + + getSupportedNetworks(selector.userWalletId) + .mapNotNullTo(derivationPaths) { network -> + if (network.id == selector.selectedNetwork.id) { + return@mapNotNullTo null // Skip default path + } + + network.toDerivationPathModel( + isSelected = network.id == selector.selectedDerivationPath?.id, + onSelectedStateChange = { + val model = SelectedDerivationPath( + id = network.id, + networkName = stringReference(network.name), + value = network.derivationPath, + ) + + selector.onDerivationPathSelected(model) + }, + ) + } + + return derivationPaths + } + + private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List { + return getSupportedNetworksUseCase(userWalletId).getOrElse { e -> + val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)) + messageSender.send(message) + + emptyList() + } + } + + private fun showCustomDerivationInput() { + val config = when (params) { + is NetworkSelector -> return + is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.userWalletId) + } + + dialogNavigation.activate(config) + } + + fun selectCustomDerivationPath(value: SelectedDerivationPath) { + when (params) { + is NetworkSelector -> return + is DerivationPathSelector -> params.onDerivationPathSelected(value) + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt index 6fdacc516a..2e88d82956 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.utils.mapper import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.utils.ui.getIconRes @@ -15,7 +16,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( return if (isExpanded) { NetworksUM.Expanded( networks = availableNetworks.map { - it.toUiModel( + it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, @@ -27,7 +28,22 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( } } -private fun SourceNetwork.toUiModel( +internal fun Network.toCurrencyNetworkModel( + isSelected: Boolean, + onSelectedStateChange: (Boolean) -> Unit, +): CurrencyNetworkUM { + return CurrencyNetworkUM( + network = this, + name = name, + iconResId = id.getIconRes(isColored = true), + isSelected = isSelected, + type = standardType.name, + isMainNetwork = false, + onSelectedStateChange = onSelectedStateChange, + ) +} + +private fun SourceNetwork.toCurrencyNetworkModel( isSelected: Boolean, isEditable: Boolean, onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt new file mode 100644 index 0000000000..2e843ec716 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt @@ -0,0 +1,34 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork +import com.tangem.features.managetokens.entity.item.DerivationPathUM +import com.tangem.features.managetokens.impl.R + +internal fun Network.toDerivationPathModel( + isSelected: Boolean, + onSelectedStateChange: (Boolean) -> Unit, +): DerivationPathUM? { + return DerivationPathUM( + id = id.value, + value = derivationPath.value ?: return null, + networkName = stringReference(name), + isSelected = isSelected, + onSelectedStateChange = onSelectedStateChange, + ) +} + +internal fun SelectedNetwork.toDerivationPathModel( + isSelected: Boolean, + onSelectedStateChange: (Boolean) -> Unit, +): DerivationPathUM? { + return DerivationPathUM( + id = id.value, + value = derivationPath.value ?: return null, + networkName = resourceReference(R.string.custom_token_derivation_path_default), + isSelected = isSelected, + onSelectedStateChange = onSelectedStateChange, + ) +} \ No newline at end of file From f78d948799d5a241446cbd49ac626e87f89fc1d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 18:06:37 +0400 Subject: [PATCH 84/90] Updated on 2026-08-14 --- .../ui/CustomTokenSelectorContent.kt | 104 ++++++++++++------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 5984b55cf3..476433fb7d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -35,6 +35,7 @@ import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.entity.item.DerivationPathUM import com.tangem.features.managetokens.impl.R @@ -110,6 +111,8 @@ private fun Header(header: CustomTokenSelectorUM.HeaderUM, modifier: Modifier = is CustomTokenSelectorUM.HeaderUM.CustomDerivationButton -> { CustomDerivationButton( modifier = modifier.padding(vertical = TangemTheme.dimens.spacing16), + enteredDerivationPath = header.value, + isSelected = header.value != null, onClick = header.onClick, ) } @@ -164,38 +167,41 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) ) }, action = { - AnimatedVisibility( - modifier = Modifier.size(TangemTheme.dimens.size24), - visible = model.isSelected, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } + SelectedIcon(isVisible = model.isSelected) }, ) } @Composable -private fun CustomDerivationButton(onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun CustomDerivationButton( + enteredDerivationPath: String?, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { InformationBlock( - modifier = modifier.clickable(onClick = onClick), + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = onClick), title = { - Text( - text = stringResource(id = R.string.custom_token_custom_derivation), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = stringResource(id = R.string.custom_token_custom_derivation), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = enteredDerivationPath ?: stringResource(id = R.string.custom_token_custom_derivation_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } }, - content = { - Text( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = stringResource(id = R.string.custom_token_custom_derivation_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + action = { + SelectedIcon(isVisible = isSelected) }, ) } @@ -206,23 +212,43 @@ private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Mod modifier = modifier, shape = RectangleShape, title = { - Text( - text = model.networkName.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = model.networkName.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = model.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } }, - content = { - Text( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = model.value, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + action = { + SelectedIcon(isVisible = model.isSelected) }, ) } +@Composable +private fun SelectedIcon(isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier.size(TangemTheme.dimens.size24), + visible = isVisible, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) @@ -243,6 +269,12 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = SelectedNetwork( + id = Network.ID(value = "0"), + name = stringReference("Ethereum"), + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + canHandleTokens = true, + ), selectedDerivationPath = SelectedDerivationPath( id = Network.ID(value = "0"), value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), From b37227d5331393f94ef9b59fdea3323712be338e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 19:04:31 +0400 Subject: [PATCH 85/90] Updated on 2026-08-14 --- .../tangem/tap/di/domain/CardDomainModule.kt | 8 ++++ .../card/DefaultDerivationsRepository.kt | 24 ++++++++++ .../card/HasMissedDerivationsUseCase.kt | 23 ++++++++++ .../card/repository/DerivationsRepository.kt | 6 +++ features/markets/impl/build.gradle.kts | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 18 ++++---- .../impl/model/MarketsPortfolioModel.kt | 38 +++++++++++++-- .../impl/model/MyPortfolioUMMFactory.kt | 46 ++++++++----------- .../portfolio/impl/model/PortfolioUIData.kt | 20 ++++++++ 9 files changed, 144 insertions(+), 40 deletions(-) create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index c74a09521c..ce9ed6e593 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -92,4 +92,12 @@ internal object CardDomainModule { fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase { return NetworkHasDerivationUseCase() } + + @Provides + @Singleton + fun provideIsRequiredDerivePublicKeysUseCase( + derivationsRepository: DerivationsRepository, + ): HasMissedDerivationsUseCase { + return HasMissedDerivationsUseCase(derivationsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index a5d6f883b7..4390c3ca9f 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -1,5 +1,7 @@ package com.tangem.tap.domain.card +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.core.TangemSdkError @@ -8,8 +10,10 @@ import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -52,6 +56,26 @@ internal class DefaultDerivationsRepository( derivePublicKeys(userWalletId = userWalletId, derivations = derivations) } + override suspend fun hasMissedDerivations( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") + + val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse) + .findByNetworks( + networksWithDerivationPath.mapNotNull { (networkId, extraDerivationPath) -> + getNetwork( + blockchain = Blockchain.fromNetworkId(networkId.value) ?: return@mapNotNull null, + extraDerivationPath = extraDerivationPath, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) + }, + ) + + return derivations.isNotEmpty() + } + override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys { tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) .doOnSuccess { response -> diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt new file mode 100644 index 0000000000..5d481fe1ca --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case to check if user has missed derivations + * +[REDACTED_AUTHOR] + */ +class HasMissedDerivationsUseCase( + private val derivationsRepository: DerivationsRepository, +) { + + /** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend operator fun invoke( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean { + return derivationsRepository.hasMissedDerivations(userWalletId, networksWithDerivationPath) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt index 5c6512a557..c4abbb8284 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt @@ -20,4 +20,10 @@ interface DerivationsRepository { userWalletId: UserWalletId, derivations: Map>, ): Map + + /** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend fun hasMissedDerivations( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 410e6dbd11..3fc641b0da 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.card) implementation(projects.domain.markets) implementation(projects.domain.staking.models) implementation(projects.domain.tokens) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 9983cd1bb2..6b8140850f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -36,21 +36,21 @@ internal class AddToPortfolioBSContentUMFactory( /** * Create [TangemBottomSheetConfig] * - * @param portfolioData portfolio data - * @param portfolioBSVisibilityModel portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param networksWithToggle networks with toggle - * @param isUserChangedNetworks flag indicates if user changed networks + * @param portfolioData portfolio data + * @param portfolioUIData portfolio bottom sheet visibility model + * @param selectedWallet selected wallet + * @param networksWithToggle networks with toggle + * @param isUserChangedNetworks flag indicates if user changed networks */ fun create( portfolioData: PortfolioData, - portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + portfolioUIData: PortfolioUIData, selectedWallet: UserWallet, networksWithToggle: Map, isUserChangedNetworks: Boolean, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( - isShow = portfolioBSVisibilityModel.addToPortfolioBSVisibility, + isShow = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, content = AddToPortfolioBSContentUM( selectedWallet = selectedWallet.toSelectedUserWalletItemUM(), @@ -58,11 +58,11 @@ internal class AddToPortfolioBSContentUMFactory( networksWithToggle = networksWithToggle, onNetworkSwitchClick = onNetworkSwitchClick, ).convert(value = token), - isScanCardNotificationVisible = false, // TODO [REDACTED_JIRA] + isScanCardNotificationVisible = portfolioUIData.hasMissedDerivations, continueButtonEnabled = isUserChangedNetworks, onContinueButtonClick = onContinueClick, walletSelectorConfig = crateWalletSelectorBSConfig( - isShow = portfolioBSVisibilityModel.walletSelectorBSVisibility, + isShow = portfolioUIData.portfolioBSVisibilityModel.walletSelectorBSVisibility, portfolioData = portfolioData, selectedWalletId = selectedWallet.walletId, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 219da20190..5d647ae942 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -5,8 +5,10 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent @@ -24,6 +26,7 @@ internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val portfolioDataLoader: PortfolioDataLoader, + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -85,16 +88,45 @@ internal class MarketsPortfolioModel @Inject constructor( private fun subscribeOnStateUpdates() { combine( flow = portfolioDataLoader.load(params.token.id), - flow2 = portfolioBSVisibilityModelFlow, + flow2 = getPortfolioUIDataFlow(), flow3 = availableNetworksFlow, - flow4 = selectedMultiWalletIdFlow, - flow5 = walletsWithChangedNetworksFlow, transform = factory::create, ) .onEach { _state.value = it } .launchIn(modelScope) } + private fun getPortfolioUIDataFlow(): Flow { + return combine( + flow = portfolioBSVisibilityModelFlow, + flow2 = selectedMultiWalletIdFlow, + flow3 = walletsWithChangedNetworksFlow, + transform = { portfolioBSVisibilityModel, selectedWalletId, walletsWithChangedNetworks -> + PortfolioUIData( + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + selectedWalletId = selectedWalletId, + walletsWithChangedNetworks = walletsWithChangedNetworks, + hasMissedDerivations = hasMissedDerivations(selectedWalletId, walletsWithChangedNetworks), + ) + }, + ) + } + + private suspend fun hasMissedDerivations( + selectedWalletId: UserWalletId?, + walletsWithChangedNetworks: Map>, + ): Boolean { + return if (selectedWalletId != null) { + hasMissedDerivationsUseCase.invoke( + userWalletId = selectedWalletId, + networksWithDerivationPath = walletsWithChangedNetworks[selectedWalletId].orEmpty() + .associate { Network.ID(it) to null }, + ) + } else { + false + } + } + private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) { _state.update { val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt index 91072c3906..109e54ad83 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt @@ -5,7 +5,6 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @@ -24,17 +23,15 @@ internal class MyPortfolioUMMFactory( ) { fun create( - portfolioModel: PortfolioData, - portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + portfolioData: PortfolioData, + portfolioUIData: PortfolioUIData, availableNetworks: List?, - selectedWalletId: UserWalletId?, - walletsWithChangedNetworks: Map>, ): MyPortfolioUM { if (availableNetworks == null) return MyPortfolioUM.Loading if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable - val walletsWithStatuses = portfolioModel.walletsWithCurrencyStatuses + val walletsWithStatuses = portfolioData.walletsWithCurrencyStatuses .filterAvailableNetworks(networks = availableNetworks) val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty() @@ -44,11 +41,9 @@ internal class MyPortfolioUMMFactory( return if (hasMultiWallets) { MyPortfolioUM.AddFirstToken( bsConfig = createAddToPortfolioBSConfig( - portfolioModel = portfolioModel, - selectedWalletId = selectedWalletId, + portfolioData = portfolioData, + portfolioUIData = portfolioUIData, availableNetworks = availableNetworks, - walletsWithChangedNetworks = walletsWithChangedNetworks, - portfolioBSVisibilityModel = portfolioBSVisibilityModel, ), onAddClick = onAddClick, ) @@ -58,15 +53,13 @@ internal class MyPortfolioUMMFactory( } return TokensPortfolioUMConverter( - appCurrency = portfolioModel.appCurrency, - isBalanceHidden = portfolioModel.isBalanceHidden, + appCurrency = portfolioData.appCurrency, + isBalanceHidden = portfolioData.isBalanceHidden, isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks), bsConfig = createAddToPortfolioBSConfig( - portfolioModel = portfolioModel, - selectedWalletId = selectedWalletId, + portfolioData = portfolioData, + portfolioUIData = portfolioUIData, availableNetworks = availableNetworks, - portfolioBSVisibilityModel = portfolioBSVisibilityModel, - walletsWithChangedNetworks = walletsWithChangedNetworks, ), onAddClick = onAddClick, onTokenItemClick = onTokenItemClick, @@ -75,23 +68,20 @@ internal class MyPortfolioUMMFactory( } private fun createAddToPortfolioBSConfig( - portfolioModel: PortfolioData, - selectedWalletId: UserWalletId?, + portfolioData: PortfolioData, + portfolioUIData: PortfolioUIData, availableNetworks: List, - portfolioBSVisibilityModel: PortfolioBSVisibilityModel, - walletsWithChangedNetworks: Map>, ): TangemBottomSheetConfig { - val walletId = requireNotNull(selectedWalletId) { - "Selected wallet must be not null in state with AddToPortfolio bottom sheet" - } + val walletId = portfolioUIData.selectedWalletId + ?: portfolioData.walletsWithCurrencyStatuses.keys.firstOrNull { it.isMultiCurrency }?.walletId - val selectedWallet = portfolioModel.walletsWithCurrencyStatuses.keys + val selectedWallet = portfolioData.walletsWithCurrencyStatuses.keys .firstOrNull { it.walletId == walletId } ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") - val changedNetworks = walletsWithChangedNetworks[selectedWalletId] + val changedNetworks = portfolioUIData.walletsWithChangedNetworks[portfolioUIData.selectedWalletId] val alreadyAddedNetworks = requireNotNull( - value = portfolioModel.walletsWithCurrencyStatuses[selectedWallet], + value = portfolioData.walletsWithCurrencyStatuses[selectedWallet], lazyMessage = { "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" }, @@ -100,8 +90,8 @@ internal class MyPortfolioUMMFactory( .map { it.currency.network.backendId } return addToPortfolioBSContentUMFactory.create( - portfolioData = portfolioModel, - portfolioBSVisibilityModel = portfolioBSVisibilityModel, + portfolioData = portfolioData, + portfolioUIData = portfolioUIData, selectedWallet = selectedWallet, networksWithToggle = availableNetworks.associateWithToggle( changedNetworks = changedNetworks, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt new file mode 100644 index 0000000000..fcb0dd86c0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Portfolio UI data. Combined data from all UI flows that required to setup portfolio + * + * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model + * @property selectedWalletId selected wallet id + * @property walletsWithChangedNetworks wallets with changed networks + * @property hasMissedDerivations flag that indicates if user has missed derivations + * +[REDACTED_AUTHOR] + */ +internal data class PortfolioUIData( + val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + val selectedWalletId: UserWalletId?, + val walletsWithChangedNetworks: Map>, + val hasMissedDerivations: Boolean, +) \ No newline at end of file From 1976f57045efd24185631ceff15a04cff2881670 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 19:50:17 +0500 Subject: [PATCH 86/90] Updated on 2026-08-14 --- app/src/main/assets/testnet_tokens.json | 10 ++++++++++ .../core/ui/extensions/BlockchainIcons.kt | 5 +++++ core/ui/src/main/res/drawable/ic_sei_22.xml | 10 ++++++++++ core/ui/src/main/res/drawable/img_sei_22.xml | 20 +++++++++++++++++++ gradle/dependencies.toml | 2 +- .../tangem/blockchainsdk/utils/Blockchain.kt | 5 +++++ 6 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/res/drawable/ic_sei_22.xml create mode 100644 core/ui/src/main/res/drawable/img_sei_22.xml diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 409a6b6067..cb960e3694 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -708,6 +708,16 @@ "networkId": "cyber/test" } ] + }, + { + "id": "sei-network", + "name": "Sei Network", + "symbol": "SEI", + "networks": [ + { + "networkId": "sei-network/test" + } + ] } ] } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index f59561c295..97adf7cbe4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -73,6 +73,7 @@ fun getActiveIconRes(blockchainId: String): Int { "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 + "sei", "sei/test" -> R.drawable.img_sei_22 else -> R.drawable.ic_alert_24 } } @@ -147,6 +148,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 + "sei", "sei/test" -> R.drawable.img_sei_22 else -> R.drawable.ic_alert_24 } } @@ -218,6 +220,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 + "sei", "sei/test" -> R.drawable.img_sei_22 else -> R.drawable.ic_alert_24 } } @@ -292,6 +295,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "blast", "blast/test" -> R.drawable.ic_blast_22 "filecoin" -> R.drawable.ic_filecoin_22 "cyber", "cyber/test" -> R.drawable.ic_cyber_22 + "sei", "sei/test" -> R.drawable.ic_sei_22 else -> R.drawable.ic_alert_24 } } @@ -366,6 +370,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "blast", "blast/test" -> R.drawable.ic_blast_22 "filecoin" -> R.drawable.ic_filecoin_22 "cyber", "cyber/test" -> R.drawable.ic_cyber_22 + "sei", "sei/test" -> R.drawable.ic_sei_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_sei_22.xml b/core/ui/src/main/res/drawable/ic_sei_22.xml new file mode 100644 index 0000000000..576382fca3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_sei_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_sei_22.xml b/core/ui/src/main/res/drawable/img_sei_22.xml new file mode 100644 index 0000000000..39ecb53ae8 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_sei_22.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index fc08cd3ad3..0d71031a5e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-750" +tangemBlockchainSdk = "develop-755" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 8d53ec6fb6..22942b8e18 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -123,6 +123,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "blast/test" -> Blockchain.BlastTestnet "cyber" -> Blockchain.Cyber "cyber/test" -> Blockchain.CyberTestnet + "sei-network" -> Blockchain.Sei + "sei-network/test" -> Blockchain.SeiTestnet else -> null } } @@ -247,6 +249,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.BlastTestnet -> "blast/test" Blockchain.Cyber -> "cyber" Blockchain.CyberTestnet -> "cyber/test" + Blockchain.Sei -> "sei-network" + Blockchain.SeiTestnet -> "sei-network/test" } } @@ -329,6 +333,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Filecoin -> "filecoin" Blockchain.Blast, Blockchain.BlastTestnet -> "blast-ethereum" Blockchain.Cyber, Blockchain.CyberTestnet -> "cyberconnect" + Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network" } } From 0387fb8a4f3137e3a05a74c4553c58bdd3714691 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Sep 2024 13:28:13 +0500 Subject: [PATCH 87/90] Updated on 2026-08-14 --- .../response/model/YieldBalanceWrapperDTO.kt | 3 +++ .../local/token/DefaultStakingBalanceStore.kt | 14 ++++++------- .../local/token/StakingBalanceStore.kt | 5 ++--- .../data/staking/DefaultStakingRepository.kt | 19 ++++------------- .../converters/YieldBalanceConverter.kt | 21 +++++++------------ .../converters/YieldBalanceListConverter.kt | 9 +------- .../staking/model/stakekit/YieldBalance.kt | 1 + .../operations/CurrencyStatusOperations.kt | 10 +++++---- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 3 +-- .../domain/tokens/mock/MockTokensStates.kt | 5 ++--- 10 files changed, 33 insertions(+), 57 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index 1cd438732e..964bde08a7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -2,12 +2,15 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.request.Address import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import org.joda.time.DateTime import java.math.BigDecimal @JsonClass(generateAdapter = true) data class YieldBalanceWrapperDTO( + @Json(name = "addresses") + val addresses: Address, @Json(name = "balances") val balances: List, @Json(name = "integrationId") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index a80f1b8e05..008cdd1c0f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -1,12 +1,11 @@ package com.tangem.datasource.local.token -import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -30,17 +29,16 @@ internal class DefaultStakingBalanceStore( } } - override fun get(userWalletId: UserWalletId, integrationId: String): Flow> { + override fun get(userWalletId: UserWalletId, integrationId: String): Flow { return dataStore.get(userWalletId.stringValue) - .map { balances -> - balances.filter { it.integrationId == integrationId } - .flatMap { it.balances } + .mapNotNull { balances -> + balances.firstOrNull { it.integrationId == integrationId } } } - override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? { + override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO? { return dataStore.getSyncOrNull(userWalletId.stringValue) - ?.firstOrNull { it.integrationId == integrationId }?.balances + ?.firstOrNull { it.integrationId == integrationId } } override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index 1dc58ba38d..421f03dae1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -1,6 +1,5 @@ package com.tangem.datasource.local.token -import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -13,9 +12,9 @@ interface StakingBalanceStore { suspend fun store(userWalletId: UserWalletId, items: Set) - fun get(userWalletId: UserWalletId, integrationId: String): Flow> + fun get(userWalletId: UserWalletId, integrationId: String): Flow - suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO? suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 24039a6bb3..7635f8e0f4 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -299,6 +299,7 @@ internal class DefaultStakingRepository( YieldBalanceWrapperDTO( balances = result, integrationId = requestBody.integrationId, + addresses = requestBody.addresses, ), ) }, @@ -317,14 +318,7 @@ internal class DefaultStakingRepository( ?: error("Could not get integrationId") stakingBalanceStore.get(userWalletId, integrationId) .collectLatest { - send( - yieldBalanceConverter.convert( - YieldBalanceConverter.Data( - balance = it, - integrationId = integrationId, - ), - ), - ) + send(yieldBalanceConverter.convert(it)) } } @@ -351,12 +345,7 @@ internal class DefaultStakingRepository( val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) ?: return@withContext YieldBalance.Error - yieldBalanceConverter.convert( - YieldBalanceConverter.Data( - balance = result, - integrationId = integrationId, - ), - ) + yieldBalanceConverter.convert(result) } } @@ -385,7 +374,7 @@ internal class DefaultStakingRepository( null } } - .distinct() + .distinctBy { it.second } .map { getBalanceRequestData(it.first, it.second) } .ifEmpty { return@invokeOnExpire } val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index ec86b52e73..66e1e735d1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -1,24 +1,22 @@ package com.tangem.data.staking.converters import com.tangem.data.staking.converters.action.PendingActionConverter -import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceItem +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.stakekit.* import com.tangem.utils.converter.Converter -internal class YieldBalanceConverter : Converter { +internal class YieldBalanceConverter : Converter { private val pendingActionConverter by lazy(LazyThreadSafetyMode.NONE) { PendingActionConverter() } - override fun convert(value: Data): YieldBalance { - return if (value.balance.isEmpty()) { + override fun convert(value: YieldBalanceWrapperDTO): YieldBalance { + return if (value.balances.isEmpty()) { YieldBalance.Empty } else { YieldBalance.Data( + address = value.addresses.address, balance = YieldBalanceItem( - items = value.balance.map { item -> + items = value.balances.map { item -> BalanceItem( type = BalanceType.valueOf(item.type.name), amount = item.amount, @@ -35,9 +33,4 @@ internal class YieldBalanceConverter : Converter, - val integrationId: String?, - ) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt index 007c73bfe1..6a3f3c793a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -15,14 +15,7 @@ internal class YieldBalanceListConverter : Converter YieldBalanceList.Empty } else { YieldBalanceList.Data( - balances = value.map { - converter.convert( - YieldBalanceConverter.Data( - balance = it.balances, - integrationId = it.integrationId, - ), - ) - }, + balances = value.map(converter::convert), ) } } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 1c7c27ac54..c759687602 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -8,6 +8,7 @@ sealed class YieldBalance { data class Data( val balance: YieldBalanceItem, + val address: String, ) : YieldBalance() { fun getTotalStakingBalance(): BigDecimal { return balance.items diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 380f77dc4f..abdf3f94fd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -57,14 +57,16 @@ internal class CurrencyStatusOperations( val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) - + val isCurrentAddressStaking = + (yieldBalance as? YieldBalance.Data)?.address == status.address.defaultAddress.value + val currentYieldBalance = yieldBalance.takeIf { isCurrentAddressStaking } return when { ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, - yieldBalance = yieldBalance, + yieldBalance = currentYieldBalance, ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, @@ -74,7 +76,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, - yieldBalance = yieldBalance, + yieldBalance = currentYieldBalance, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -85,7 +87,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, - yieldBalance = yieldBalance, + yieldBalance = currentYieldBalance, ) } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 824a8e89c2..b96776f92b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -4,7 +4,6 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.error.DataError -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes @@ -123,7 +122,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), - yieldBalance = YieldBalance.Error, + yieldBalance = null, ), ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 78858d98c1..45c2f537d4 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -152,7 +151,7 @@ internal object MockTokensStates { pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, - yieldBalance = YieldBalance.Error, + yieldBalance = null, ), ) } @@ -168,7 +167,7 @@ internal object MockTokensStates { .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, - yieldBalance = YieldBalance.Error, + yieldBalance = null, ), ) } From a7fbf9b3ee2abcc7fb8c288a266d0d1e8ab29abe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Sep 2024 13:21:06 +0300 Subject: [PATCH 88/90] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 5 ++-- .../converters/YieldBalanceConverter.kt | 3 ++- .../domain/staking/model/stakekit/Yield.kt | 12 +++++----- .../staking/model/stakekit/YieldBalance.kt | 2 +- .../converters/YieldBalancesConverter.kt | 11 +++++++-- .../state/stub/StakingClickIntentsStub.kt | 2 +- .../ActionTypeActiveStakeTransformer.kt | 24 +++++++++++++++++++ .../SetButtonsStateTransformer.kt | 3 +-- .../viewmodel/StakingClickIntents.kt | 4 ++-- .../viewmodel/StakingViewModel.kt | 18 ++++++-------- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 ++++ 11 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 24039a6bb3..779640e32c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.raise.catch -import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData @@ -108,8 +107,7 @@ internal class DefaultStakingRepository( value = emptyMap(), ) - private val tronStakeKitTransactionAdapter: JsonAdapter = - moshi.adapter(TronStakeKitTransaction::class.java) + private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) } override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { rawNetworkId.plus(rawCurrencyId) @@ -537,6 +535,7 @@ internal class DefaultStakingRepository( args = ActionRequestBodyArgs( amount = params.amount.toPlainString(), validatorAddress = params.validatorAddress, + validatorAddresses = listOf(params.validatorAddress), ), ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index ec86b52e73..c171ba5acd 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -25,7 +25,8 @@ internal class YieldBalanceConverter : Converter true AVAILABLE, UNSTAKING, PREPARING, REWARDS, - LOCKED, UNLOCKING, UNKNOWN, -> false diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 17c6e659d7..098483c942 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -85,7 +85,7 @@ internal class YieldBalancesConverter( .mapNotNull { balance -> val validator = yield.validators.firstOrNull { balance.validatorAddress?.contains(it.address, ignoreCase = true) == true - } + } ?: mockedValidator val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) val unbonding = getUnbondingDate(balance.date) @@ -130,10 +130,10 @@ internal class YieldBalancesConverter( BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer) BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null + BalanceType.LOCKED -> resourceReference(R.string.staking_locked) to null BalanceType.AVAILABLE -> null to null BalanceType.PREPARING -> resourceReference(R.string.staking_preparing) to null BalanceType.REWARDS -> null to null - BalanceType.LOCKED -> null to null BalanceType.UNLOCKING -> null to null BalanceType.UNKNOWN -> null to null } @@ -164,5 +164,12 @@ internal class YieldBalancesConverter( private companion object { const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000 + + private val mockedValidator = Yield.Validator( + address = "", + status = "", + name = "Mocked validator", + preferred = true, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index b2edad1892..4697e7fe88 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -15,7 +15,7 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) {} + override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) {} override fun onActionClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt new file mode 100644 index 0000000000..1e451a27c2 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.lib.crypto.BlockchainUtils.isTron +import com.tangem.utils.transformer.Transformer + +internal class ActionTypeActiveStakeTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val activeStake: BalanceState, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) + val actionType = if (activeStake.pendingActions.isEmpty() || isTron) { + StakingActionCommonType.EXIT + } else { + StakingActionCommonType.PENDING_OTHER + } + + return prevState.copy(actionType = actionType) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index b18c96f147..5ad39ad9be 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -137,9 +137,8 @@ internal class SetButtonsStateTransformer : Transformer { private fun StakingUiState.onPrimaryClick() { when (currentStep) { StakingStep.InitialInfo -> { - val actionType = StakingActionCommonType.ENTER clickIntents.onAmountValueChange("") // reset amount state - clickIntents.onNextClick(actionType) + clickIntents.onNextClick(StakingActionCommonType.ENTER) } StakingStep.Validators, StakingStep.Amount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 11910f51df..076d61b47d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -15,7 +15,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick(actionType: StakingActionCommonType? = null, pendingAction: PendingAction? = null) + fun onNextClick(actionTypeToOverwrite: StakingActionCommonType? = null, pendingAction: PendingAction? = null) fun onActionClick() @@ -27,7 +27,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun getFee(pendingAction: PendingAction?) - override fun onAmountNext() = onNextClick(actionType = null) + override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null) fun openValidators() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 4eba6fabde..a79ffd5875 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -159,9 +159,9 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) { - if (actionType != null) { - stateController.update { it.copy(actionType = actionType) } + override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) { + if (actionTypeToOverwrite != null) { + stateController.update { it.copy(actionType = actionTypeToOverwrite) } } stakingStateRouter.onNextClick() when { @@ -395,7 +395,7 @@ internal class StakingViewModel @Inject constructor( if (rewards != null && rewards.isSingleItem()) { onActiveStake(rewards.first()) } else { - onNextClick(actionType = StakingActionCommonType.PENDING_REWARDS) + onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS) } } @@ -405,7 +405,7 @@ internal class StakingViewModel @Inject constructor( ShowActionSelectorBottomSheetTransformer( pendingActions = activeStake.pendingActions, onActionSelect = { - onNextClick(actionType = StakingActionCommonType.PENDING_OTHER, pendingAction = it) + onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, pendingAction = it) stateController.update(DismissBottomSheetStateTransformer()) }, onDismiss = { @@ -414,14 +414,10 @@ internal class StakingViewModel @Inject constructor( ), ) } else { - val actionType = if (activeStake.pendingActions.isEmpty()) { - StakingActionCommonType.EXIT - } else { - StakingActionCommonType.PENDING_OTHER - } + stateController.update(ActionTypeActiveStakeTransformer(cryptoCurrencyStatus, activeStake)) stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - onNextClick(actionType, activeStake.pendingActions.firstOrNull()) + onNextClick(null, activeStake.pendingActions.firstOrNull()) } } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 89f796a6b7..bc53484728 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -52,6 +52,11 @@ object BlockchainUtils { return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet } + fun isTron(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet + } + fun isSupportedNetworkId(networkId: String): Boolean { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } From d7374fb83f68bbae42bfe3216b24930f8ccdcf7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 10:12:05 +0300 Subject: [PATCH 89/90] Updated on 2026-08-14 --- .../impl/presentation/state/StakingStateController.kt | 2 ++ .../staking/impl/presentation/state/StakingUiState.kt | 2 ++ .../transformers/SetInitialDataStateTransformer.kt | 1 + .../state/transformers/SetTitleTransformer.kt | 11 ++++++++++- .../staking/impl/presentation/ui/StakingScreen.kt | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 8bb0aef58a..6f007f76cb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -61,7 +61,9 @@ internal class StakingStateController @Inject constructor() { private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, + subtitle = null, clickIntents = StakingClickIntentsStub, + walletName = "", cryptoCurrencyName = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index d06f0e1507..637d40f910 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -22,7 +22,9 @@ import java.math.BigDecimal @Immutable internal data class StakingUiState( val title: TextReference, + val subtitle: TextReference?, val clickIntents: StakingClickIntents, + val walletName: String, val cryptoCurrencyName: String, val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 7d873f5da1..cd6d2f6166 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -65,6 +65,7 @@ internal class SetInitialDataStateTransformer( R.string.staking_title_stake, wrappedList(cryptoCurrencyStatusProvider().currency.name), ), + walletName = userWalletProvider.invoke().name, cryptoCurrencyName = cryptoCurrencyStatusProvider.invoke().currency.name, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index ffb66a5ee1..7ad24a9041 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R @@ -36,6 +37,14 @@ internal object SetTitleTransformer : Transformer { } } - return prevState.copy(title = title) + val subtitle = when (currentStep) { + StakingStep.Confirmation -> stringReference(prevState.walletName) + else -> null + } + + return prevState.copy( + title = title, + subtitle = subtitle, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 9fad6829f4..568d824f9e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -97,6 +97,7 @@ private fun SendAppBar(uiState: StakingUiState) { } AppBarWithBackButtonAndIcon( text = uiState.title.resolveReference(), + subtitle = uiState.subtitle?.resolveReference(), backIconRes = backIcon, onBackClick = uiState.clickIntents::onBackClick, backgroundColor = TangemTheme.colors.background.secondary, From 9de7841edda0859b0b83eb257829d7163137d724 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 11:05:02 +0300 Subject: [PATCH 90/90] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 18 -- .../common/ui/tokens/TokenActionsUtils.kt | 69 ++++++ .../bottomsheets/TangemBottomSheetConfig.kt | 7 +- .../core/ui/components/token/TokenItem.kt | 48 +++-- .../model/ScenarioUnavailabilityReason.kt | 0 .../tokens/GetCryptoCurrencyActionsUseCase.kt | 17 +- .../domain/tokens/legacy/TradeCryptoAction.kt | 10 +- .../domain/tokens/model/TokenActionsState.kt | 6 +- features/markets/impl/build.gradle.kts | 7 + .../impl/model/MarketsTokenDetailsModel.kt | 15 +- .../impl/model/formatter/Formatters.kt | 11 - .../portfolio/impl/loader/PortfolioData.kt | 13 +- .../impl/loader/PortfolioDataLoader.kt | 40 +++- .../model/AddToPortfolioBSContentUMFactory.kt | 4 +- .../impl/model/MarketsPortfolioModel.kt | 42 +++- ...oUMMFactory.kt => MyPortfolioUMFactory.kt} | 38 ++-- .../impl/model/PortfolioTokenUMConverter.kt | 72 ++++++- .../impl/model/TokenActionsHandler.kt | 199 ++++++++++++++++++ .../impl/model/TokensPortfolioUMConverter.kt | 20 +- .../impl/ui/AddToPortfolioBottomSheet.kt | 19 +- .../markets/portfolio/impl/ui/MyPortfolio.kt | 10 +- .../portfolio/impl/ui/PortfolioItem.kt | 11 +- .../impl/ui/PortfolioQuickActions.kt | 73 ++++--- .../impl/ui/TokenActionsBottomSheet.kt | 18 +- .../PreviewAddToPortfolioBSContentProvider.kt | 15 +- .../preview/PreviewMyPortfolioUMProvider.kt | 39 ++-- .../ui/state/AddToPortfolioBSContentUM.kt | 1 + .../portfolio/impl/ui/state/MyPortfolioUM.kt | 6 +- .../impl/ui/state/PortfolioTokenUM.kt | 12 +- .../portfolio/impl/ui/state/QuickActionUM.kt | 2 + .../impl/ui/state/TokenActionsBSContentUM.kt | 40 +++- .../converters/MarketsTokenItemConverter.kt | 12 +- features/tokendetails/impl/build.gradle.kts | 1 + .../state/factory/TokenDetailsStateFactory.kt | 66 +----- features/wallet/impl/build.gradle.kts | 2 +- .../wallet/presentation/WalletFragment.kt | 6 + .../MultiWalletCurrencyActionsConverter.kt | 2 +- .../WalletCurrencyActionsClickIntents.kt | 88 +------- 38 files changed, 687 insertions(+), 372 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt rename domain/tokens/{src/main/kotlin => models/src/main/java}/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt (100%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/{MyPortfolioUMMFactory.kt => MyPortfolioUMFactory.kt} (76%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 9f5d8aecb2..1abdf861b9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkAddress @@ -48,11 +47,6 @@ object TradeCryptoMiddleware { is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.Buy -> proceedBuyAction(state, action) is TradeCryptoAction.Sell -> proceedSellAction(action) - is TradeCryptoAction.Stake -> openStaking( - userWalletId = action.userWalletId, - cryptoCurrencyId = action.cryptoCurrencyId, - yield = action.yield, - ) is TradeCryptoAction.SendToken -> handleNewSendToken(action = action) is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action) } @@ -140,18 +134,6 @@ object TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) { - store.dispatchNavigationAction { - push( - AppRoute.Staking( - userWalletId = userWalletId, - cryptoCurrencyId = cryptoCurrencyId, - yield = yield, - ), - ) - } - } - private fun handleNewSendToken(action: TradeCryptoAction.SendToken) { handleNewSend( userWalletId = action.userWallet.walletId, diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt new file mode 100644 index 0000000000..d33e9330f1 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt @@ -0,0 +1,69 @@ +package com.tangem.common.ui.tokens + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason + +fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { + return when (val unavailabilityReason = this) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.PendingTransaction -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_send, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + } + } + is ScenarioUnavailabilityReason.EmptyBalance -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_send, + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_sell, + ) + } + } + is ScenarioUnavailabilityReason.BuyUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_buy_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotExchangeable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_not_exchangeable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotSupportedBySellService -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.Unreachable -> { + resourceReference( + 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") + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt index 0bb0162ccc..d2eb1f9935 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt @@ -11,4 +11,9 @@ data class TangemBottomSheetConfig( val isShow: Boolean, val onDismissRequest: () -> Unit, val content: TangemBottomSheetConfigContent, -) \ No newline at end of file +) { + + companion object { + val Empty = TangemBottomSheetConfig(false, {}, TangemBottomSheetConfigContent.Empty) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 436f507958..0a519a13e1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -47,12 +48,18 @@ private enum class LayoutId { * >Figma Component */ @Composable -fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { +fun TokenItem( + state: TokenItemState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, + itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), +) { TokenItem( state = state, isBalanceHidden = isBalanceHidden, modifier = modifier, reorderableTokenListState = null, + itemPaddingValues = itemPaddingValues, ) } @@ -63,6 +70,7 @@ fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifie * @param isBalanceHidden flag that shows/hides balance * @param reorderableTokenListState reorderable token list state * @param modifier modifier + * @param itemPaddingValues padding values * * @see Figma Component @@ -73,12 +81,15 @@ fun TokenItem( isBalanceHidden: Boolean, reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier, + itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), ) { val betweenRowsMargin = TangemTheme.dimens.spacing2 CustomContainer( state = state, - modifier = modifier.tokenClickable(state = state), + modifier = modifier + .tokenClickable(state = state) + .padding(itemPaddingValues), ) { CurrencyIcon( state = state.iconState, @@ -163,10 +174,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier Layout(content = content, modifier = modifier) { measurables, constraints -> val layoutWidth = constraints.maxWidth - val horizontalPadding = with(density) { dimens.size12.roundToPx() } val verticalPadding = with(density) { dimens.size15.roundToPx() } - val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding - val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() @@ -202,35 +210,35 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier -> { fiatAmount = measurables.measureFiatAmount( state = state, - maxWidth = layoutWidthWithoutPaddings - icon.width - titleMinWidth, + maxWidth = layoutWidth - icon.width - titleMinWidth, defaultConstraints = constraints, ) cryptoAmount = measurables.measureCryptoAmount( state = state, - maxWidth = layoutWidthWithoutPaddings - icon.width - priceChangeMinWidth, + maxWidth = layoutWidth - icon.width - priceChangeMinWidth, defaultConstraints = constraints, ) - firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - fiatAmount.width - secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - cryptoAmount.width + firstRowRemainingFreeSpace = layoutWidth - icon.width - fiatAmount.width + secondRowRemainingFreeSpace = layoutWidth - icon.width - cryptoAmount.width } is TokenItemState.Draggable -> { cryptoAmount = measurables.measureCryptoAmount( state = state, - maxWidth = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width, + maxWidth = layoutWidth - icon.width - nonFiatContent.width, defaultConstraints = constraints, ) - firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width } is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { - firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width if (state.subtitleState != null) { - secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + secondRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width } } } @@ -262,10 +270,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) layout(width = constraints.maxWidth, height = layoutHeight) { - icon.placeRelative(x = horizontalPadding, y = (layoutHeight - icon.height).div(other = 2)) + icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2)) title.placeRelative( - x = horizontalPadding + icon.width, + x = icon.width, y = when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, @@ -280,23 +288,23 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier }, ) - fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - horizontalPadding, y = verticalPadding) + fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width, y = verticalPadding) priceChange?.placeRelative( - x = horizontalPadding + icon.width, + x = icon.width, y = layoutHeight - priceChange.height - verticalPadding, ) cryptoAmount?.placeRelative( x = when (state) { - is TokenItemState.Draggable -> horizontalPadding + icon.width - else -> layoutWidth - cryptoAmount.width - horizontalPadding + is TokenItemState.Draggable -> icon.width + else -> layoutWidth - cryptoAmount.width }, y = layoutHeight - cryptoAmount.height - verticalPadding, ) nonFiatContent.placeRelative( - x = layoutWidth - nonFiatContent.width - horizontalPadding, + x = layoutWidth - nonFiatContent.width, y = (layoutHeight - nonFiatContent.height).div(other = 2), ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt similarity index 100% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt 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 c83af879ba..99ec404379 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 @@ -130,11 +130,24 @@ class GetCryptoCurrencyActionsUseCase( // staking if (stakingFeatureToggles.isStakingEnabled) { if (isStakingAvailable(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None)) + val yield = kotlin.runCatching { + stakingRepository.getYield( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) + }.getOrNull() + + activeList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.None, + yield = yield, + ), + ) } else { disabledList.add( TokenActionsState.ActionState.Stake( unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + yield = null, ), ) } @@ -254,7 +267,7 @@ class GetCryptoCurrencyActionsUseCase( actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } if (stakingFeatureToggles.isStakingEnabled) { - actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable)) + actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) } actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 706493a6ea..b32c7a091f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -1,10 +1,8 @@ package com.tangem.domain.tokens.legacy -import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import org.rekotlin.Action import java.math.BigDecimal @@ -24,6 +22,7 @@ sealed class TradeCryptoAction : Action { val appCurrencyCode: String, ) : TradeCryptoAction() + @Deprecated("Use AppRoute instead") data class SendToken( val userWallet: UserWallet, val tokenCurrency: CryptoCurrency.Token, @@ -33,6 +32,7 @@ sealed class TradeCryptoAction : Action { val transactionInfo: TransactionInfo? = null, ) : TradeCryptoAction() + @Deprecated("Use AppRoute instead") data class SendCoin( val userWallet: UserWallet, val coinStatus: CryptoCurrencyStatus, @@ -40,12 +40,6 @@ sealed class TradeCryptoAction : Action { val transactionInfo: TransactionInfo? = null, ) : TradeCryptoAction() - data class Stake( - val userWalletId: UserWalletId, - val cryptoCurrencyId: CryptoCurrency.ID, - val yield: Yield, - ) : TradeCryptoAction() - data class TransactionInfo( val transactionId: String, val destinationAddress: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index a3dbea0959..c6db8e28e2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.wallets.models.UserWalletId data class TokenActionsState( @@ -20,7 +21,10 @@ data class TokenActionsState( data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Stake(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Stake( + override val unavailabilityReason: ScenarioUnavailabilityReason, + val yield: Yield?, + ) : ActionState() data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 3fc641b0da..8cccec2563 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -29,6 +29,13 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + // FIXME [REDACTED_TASK_KEY] + // Remove the "Buy" and "Sell" actions from the redux middleware. + // Instead, create some kind of interface for such cases. + /* Redux -_- */ + implementation(projects.domain.legacy) + implementation(deps.reKotlin) + /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index e899cfeb23..83b17519eb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -346,9 +346,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartDataProducer.runTransaction { updateLook { - it.copy( - type = getChartTypeByPercent(percent), - ) + it.copy(type = percent.percentChangeType().toChartType()) } } } @@ -356,13 +354,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( private suspend fun updateQuotes(newQuotes: TokenQuotes) { quotesStateUpdater.updateQuotes(newQuotes) - val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) + val percent = newQuotes + .getPercentByInterval(interval = state.value.selectedInterval) chartDataProducer.runTransaction { updateLook { - it.copy( - type = getChartTypeByPercent(percent), - ) + it.copy(type = percent.percentChangeType().toChartType()) } } } @@ -430,7 +427,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartDataProducer.runTransaction { updateLook { it.copy( - type = getChartTypeByPercent(percent), + type = percent.percentChangeType().toChartType(), ) } } @@ -477,7 +474,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( launch { while (true) { delay(timeMillis) - // Update quotes only when content visible on the screen + // Update quotes only when content is visible on the screen isVisibleOnScreen.first { it } loadQuotes() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt index c4cf1f6e3f..d2f2869872 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt @@ -87,15 +87,4 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type { PriceChangeType.DOWN -> MarketChartLook.Type.Falling PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral } -} - -@Suppress("MagicNumber") -internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type { - val scaled = percent?.setScale(4, RoundingMode.HALF_UP) - return when { - scaled == null -> return MarketChartLook.Type.Neutral - scaled > BigDecimal.ZERO -> MarketChartLook.Type.Growing - scaled < BigDecimal.ZERO -> MarketChartLook.Type.Falling - else -> MarketChartLook.Type.Neutral - } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt index 8ac05a4e90..ece4a0a217 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt @@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -11,7 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId /** * Portfolio data. Combined data from all flows that required to setup portfolio * - * @property walletsWithCurrencyStatuses wallets with crypto currency statuses + * @property walletsWithCurrencies wallets with crypto currency statuses * @property appCurrency app currency * @property isBalanceHidden flag that indicates if balance should be hidden * @property walletsWithBalance wallets with total balance @@ -19,8 +20,14 @@ import com.tangem.domain.wallets.models.UserWalletId [REDACTED_AUTHOR] */ internal data class PortfolioData( - val walletsWithCurrencyStatuses: Map>, + val walletsWithCurrencies: Map>, val appCurrency: AppCurrency, val isBalanceHidden: Boolean, val walletsWithBalance: Map>, -) \ No newline at end of file +) { + data class CryptoCurrencyData( + val userWallet: UserWallet, + val status: CryptoCurrencyStatus, + val actions: List, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index e2e08f4b70..0685abffb7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -6,9 +6,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -32,18 +32,19 @@ internal class PortfolioDataLoader @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, ) { /** Load data by [currencyRawId] */ @OptIn(ExperimentalCoroutinesApi::class) fun load(currencyRawId: String): Flow { return combine( - flow = getAllWalletsCryptoCurrencyStatusesFlow(currencyRawId = currencyRawId), + flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), flow2 = getSelectedAppCurrencyFlow(), flow3 = getBalanceHidingSettingsFlow(), - ) { walletsWithCurrencyStatuses, appCurrency, isBalanceHidden -> + ) { walletsWithCurrencies, appCurrency, isBalanceHidden -> PortfolioData( - walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, + walletsWithCurrencies = walletsWithCurrencies, appCurrency = appCurrency, isBalanceHidden = isBalanceHidden, walletsWithBalance = emptyMap(), @@ -52,15 +53,16 @@ internal class PortfolioDataLoader @Inject constructor( // setup balances for wallets from walletsWithCurrencyStatuses .flatMapLatest { portfolioData -> getWalletsWithTotalBalanceFlow( - ids = portfolioData.walletsWithCurrencyStatuses.keys.map(UserWallet::walletId), + ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), ) .map { portfolioData.copy(walletsWithBalance = it) } } } - private fun getAllWalletsCryptoCurrencyStatusesFlow( + @OptIn(ExperimentalCoroutinesApi::class) + private fun getAllWalletsCryptoCurrenciesData( currencyRawId: String, - ): Flow>> { + ): Flow>> { return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) .distinctUntilChanged() .map { walletsWithMaybeStatuses -> @@ -68,6 +70,30 @@ internal class PortfolioDataLoader @Inject constructor( entry.value.mapNotNull { it.getOrNull() } } } + .flatMapLatest { walletsWithStatuses -> + val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> + statuses.map { status -> + getCryptoCurrencyActionsUseCase(wallet, status) + .map { + PortfolioData.CryptoCurrencyData( + userWallet = wallet, + status = status, + actions = it.states, + ) + } + } + } + + combine(actionsFlows) { actions -> + walletsWithStatuses.mapValues { entry -> + entry.value.mapNotNull { status -> + actions.firstOrNull { + it.userWallet == entry.key && it.status == status + } + } + } + } + } } private fun getSelectedAppCurrencyFlow(): Flow { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 6b8140850f..e88ef7d966 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -66,6 +66,8 @@ internal class AddToPortfolioBSContentUMFactory( portfolioData = portfolioData, selectedWalletId = selectedWallet.walletId, ), + // TODO maybe do not show wallets with already added token + isWalletBlockVisible = portfolioData.walletsWithBalance.size > 1, ), ) } @@ -86,7 +88,7 @@ internal class AddToPortfolioBSContentUMFactory( isShow = isShow, onDismissRequest = { onWalletSelectorVisibilityChange(false) }, content = WalletSelectorBSContentUM( - userWallets = portfolioData.walletsWithCurrencyStatuses + userWallets = portfolioData.walletsWithCurrencies .filterKeys(UserWallet::isMultiCurrency) .map { it.key } .map { userWallet -> diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 5d647ae942..891fc3b8aa 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -6,6 +6,8 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.card.HasMissedDerivationsUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -14,20 +16,24 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ComponentScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, - override val dispatchers: CoroutineDispatcherProvider, + private val tokenActionsIntentsFactory: TokenActionsHandler.Factory, ) : Model() { val state: StateFlow get() = _state @@ -45,7 +51,17 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) - private val factory = MyPortfolioUMMFactory( + private val currentAppCurrency = getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + + private val factory = MyPortfolioUMFactory( onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) }, onTokenItemClick = ::onTokenItemClick, addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( @@ -56,6 +72,15 @@ internal class MarketsPortfolioModel @Inject constructor( onWalletSelect = ::onWalletSelect, onContinueClick = ::onContinueClick, ), + tokenActionsHandler = tokenActionsIntentsFactory.create( + currentAppCurrency = Provider { currentAppCurrency.value }, + updateTokenActionsBSConfig = { updateBlock -> + updateTokensState { it.copy(tokenActionsBSConfig = updateBlock(it.tokenActionsBSConfig)) } + }, + updateTokenReceiveBSConfig = { updateBlock -> + updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } + }, + ), ) init { @@ -128,9 +153,7 @@ internal class MarketsPortfolioModel @Inject constructor( } private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) { - _state.update { - val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it - + updateTokensState { state -> state.copy( tokens = state.tokens .mapIndexed { i, tokenUM -> @@ -138,7 +161,7 @@ internal class MarketsPortfolioModel @Inject constructor( if (index == i && id.value == tokenUM.tokenItemState.id) { tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown) } else { - tokenUM + tokenUM.copy(isQuickActionsShown = false) } } .toImmutableList(), @@ -194,4 +217,11 @@ internal class MarketsPortfolioModel @Inject constructor( it.copy(addToPortfolioBSVisibility = true, walletSelectorBSVisibility = isShow) } } + + private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) { + _state.update { stateToUpdate -> + val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate + block(tokensState) + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt similarity index 76% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 109e54ad83..fb632b9acc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -16,10 +16,11 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM * [REDACTED_AUTHOR] */ -internal class MyPortfolioUMMFactory( +internal class MyPortfolioUMFactory( private val onAddClick: () -> Unit, private val onTokenItemClick: (Int, CryptoCurrency.ID) -> Unit, private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, + private val tokenActionsHandler: TokenActionsHandler, ) { fun create( @@ -31,16 +32,16 @@ internal class MyPortfolioUMMFactory( if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable - val walletsWithStatuses = portfolioData.walletsWithCurrencyStatuses + val walletsWithCurrencies = portfolioData.walletsWithCurrencies .filterAvailableNetworks(networks = availableNetworks) - val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty() + val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() if (isPortfolioEmpty) { - val hasMultiWallets = walletsWithStatuses.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() + val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() return if (hasMultiWallets) { MyPortfolioUM.AddFirstToken( - bsConfig = createAddToPortfolioBSConfig( + addToPortfolioBSConfig = createAddToPortfolioBSConfig( portfolioData = portfolioData, portfolioUIData = portfolioUIData, availableNetworks = availableNetworks, @@ -55,7 +56,7 @@ internal class MyPortfolioUMMFactory( return TokensPortfolioUMConverter( appCurrency = portfolioData.appCurrency, isBalanceHidden = portfolioData.isBalanceHidden, - isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks), + isAllAvailableNetworksAdded = walletsWithCurrencies.isAllAvailableNetworksAdded(availableNetworks), bsConfig = createAddToPortfolioBSConfig( portfolioData = portfolioData, portfolioUIData = portfolioUIData, @@ -63,8 +64,9 @@ internal class MyPortfolioUMMFactory( ), onAddClick = onAddClick, onTokenItemClick = onTokenItemClick, + quickActionsIntents = tokenActionsHandler, ) - .convert(walletsWithStatuses) + .convert(walletsWithCurrencies) } private fun createAddToPortfolioBSConfig( @@ -73,21 +75,21 @@ internal class MyPortfolioUMMFactory( availableNetworks: List, ): TangemBottomSheetConfig { val walletId = portfolioUIData.selectedWalletId - ?: portfolioData.walletsWithCurrencyStatuses.keys.firstOrNull { it.isMultiCurrency }?.walletId + ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }?.walletId - val selectedWallet = portfolioData.walletsWithCurrencyStatuses.keys + val selectedWallet = portfolioData.walletsWithCurrencies.keys .firstOrNull { it.walletId == walletId } ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") val changedNetworks = portfolioUIData.walletsWithChangedNetworks[portfolioUIData.selectedWalletId] val alreadyAddedNetworks = requireNotNull( - value = portfolioData.walletsWithCurrencyStatuses[selectedWallet], + value = portfolioData.walletsWithCurrencies[selectedWallet], lazyMessage = { "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" }, ) .filterAvailableNetworks(availableNetworks) - .map { it.currency.network.backendId } + .map { it.status.currency.network.backendId } return addToPortfolioBSContentUMFactory.create( portfolioData = portfolioData, @@ -117,7 +119,7 @@ internal class MyPortfolioUMMFactory( } } - private fun Map>.isAllAvailableNetworksAdded( + private fun Map>.isAllAvailableNetworksAdded( availableNetworks: List, ): Boolean { val networkIds = availableNetworks.map { it.networkId } @@ -125,26 +127,26 @@ internal class MyPortfolioUMMFactory( return this // User can add currencies only in multi-currency wallets .filterKeys(UserWallet::isMultiCurrency) - .mapValues { it.value.map { it.currency.network.backendId } } + .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } // Each wallets contains all available networks? .all { it.value.containsAll(networkIds) } } /** Filter map values by available networks [networks] */ - private fun Map>.filterAvailableNetworks( + private fun Map>.filterAvailableNetworks( networks: List, - ): Map> { + ): Map> { return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } } /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ - private fun List.filterAvailableNetworks( + private fun List.filterAvailableNetworks( networks: List, - ): List { + ): List { val networkIds = networks.map(TokenMarketInfo.Network::networkId) return mapNotNull { - it.takeIf { networkIds.contains(it.currency.network.backendId) } + it.takeIf { networkIds.contains(it.status.currency.network.backendId) } } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index 5e05440a85..cf437c80c1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -4,9 +4,15 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList /** * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] @@ -17,27 +23,75 @@ internal class PortfolioTokenUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, -) : Converter, PortfolioTokenUM> { - - override fun convert(value: Pair): PortfolioTokenUM { - val (userWallet, cryptoCurrencyStatus) = value + private val tokenActionsHandler: TokenActionsHandler, +) : Converter { + override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM { val tokenItemStateConverter = TokenItemStateConverter( appCurrency = appCurrency, - titleStateProvider = { TokenItemState.TitleState.Content(text = userWallet.name) }, + titleStateProvider = { TokenItemState.TitleState.Content(text = value.userWallet.name) }, subtitleStateProvider = { - TokenItemState.SubtitleState.TextContent(value = cryptoCurrencyStatus.currency.name) + TokenItemState.SubtitleState.TextContent(value = value.status.currency.name) }, onItemClick = onTokenItemClick, - onItemLongClick = null, + onItemLongClick = { + tokenActionsHandler.onTokenLongClick(cryptoCurrencyData = value) + }, ) return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = cryptoCurrencyStatus), + tokenItemState = tokenItemStateConverter.convert(value = value.status), isBalanceHidden = isBalanceHidden, isQuickActionsShown = false, + quickActions = quickActions(cryptoData = value), + ) + } + + private fun quickActions(cryptoData: PortfolioData.CryptoCurrencyData): PortfolioTokenUM.QuickActions { + return PortfolioTokenUM.QuickActions( + actions = listOfNotNull( + QuickActionUM.Buy.takeIf { + cryptoData.actions.any { + it is TokenActionsState.ActionState.Buy && + it.unavailabilityReason == ScenarioUnavailabilityReason.None + } + }, + QuickActionUM.Exchange.takeIf { + cryptoData.actions.any { + it is TokenActionsState.ActionState.Swap && + it.unavailabilityReason == ScenarioUnavailabilityReason.None + } + }, + QuickActionUM.Receive.takeIf { + cryptoData.actions.any { + it is TokenActionsState.ActionState.Receive && + it.unavailabilityReason == ScenarioUnavailabilityReason.None + } + }, + ).toImmutableList(), onQuickActionClick = { - // TODO: [REDACTED_JIRA] + when (it) { + QuickActionUM.Buy -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.Buy, + cryptoCurrencyData = cryptoData, + ) + QuickActionUM.Exchange -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.Exchange, + cryptoCurrencyData = cryptoData, + ) + QuickActionUM.Receive -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.Receive, + cryptoCurrencyData = cryptoData, + ) + } + }, + onQuickActionLongClick = { + if (it == QuickActionUM.Receive) { + tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.CopyAddress, + cryptoCurrencyData = cryptoData, + ) + } }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt new file mode 100644 index 0000000000..8dea5f71b6 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -0,0 +1,199 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.toImmutableList + +@Suppress("LongParameterList") +@ComponentScoped +internal class TokenActionsHandler @AssistedInject constructor( + private val router: Router, + private val clipboardManager: ClipboardManager, + private val uiMessageSender: UiMessageSender, + private val reduxStateHolder: ReduxStateHolder, + @Assisted + private val currentAppCurrency: Provider, + @Assisted("updateTokenReceiveBSConfig") + private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + @Assisted("updateTokenActionsBSConfig") + private val updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, +) { + + fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + when (action) { + TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Receive -> onReceiveClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData) + } + } + + fun onTokenLongClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + updateTokenActionsBSConfig { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = { + updateTokenActionsBSConfig { + it.copy(isShow = false) + } + }, + content = TokenActionsBSContentUM( + title = cryptoCurrencyData.userWallet.name, + actions = cryptoCurrencyData.actions + .mapNotNull { it.toAction(cryptoCurrencyData) } + .sortedBy { it.order } + .toImmutableList(), + onActionClick = { action -> + handle(action, cryptoCurrencyData) + updateTokenActionsBSConfig { + it.copy(isShow = false) + } + }, + ), + ) + } + } + + private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val cryptoCurrencyStatus = cryptoCurrencyData.status + val currency = cryptoCurrencyStatus.currency + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return + + updateTokenReceiveBSConfig { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = { + updateTokenReceiveBSConfig { + it.copy(isShow = false) + } + }, + content = TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(currency) + .toImmutableList(), + onCopyClick = {}, + onShareClick = {}, + ), + ) + } + } + + private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val cryptoCurrencyStatus = cryptoCurrencyData.status + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return + val addresses = networkAddress.availableAddresses + .mapToAddressModels(cryptoCurrencyStatus.currency) + .toImmutableList() + val defaultAddress = addresses.firstOrNull()?.value ?: return + + clipboardManager.setText(text = defaultAddress) + uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) + } + + private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + reduxStateHolder.dispatch( + TradeCryptoAction.Buy( + userWallet = cryptoCurrencyData.userWallet, + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ), + ) + } + + private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + reduxStateHolder.dispatch( + TradeCryptoAction.Sell( + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ), + ) + } + + private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + router.push( + AppRoute.Swap( + currency = cryptoCurrencyData.status.currency, + userWalletId = cryptoCurrencyData.userWallet.walletId, + ), + ) + } + + private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + router.push( + AppRoute.Send( + userWalletId = cryptoCurrencyData.userWallet.walletId, + currency = cryptoCurrencyData.status.currency, + ), + ) + } + + private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val yield = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake } + ?.let { it as TokenActionsState.ActionState.Stake } + ?.yield ?: return + + router.push( + AppRoute.Staking( + userWalletId = cryptoCurrencyData.userWallet.walletId, + cryptoCurrencyId = cryptoCurrencyData.status.currency.id, + yield = yield, + ), + ) + } + + private fun TokenActionsState.ActionState.toAction( + cryptoCurrencyData: PortfolioData.CryptoCurrencyData, + ): TokenActionsBSContentUM.Action? = when (this) { + is TokenActionsState.ActionState.Buy -> TokenActionsBSContentUM.Action.Buy + is TokenActionsState.ActionState.CopyAddress -> TokenActionsBSContentUM.Action.CopyAddress + is TokenActionsState.ActionState.HideToken -> null + is TokenActionsState.ActionState.Receive -> TokenActionsBSContentUM.Action.Receive + is TokenActionsState.ActionState.Sell -> TokenActionsBSContentUM.Action.Sell + is TokenActionsState.ActionState.Send -> { + TokenActionsBSContentUM.Action.Send.takeIf { + cryptoCurrencyData.status.value.amount.isNullOrZero().not() + } + } + is TokenActionsState.ActionState.Stake -> TokenActionsBSContentUM.Action.Stake + is TokenActionsState.ActionState.Swap -> TokenActionsBSContentUM.Action.Exchange + } + .takeIf { this.unavailabilityReason == ScenarioUnavailabilityReason.None } + + @AssistedFactory + interface Factory { + fun create( + currentAppCurrency: Provider, + @Assisted("updateTokenReceiveBSConfig") + updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + @Assisted("updateTokenActionsBSConfig") + updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + ): TokenActionsHandler + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index e9e61f5618..1614ad4ebb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -5,6 +5,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState import com.tangem.utils.converter.Converter @@ -15,6 +16,7 @@ import kotlinx.collections.immutable.toImmutableList * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class TokensPortfolioUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, @@ -22,25 +24,27 @@ internal class TokensPortfolioUMConverter( private val bsConfig: TangemBottomSheetConfig, private val onAddClick: () -> Unit, private val onTokenItemClick: (index: Int, id: CryptoCurrency.ID) -> Unit, -) : Converter>, MyPortfolioUM.Tokens> { + private val quickActionsIntents: TokenActionsHandler, +) : Converter>, MyPortfolioUM.Tokens> { - override fun convert(value: Map>): MyPortfolioUM.Tokens { + override fun convert(value: Map>): MyPortfolioUM.Tokens { return MyPortfolioUM.Tokens( tokens = value - .flatMap { entry -> - entry.value.map { entry.key to it } - } - .mapIndexed { index, status -> + .flatMap { entry -> entry.value } + .mapIndexed { index, cryptoData -> PortfolioTokenUMConverter( appCurrency = appCurrency, isBalanceHidden = isBalanceHidden, onTokenItemClick = { onTokenItemClick(index, it.currency.id) }, - ).convert(status) + tokenActionsHandler = quickActionsIntents, + ).convert(cryptoData) } .toImmutableList(), buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available, - bsConfig = bsConfig, + addToPortfolioBSConfig = bsConfig, onAddClick = onAddClick, + tokenReceiveBSConfig = TangemBottomSheetConfig.Empty, + tokenActionsBSConfig = TangemBottomSheetConfig.Empty, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 315718d001..f04e4206dc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -73,15 +73,16 @@ private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modif .verticalScroll(state = scrollState) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - UserWalletItem( - state = state.selectedWallet, - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) - - SpacerH12() + if (state.isWalletBlockVisible) { + UserWalletItem( + state = state.selectedWallet, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) + SpacerH12() + } NetworkSelection( modifier = Modifier.fillMaxWidth(), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 8c3b6118fd..dda1777df3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SmallButtonShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -24,8 +25,8 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @Composable internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { @@ -54,8 +55,8 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { } when (state) { - is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.bsConfig) - is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.bsConfig) + is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig) + is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig) else -> Unit } } @@ -105,6 +106,9 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier } } } + + TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig) + TokenActionsBottomSheet(config = state.tokenActionsBSConfig) } @Composable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt index 2b781bd2a7..3b3ed01682 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -25,11 +26,15 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie state = state.tokenItemState, isBalanceHidden = state.isBalanceHidden, modifier = Modifier.background(color = TangemTheme.colors.background.action), + itemPaddingValues = PaddingValues( + start = TangemTheme.dimens.spacing10, + end = TangemTheme.dimens.spacing12, + ), ) PortfolioQuickActions( modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing12) + .background(color = TangemTheme.colors.background.action) .padding( bottom = if (lastInList) { TangemTheme.dimens.spacing12 @@ -37,8 +42,10 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie TangemTheme.dimens.spacing24 }, ), + actions = state.quickActions.actions, isVisible = state.isQuickActionsShown, - onActionClick = state.onQuickActionClick, + onActionClick = state.quickActions.onQuickActionClick, + onActionLongClick = state.quickActions.onQuickActionLongClick, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt index f498942443..ba5ff99531 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt @@ -4,9 +4,7 @@ import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button @@ -15,7 +13,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalLayoutDirection @@ -23,6 +20,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.haptic.TangemHapticEffect @@ -30,34 +28,33 @@ import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList @Composable internal fun PortfolioQuickActions( + actions: ImmutableList, isVisible: Boolean, onActionClick: (QuickActionUM) -> Unit, + onActionLongClick: (QuickActionUM) -> Unit, modifier: Modifier = Modifier, ) { + if (actions.isEmpty()) return + AnimatedVisibility( visible = isVisible, enter = expandVertically(expandFrom = Alignment.Top), exit = shrinkVertically(shrinkTowards = Alignment.Top), ) { Column(modifier = modifier) { - LineSeparator() - QuickActionItem( - state = QuickActionUM.Buy, - onClick = { onActionClick(QuickActionUM.Buy) }, - ) - LineSeparator() - QuickActionItem( - state = QuickActionUM.Exchange, - onClick = { onActionClick(QuickActionUM.Exchange) }, - ) - LineSeparator() - QuickActionItem( - state = QuickActionUM.Receive, - onClick = { onActionClick(QuickActionUM.Receive) }, - ) + actions.fastForEach { action -> + LineSeparator() + QuickActionItem( + state = action, + onClick = { onActionClick(action) }, + onLongClick = { onActionLongClick(action) }.takeIf { action.longClickAvailable }, + ) + } } } } @@ -68,10 +65,9 @@ private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) val lineColor = TangemTheme.colors.stroke.primary val strokeWidth = TangemTheme.dimens.size1 val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val verticalPadding = TangemTheme.dimens.spacing2 - val startPadding = TangemTheme.dimens.spacing18 + val startPadding = TangemTheme.dimens.spacing30 - val height = TangemTheme.dimens.size16 + verticalPadding * 2 + val height = TangemTheme.dimens.size16 Canvas( modifier = modifier @@ -96,31 +92,42 @@ private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) drawLine( color = lineColor, - start = Offset(x, verticalPadding.toPx()), - end = Offset(x, size.height - verticalPadding.toPx()), + start = Offset(x, 0f), + end = Offset(x, size.height), strokeWidth = strokeWidth.toPx(), ) } } -@OptIn(ExperimentalAnimationApi::class) +@OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class) @Composable private fun AnimatedVisibilityScope.QuickActionItem( state: QuickActionUM, onClick: () -> Unit, + onLongClick: (() -> Unit)?, modifier: Modifier = Modifier, ) { val hapticManager = LocalHapticManager.current + val onLongClickInternal: (() -> Unit)? = if (onLongClick != null) { + { + hapticManager.perform(TangemHapticEffect.View.LongPress) + onLongClick() + } + } else { + null + } Row( modifier = modifier .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersMedium) - .clickable { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - } - .padding(horizontal = TangemTheme.dimens.spacing2, vertical = TangemTheme.dimens.spacing3), + .combinedClickable( + onLongClick = onLongClickInternal, + onClick = { + hapticManager.perform(TangemHapticEffect.View.SegmentTick) + onClick() + }, + ) + .padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), ) { @@ -190,8 +197,10 @@ private fun Preview() { modifier = Modifier.background(color = TangemTheme.colors.background.action), ) { PortfolioQuickActions( + actions = QuickActionUM.entries.toImmutableList(), isVisible = isVisible, onActionClick = {}, + onActionLongClick = {}, ) } } @@ -205,8 +214,10 @@ private fun PreviewRtl() { TangemThemePreview(rtl = true) { Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { PortfolioQuickActions( + actions = QuickActionUM.entries.toImmutableList(), isVisible = true, onActionClick = {}, + onActionLongClick = {}, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt index bc725d98f3..57a8133772 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -2,7 +2,6 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -10,12 +9,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SimpleSettingsRow import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle -import com.tangem.core.ui.components.inputrow.InputRowChecked -import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -50,16 +49,15 @@ private fun Content(content: TokenActionsBSContentUM) { else -> CornersToRound.ZERO } - DividerContainer( + Box( modifier = Modifier .clip(cornersToRound.getShape()) - .background(TangemTheme.colors.background.action) - .clickable { content.onActionClick(action) }, - showDivider = index != content.actions.lastIndex, + .background(TangemTheme.colors.background.action), ) { - InputRowChecked( - text = action.text, - checked = false, + SimpleSettingsRow( + title = action.text.resolveReference(), + icon = action.iconRes, + onItemsClick = { content.onActionClick(action) }, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index 725f9518be..3b0e811163 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -3,7 +3,6 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId @@ -54,13 +53,10 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider iconUrl = null, ), isScanCardNotificationVisible = true, + isWalletBlockVisible = true, continueButtonEnabled = true, onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig( - isShow = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), + walletSelectorConfig = TangemBottomSheetConfig.Empty, ), AddToPortfolioBSContentUM( selectedWallet = userWallet, @@ -81,13 +77,10 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider iconUrl = null, ), isScanCardNotificationVisible = true, + isWalletBlockVisible = false, continueButtonEnabled = false, onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig( - isShow = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), + walletSelectorConfig = TangemBottomSheetConfig.Empty, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 02339b5257..247b98ce7d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -2,12 +2,13 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { @@ -16,39 +17,29 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider Unit, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt index 4f08610f4d..cee5e178a7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -10,7 +10,9 @@ internal sealed class MyPortfolioUM { data class Tokens( val tokens: ImmutableList, val buttonState: AddButtonState, - val bsConfig: TangemBottomSheetConfig, + val addToPortfolioBSConfig: TangemBottomSheetConfig, + val tokenReceiveBSConfig: TangemBottomSheetConfig, + val tokenActionsBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() { @@ -22,7 +24,7 @@ internal sealed class MyPortfolioUM { } data class AddFirstToken( - val bsConfig: TangemBottomSheetConfig, + val addToPortfolioBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt index 59dc297746..b4e01c2896 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt @@ -1,10 +1,18 @@ package com.tangem.features.markets.portfolio.impl.ui.state import com.tangem.core.ui.components.token.state.TokenItemState +import kotlinx.collections.immutable.ImmutableList internal data class PortfolioTokenUM( val tokenItemState: TokenItemState, val isBalanceHidden: Boolean, val isQuickActionsShown: Boolean, - val onQuickActionClick: (QuickActionUM) -> Unit, -) \ No newline at end of file + val quickActions: QuickActions, +) { + + data class QuickActions( + val actions: ImmutableList, + val onQuickActionClick: (QuickActionUM) -> Unit, + val onQuickActionLongClick: (QuickActionUM) -> Unit, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt index b079b5ce50..dba8458d97 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -11,6 +11,7 @@ internal enum class QuickActionUM( val title: TextReference, val description: TextReference, @DrawableRes val icon: Int, + val longClickAvailable: Boolean = false, ) { Buy( title = resourceReference(R.string.common_buy), @@ -26,5 +27,6 @@ internal enum class QuickActionUM( title = resourceReference(R.string.common_receive), description = resourceReference(R.string.receive_token_description), icon = R.drawable.ic_arrow_down_24, + longClickAvailable = true, ), } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt index 7b2a115f54..83450b1ebc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.impl.ui.state +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference @@ -16,13 +17,38 @@ internal data class TokenActionsBSContentUM( @Immutable enum class Action( val text: TextReference, + @DrawableRes val iconRes: Int, ) { - CopyAddress(text = resourceReference(R.string.common_copy_address)), - Receive(text = resourceReference(R.string.common_receive)), - Sell(text = resourceReference(R.string.common_sell)), - Buy(text = resourceReference(R.string.common_buy)), - Send(text = resourceReference(R.string.common_send)), - Exchange(text = resourceReference(R.string.common_exchange)), - Stake(text = resourceReference(R.string.common_stake)), + CopyAddress( + text = resourceReference(R.string.common_copy_address), + iconRes = R.drawable.ic_copy_24, + ), + Send( + text = resourceReference(R.string.common_send), + iconRes = R.drawable.ic_arrow_up_24, + ), + Receive( + text = resourceReference(R.string.common_receive), + iconRes = R.drawable.ic_arrow_down_24, + ), + Buy( + text = resourceReference(R.string.common_buy), + iconRes = R.drawable.ic_plus_24, + ), + Sell( + text = resourceReference(R.string.common_sell), + iconRes = R.drawable.ic_currency_24, + ), + Exchange( + text = resourceReference(R.string.common_exchange), + iconRes = R.drawable.ic_exchange_horizontal_24, + ), + Stake( + text = resourceReference(R.string.common_stake), + iconRes = R.drawable.ic_staking_24, + ), + ; + + val order: Int = ordinal } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index 295cebedcf..0a482edd91 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -121,16 +121,18 @@ internal class MarketsTokenItemConverter( } } + @Suppress("MagicNumber") private fun TokenMarket.getTrendType(): PriceChangeType { val percent = when (currentTrendInterval) { TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent TrendInterval.D7 -> tokenQuotesShort.weekChangePercent TrendInterval.M1 -> tokenQuotesShort.monthChangePercent - }.setScale(2, RoundingMode.UP) - - return when (percent.compareTo(BigDecimal.ZERO)) { - 1 -> PriceChangeType.UP - -1 -> PriceChangeType.DOWN + } + val scaled = percent.setScale(4, RoundingMode.HALF_UP) + return when { + scaled == null -> PriceChangeType.NEUTRAL + scaled > BigDecimal.ZERO -> PriceChangeType.UP + scaled < BigDecimal.ZERO -> PriceChangeType.DOWN else -> PriceChangeType.NEUTRAL } } diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index de5b9043b9..32efd6bb49 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.core.deepLinks.global) implementation(projects.core.featuretoggles) implementation(projects.core.decompose) + implementation(projects.common.ui) implementation(projects.libs.crypto) 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 a13a7b2a77..934b75a0d1 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 @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import androidx.paging.PagingData import arrow.core.Either +import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig @@ -12,7 +13,6 @@ import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent 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.card.NetworkHasDerivationUseCase @@ -216,7 +216,7 @@ internal class TokenDetailsStateFactory( isShow = true, onDismissRequest = clickIntents::onDismissDialog, content = TokenDetailsDialogConfig.DialogContentConfig.DisabledButtonReasonDialogConfig( - text = getUnavailabilityReasonText(unavailabilityReason), + text = unavailabilityReason.getUnavailabilityReasonText(), onConfirmClick = clickIntents::onDismissDialog, ), ), @@ -408,66 +408,4 @@ internal class TokenDetailsStateFactory( }.toImmutableList(), ) } - - private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { - return when (unavailabilityReason) { - is ScenarioUnavailabilityReason.StakingUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_staking_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.PendingTransaction -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_send, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_sell, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - } - } - is ScenarioUnavailabilityReason.EmptyBalance -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_send, - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_sell, - ) - } - } - is ScenarioUnavailabilityReason.BuyUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_buy_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotExchangeable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_not_exchangeable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotSupportedBySellService -> { - resourceReference( - id = R.string.token_button_unavailability_reason_sell_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - ScenarioUnavailabilityReason.Unreachable -> { - resourceReference( - 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") - } - } - } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 75b2af5fb3..5aa98a53a1 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -78,7 +78,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.analytics) implementation(projects.domain.visa) - implementation(projects.domain.staking) + implementation(projects.domain.staking.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 9d9206c566..74b81ff310 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.arkivanov.decompose.defaultComponentContext +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.utils.asRouter import com.tangem.core.decompose.context.DefaultAppComponentContext import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.ui.UiDependencies @@ -44,6 +46,9 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var marketsFeatureToggles: MarketsFeatureToggles + @Inject + internal lateinit var appRouter: AppRouter + private var marketsEntryComponent: MarketsEntryComponent? = null private val _walletRouter: InnerWalletRouter @@ -60,6 +65,7 @@ internal class WalletFragment : ComposeFragment() { messageHandler = uiDependencies.eventMessageHandler, dispatchers = coroutineDispatcherProvider, hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), ) marketsEntryComponent = marketsEntryComponentFactory.create(appContext) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 95f595916e..f4009e9c73 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -62,7 +62,7 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Stake -> { title = resourceReference(R.string.common_stake) icon = R.drawable.ic_staking_24 - action = { clickIntents.onStakeClick(cryptoCurrencyStatus) } + action = { clickIntents.onStakeClick(cryptoCurrencyStatus, actionsState.yield) } } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) 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 f00e79aa55..e66128d2c8 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 @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents -import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -14,7 +14,6 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModel import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -22,7 +21,7 @@ import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.staking.GetYieldUseCase +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency @@ -67,7 +66,7 @@ interface WalletCurrencyActionsClickIntents { fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? @@ -100,7 +99,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, - private val getYieldUseCase: GetYieldUseCase, private val appRouter: AppRouter, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { @@ -380,25 +378,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } - override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) viewModelScope.launch { val userWalletId = stateHolder.getSelectedWalletId() val cryptoCurrency = cryptoCurrencyStatus.currency - val yield = getYieldUseCase.invoke( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, - ).getOrElse { - error("Staking is unavailable for ${cryptoCurrency.name}") - } - reduxStateHolder.dispatch( - TradeCryptoAction.Stake( + appRouter.push( + AppRoute.Staking( userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrency.id, - yield = yield, + yield = yield ?: return@launch, ), ) } @@ -487,14 +479,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false - val unavailabilityReasonText = getUnavailabilityReasonText(unavailabilityReason) - viewModelScope.launch(dispatchers.main) { walletEventSender.send( event = WalletEvent.ShowAlert( state = WalletAlertState.DefaultAlert( title = null, - message = unavailabilityReasonText, + message = unavailabilityReason.getUnavailabilityReasonText(), onConfirmClick = null, ), ), @@ -503,66 +493,4 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( return true } - - private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { - return when (unavailabilityReason) { - is ScenarioUnavailabilityReason.StakingUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_staking_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.PendingTransaction -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_send, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_sell, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - } - } - is ScenarioUnavailabilityReason.EmptyBalance -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_send, - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_sell, - ) - } - } - is ScenarioUnavailabilityReason.BuyUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_buy_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotExchangeable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_not_exchangeable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotSupportedBySellService -> { - resourceReference( - id = R.string.token_button_unavailability_reason_sell_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - ScenarioUnavailabilityReason.Unreachable -> { - resourceReference( - 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") - } - } - } } \ No newline at end of file