From 20eb597613325c26ff00d7e35729ee92fb585d4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 20:29:25 +0500 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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