From 20eb597613325c26ff00d7e35729ee92fb585d4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 20:29:25 +0500 Subject: [PATCH 01/41] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 +- .../datasource/api/stakekit/StakeKitApi.kt | 2 +- .../local/token/DefaultStakingBalanceStore.kt | 46 ++++++------ .../local/token/StakingBalanceStore.kt | 13 ++-- .../data/staking/DefaultStakingRepository.kt | 75 ++++++++++--------- .../converters/YieldBalanceListConverter.kt | 4 +- .../DefaultWalletManagersFacade.kt | 11 +-- .../walletmanager/WalletManagersFacade.kt | 6 +- .../FetchStakingYieldBalanceUseCase.kt | 6 +- .../staking/GetStakingYieldBalanceUseCase.kt | 6 +- .../staking/repositories/StakingRepository.kt | 15 ++-- .../tokens/FetchCardTokenListUseCase.kt | 10 ++- .../tokens/FetchCurrencyStatusUseCase.kt | 19 ++++- .../CurrenciesStatusesLceOperations.kt | 7 +- .../CurrenciesStatusesOperations.kt | 50 +++++-------- .../repository/MockStakingRepository.kt | 15 ++-- .../viewmodel/StakingViewModel.kt | 70 ++++++++++++++--- .../WalletCurrencyActionsClickIntents.kt | 17 ++--- 18 files changed, 217 insertions(+), 158 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 24c222149b..6e5da90b47 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -158,8 +158,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): FetchCurrencyStatusUseCase { - return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository) + return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 2d5bd551ac..1c9b1ab3e4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -34,7 +34,7 @@ interface StakeKitApi { @POST("yields/balances") suspend fun getMultipleYieldBalances( @Body body: List, - ): ApiResponse> + ): ApiResponse> @POST("yields/{integrationId}/balances") suspend fun getSingleYieldBalance( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index c88970b238..a80f1b8e05 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -3,49 +3,53 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock internal class DefaultStakingBalanceStore( - private val dataStore: StringKeyDataStore>, + private val dataStore: StringKeyDataStore>, ) : StakingBalanceStore { - override fun get(): Flow> { - return dataStore.get(STAKING_BALANCE_KEY) + private val mutex = Mutex() + + override fun get(userWalletId: UserWalletId): Flow> { + return dataStore.get(userWalletId.stringValue) } - override suspend fun getSyncOrNull(): List? { - return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? { + return dataStore.getSyncOrNull(userWalletId.stringValue) } - override suspend fun store(items: List) { - return dataStore.store(STAKING_BALANCE_KEY, items) + override suspend fun store(userWalletId: UserWalletId, items: Set) { + mutex.withLock { + dataStore.store(userWalletId.stringValue, items) + } } - override fun get(integrationId: String): Flow> { - return dataStore.get(STAKING_BALANCE_KEY) + override fun get(userWalletId: UserWalletId, integrationId: String): Flow> { + return dataStore.get(userWalletId.stringValue) .map { balances -> balances.filter { it.integrationId == integrationId } .flatMap { it.balances } } } - override suspend fun getSyncOrNull(integrationId: String): List? { - return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? { + return dataStore.getSyncOrNull(userWalletId.stringValue) ?.firstOrNull { it.integrationId == integrationId }?.balances } - override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) { - val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY) - ?.toMutableList() - ?.addOrReplace(item) { item.integrationId == integrationId } - ?: listOf(item) + override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) { + mutex.withLock { + val balances = dataStore.getSyncOrNull(userWalletId.stringValue) + ?.addOrReplace(item) { it.integrationId == integrationId } + ?: setOf(item) - return dataStore.store(STAKING_BALANCE_KEY, balances) - } - - companion object { - private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY" + dataStore.store(userWalletId.stringValue, balances) + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index 0a9ea06c9e..1dc58ba38d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -2,19 +2,20 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow interface StakingBalanceStore { - fun get(): Flow> + fun get(userWalletId: UserWalletId): Flow> - suspend fun getSyncOrNull(): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? - suspend fun store(items: List) + suspend fun store(userWalletId: UserWalletId, items: Set) - fun get(integrationId: String): Flow> + fun get(userWalletId: UserWalletId, integrationId: String): Flow> - suspend fun getSyncOrNull(integrationId: String): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? - suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) + suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 9ce502abe3..541762c34d 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -45,7 +45,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -261,25 +260,27 @@ internal class DefaultStakingRepository( override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext - val cryptoCurrency = address.cryptoCurrency val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() + cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val requestBody = getBalanceRequestData(address.address, integrationId) + val requestBody = getBalanceRequestData(address, integrationId) val result = stakeKitApi.getSingleYieldBalance( integrationId = requestBody.integrationId, body = requestBody, ).getOrThrow() stakingBalanceStore.store( + userWalletId, requestBody.integrationId, YieldBalanceWrapperDTO( balances = result, @@ -292,15 +293,15 @@ internal class DefaultStakingRepository( override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalance.Empty) } else { launch(dispatchers.io) { - val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: error("Could not get integrationId") - stakingBalanceStore.get(integrationId) + stakingBalanceStore.get(userWalletId, integrationId) .collectLatest { send( yieldBalanceConverter.convert( @@ -316,7 +317,7 @@ internal class DefaultStakingRepository( withContext(dispatchers.io) { fetchSingleYieldBalance( userWalletId, - address, + cryptoCurrency, ) } } @@ -324,16 +325,18 @@ internal class DefaultStakingRepository( override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): YieldBalance = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) { YieldBalance.Empty } else { - fetchSingleYieldBalance(userWalletId, address) + fetchSingleYieldBalance(userWalletId, cryptoCurrency) - val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: error("Could not get integrationId") - val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error + val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) + ?: return@withContext YieldBalance.Error + yieldBalanceConverter.convert( YieldBalanceConverter.Data( balance = result, @@ -345,7 +348,7 @@ internal class DefaultStakingRepository( override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext @@ -357,23 +360,23 @@ internal class DefaultStakingRepository( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val result = stakeKitApi.getMultipleYieldBalances( - addresses - .mapNotNull { networkAddress -> - val cryptoCurrency = networkAddress.cryptoCurrency - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] + val availableCurrencies = cryptoCurrencies + .mapNotNull { currency -> + val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network) + val integrationId = integrationIdMap[currency.id.getIntegrationKey()] - if (integrationId != null) { - networkAddress.address to integrationId - } else { - null - } + if (integrationId != null && address != null) { + address to integrationId + } else { + null } - .distinct() - .map { getBalanceRequestData(it.first, it.second) }, - ).getOrThrow() + } + .distinct() + .map { getBalanceRequestData(it.first, it.second) } + .ifEmpty { return@invokeOnExpire } + val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() - stakingBalanceStore.store(result) + stakingBalanceStore.store(userWalletId, result) }, ) } finally { @@ -385,20 +388,20 @@ internal class DefaultStakingRepository( override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { launch(dispatchers.io) { - stakingBalanceStore.get() + stakingBalanceStore.get(userWalletId) .collectLatest { send(yieldBalanceListConverter.convert(it)) } } withContext(dispatchers.io) { fetchMultiYieldBalance( userWalletId, - addresses, + cryptoCurrencies, ) } } @@ -406,14 +409,14 @@ internal class DefaultStakingRepository( override fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow = lceFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { launch(dispatchers.io) { combine( - stakingBalanceStore.get(), + stakingBalanceStore.get(userWalletId), isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } }, ) { result, isFetching -> val balances = yieldBalanceListConverter.convert(result) @@ -422,7 +425,7 @@ internal class DefaultStakingRepository( } withContext(dispatchers.io) { catch( - block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) }, + block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) }, catch = { raise(it) }, ) } @@ -431,13 +434,13 @@ internal class DefaultStakingRepository( override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) { YieldBalanceList.Empty } else { - fetchMultiYieldBalance(userWalletId, addresses) - val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error + fetchMultiYieldBalance(userWalletId, cryptoCurrencies) + val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error yieldBalanceListConverter.convert(result) } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt index 0476368b30..007c73bfe1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.utils.converter.Converter -internal class YieldBalanceListConverter : Converter, YieldBalanceList> { +internal class YieldBalanceListConverter : Converter, YieldBalanceList> { internal val converter by lazy(LazyThreadSafetyMode.NONE) { YieldBalanceConverter() } - override fun convert(value: List): YieldBalanceList { + override fun convert(value: Set): YieldBalanceList { return if (value.isEmpty()) { YieldBalanceList.Empty } else { diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 68f4060640..7b187af646 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -376,15 +376,12 @@ class DefaultWalletManagersFacade( return walletManagersStore.getAllSync(userWalletId) } - @Deprecated( - "Use NetworkAddress from CryptoCurrencyStatus", - ReplaceWith("cryptoCurrencyStatus.value.networkAddress"), - ) - override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ - return getAddresses(userWalletId, network).sortedBy { it.type } + override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? { + return getAddresses(userWalletId, network) + .firstOrNull { it.type == AddressType.Default } + ?.value } - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
{ val manager = getOrCreateWalletManager( userWalletId = userWalletId, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 4ecfbe3805..e55a359854 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -117,20 +117,18 @@ interface WalletManagersFacade { suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List /** - * Returns ordered list of addresses for selected wallet for given currency + * Returns default network address for selected wallet in given network * * @param userWalletId selected wallet id * @param network network of currency */ - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") - suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
+ suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? /** Returns list of all addresses for all currencies in selected wallet * * @param userWalletId selected wallet id * @param network required to create wallet manager */ - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
/** diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 333762d0e1..e09a9309c7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.raise.either import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( @@ -16,7 +16,7 @@ class FetchStakingYieldBalanceUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean = false, ): Either { return either { @@ -24,7 +24,7 @@ class FetchStakingYieldBalanceUseCase( block = { stakingRepository.fetchSingleYieldBalance( userWalletId = userWalletId, - address = address, + cryptoCurrency = cryptoCurrency, refresh = refresh, ) }, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt index 5c5528b931..16d17d97ad 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -20,11 +20,11 @@ class GetStakingYieldBalanceUseCase( operator fun invoke( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): EitherFlow { return stakingRepository.getSingleYieldBalanceFlow( userWalletId = userWalletId, - address = address, + cryptoCurrency = cryptoCurrency, ).map> { it.right() } .catch { emit(stakingErrorResolver.resolve(it).left()) } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 5b6604423e..8a48c154a9 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -15,7 +15,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -38,33 +37,33 @@ interface StakingRepository { suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean = false, ) - fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, address: CryptoCurrencyAddress): Flow + fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow - suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, address: CryptoCurrencyAddress): YieldBalance + suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean = false, ) fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index ca9266b2d1..e40ecd7e98 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -44,6 +44,7 @@ class FetchCardTokenListUseCase( val yieldBalances = async { fetchYieldBalances( userWalletId = userWalletId, + currencies = currencies, refresh = refresh, ) } @@ -77,10 +78,13 @@ class FetchCardTokenListUseCase( ) } - private suspend fun fetchYieldBalances(userWalletId: UserWalletId, refresh: Boolean) { - val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) + private suspend fun fetchYieldBalances( + userWalletId: UserWalletId, + currencies: List, + refresh: Boolean, + ) { catch( - block = { stakingRepository.fetchMultiYieldBalance(userWalletId, networkAddresses, refresh) }, + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, catch = { /* Ignore error */ }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 8fbd29d4f5..7b631669e7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -29,6 +30,7 @@ class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, + private val stakingRepository: StakingRepository, ) { /** @@ -80,8 +82,11 @@ class FetchCurrencyStatusUseCase( val fetchQuote = async { fetchQuote(currency.id, refresh) } + val fetchStakingBalance = async { + fetchStakingBalance(userWalletId, currency, refresh) + } - awaitAll(fetchStatus, fetchQuote) + awaitAll(fetchStatus, fetchQuote, fetchStakingBalance) } private suspend fun Raise.getCurrency( @@ -122,4 +127,16 @@ class FetchCurrencyStatusUseCase( raise(CurrencyStatusError.DataError(it)) } } + + private suspend fun Raise.fetchStakingBalance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + refresh: Boolean, + ) { + catch( + block = { stakingRepository.fetchSingleYieldBalance(userWalletId, cryptoCurrency, refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 18f07bea3e..59461356b9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -69,11 +69,10 @@ internal class CurrenciesStatusesLceOperations( val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - val addresses = networksRepository.getNetworkAddresses(userWalletId) combine( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), - getYieldBalances(userWalletId, addresses), + getYieldBalances(userWalletId, nonEmptyCurrencies), ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> val statuses = createCurrenciesStatuses( currencies = nonEmptyCurrencies, @@ -196,11 +195,11 @@ internal class CurrenciesStatusesLceOperations( private fun getYieldBalances( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow { return stakingRepository.getMultiYieldBalanceLce( userWalletId = userWalletId, - addresses = addresses, + cryptoCurrencies = cryptoCurrencies, ).map { maybeBalances -> maybeBalances.mapError { TokenListError.DataError(it) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index e82bad56f2..55ba05a3d3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -11,7 +11,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* // FIXME: Refactor - [REDACTED_JIRA] @@ -35,7 +34,7 @@ internal class CurrenciesStatusesOperations( val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() val networkStatuses = networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() - val yieldBalances = getYieldBalancesSync() + val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies) return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances) }, @@ -147,7 +146,7 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - getYieldBalances(), + getYieldBalances(nonEmptyCurrencies), ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances) } @@ -385,25 +384,23 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } - @OptIn(ExperimentalCoroutinesApi::class) - private fun getYieldBalances(): EitherFlow { - return networksRepository.getNetworkAddressesFlow(userWalletId).flatMapLatest { addresses -> - stakingRepository.getMultiYieldBalanceFlow( - userWalletId = userWalletId, - addresses = addresses, - ).map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } - } + private fun getYieldBalances(cryptoCurrencies: List): Flow> { + return stakingRepository.getMultiYieldBalanceFlow( + userWalletId = userWalletId, + cryptoCurrencies = cryptoCurrencies, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } } - private suspend fun getYieldBalancesSync(): Either { + private suspend fun getYieldBalancesSync( + cryptoCurrencies: List, + ): Either { return catch( block = { - val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) stakingRepository.getMultiYieldBalanceSync( userWalletId, - networkAddresses, + cryptoCurrencies, ).right() }, catch = { @@ -417,10 +414,9 @@ internal class CurrenciesStatusesOperations( ): Either { return catch( block = { - val address = networksRepository.getNetworkAddress(userWalletId, cryptoCurrency) stakingRepository.getSingleYieldBalanceSync( userWalletId, - address, + cryptoCurrency, ).right() }, catch = { @@ -429,19 +425,13 @@ internal class CurrenciesStatusesOperations( ) } - @OptIn(ExperimentalCoroutinesApi::class) private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow { - return networksRepository.getNetworkAddressFlow( - userWalletId, - cryptoCurrency, - ).flatMapLatest { address -> - stakingRepository.getSingleYieldBalanceFlow( - userWalletId = userWalletId, - address = address, - ).map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } - } + return stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } } private fun getIds( diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 33c6654e89..a19fea75b9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -16,7 +16,6 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -118,7 +117,7 @@ class MockStakingRepository : StakingRepository { override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean, ) { /* no-op */ @@ -126,19 +125,19 @@ class MockStakingRepository : StakingRepository { override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { send(YieldBalance.Error) } override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): YieldBalance = YieldBalance.Error override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean, ) { /* no-op */ @@ -146,7 +145,7 @@ class MockStakingRepository : StakingRepository { override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow = channelFlow { send( YieldBalanceList.Data( @@ -157,7 +156,7 @@ class MockStakingRepository : StakingRepository { override fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow = lceFlow { send( YieldBalanceList.Data( @@ -168,7 +167,7 @@ class MockStakingRepository : StakingRepository { override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList = YieldBalanceList.Data( balances = listOf(YieldBalance.Error), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 33f5b8df76..09221fb03d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -25,16 +25,19 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -54,8 +57,8 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -80,12 +83,17 @@ internal class StakingViewModel @Inject constructor( private val submitHashUseCase: SubmitHashUseCase, private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val getAllowanceUseCase: GetAllowanceUseCase, private val getFeeUseCase: GetFeeUseCase, private val isApproveNeededUseCase: IsApproveNeededUseCase, private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, + @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -575,7 +583,7 @@ internal class StakingViewModel @Inject constructor( }, ifRight = { txHash -> submitHash(transactionId, txHash) - updateStakeBalance() + scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( txHash = txHash, networkId = cryptoCurrencyStatus.currency.network.id, @@ -608,14 +616,55 @@ internal class StakingViewModel @Inject constructor( } } - private fun updateStakeBalance() { - viewModelScope.launch { - stakingYieldBalanceUseCase( + private fun scheduleUpdates() { + coroutineScope.launch { + listOf( + // we should update network to find pending tx after 1 sec + async { + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network)) + }, + // we should update tx history and network for new balances + async { + updateStakeBalance() + }, + async { + updateTxHistory() + }, + async { + updateNetworkStatuses() + }, + ).awaitAll() + } + } + + private suspend fun updateNetworkStatuses() { + updateDelayedNetworkStatusUseCase( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + delayMillis = BALANCE_UPDATE_DELAY, + refresh = true, + ) + } + + private suspend fun updateStakeBalance() { + stakingYieldBalanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + refresh = true, + ) + } + + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWalletId, + currency = cryptoCurrencyStatus.currency, + ) + + txHistoryItemsCountEither.onRight { + getTxHistoryItemsUseCase( userWalletId = userWalletId, - address = CryptoCurrencyAddress( - cryptoCurrencyStatus.currency, - cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), - ), + currency = cryptoCurrencyStatus.currency, refresh = true, ) } @@ -630,5 +679,6 @@ internal class StakingViewModel @Inject constructor( private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking" const val ALLOWANCE_UPDATE_DELAY = 10_000L + const val BALANCE_UPDATE_DELAY = 11_000L } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 98590b9a89..cb24122b08 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -223,19 +223,16 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) viewModelScope.launch(dispatchers.main) { - walletManagersFacade.getAddress( + walletManagersFacade.getDefaultAddress( userWalletId = stateHolder.getSelectedWalletId(), network = cryptoCurrencyStatus.currency.network, - ) - .find { it.type == AddressType.Default } - ?.value - ?.let { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + )?.let { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) - walletEventSender.send( - event = WalletEvent.CopyAddress(address = it), - ) - } + walletEventSender.send( + event = WalletEvent.CopyAddress(address = it), + ) + } } } From 1715bcd79c0ff814041a89c1ce313fd9a695d6bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 15:05:00 +0300 Subject: [PATCH 02/41] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 4 +- .../java/com/tangem/tap/TangemApplication.kt | 8 +- .../domain/tasks/product/DerivationsFinder.kt | 11 ++- .../datasource/di/UserTokensStoreModule.kt | 34 ------- .../token/AppPreferencesUserTokensStore.kt | 63 ------------- .../datasource/local/token/UserTokensStore.kt | 46 ---------- .../local/token/UserTokensStoreMigration.kt | 56 ------------ .../token/UserTokensStoreMigrationRunner.kt | 50 ----------- .../tangem/data/tokens/di/TokensDataModule.kt | 9 +- .../repository/DefaultCurrenciesRepository.kt | 90 +++++++++++++------ .../repository/DefaultNetworksRepository.kt | 18 ++-- 11 files changed, 96 insertions(+), 293 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ff4810cfc6..21ab576e7f 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -10,7 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -76,7 +76,7 @@ interface ApplicationEntryPoint { fun getBalanceHidingRepository(): BalanceHidingRepository - fun getUserTokensStore(): UserTokensStore + fun getAppPreferencesStore(): AppPreferencesStore fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 42280203ef..ef8490029a 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -22,7 +22,7 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -126,8 +126,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val balanceHidingRepository: BalanceHidingRepository get() = entryPoint.getBalanceHidingRepository() - private val userTokensStore: UserTokensStore - get() = entryPoint.getUserTokensStore() + private val appPreferencesStore: AppPreferencesStore + get() = entryPoint.getAppPreferencesStore() val getAppThemeModeUseCase: GetAppThemeModeUseCase get() = entryPoint.getGetAppThemeModeUseCase() @@ -228,7 +228,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { } derivationsFinder = DerivationsFinder( - newTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, dispatchers = AppCoroutineDispatcherProvider(), ) appStateHolder.mainStore = store diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index d1f7b8a3d6..6fcad66476 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -5,7 +5,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.models.scan.CardDTO @@ -22,7 +25,7 @@ internal data class BlockchainToDerive( // FIXME: May be move to DI, currently unnecessary internal class DerivationsFinder( - private val newTokensStore: UserTokensStore, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -64,7 +67,9 @@ internal class DerivationsFinder( } private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - val responseTokens = newTokensStore.getSyncOrNull(userWalletId) + val responseTokens = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?.tokens ?: return hashSetOf() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt deleted file mode 100644 index b2997661fb..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.token.AppPreferencesUserTokensStore -import com.tangem.datasource.local.token.UserTokensStore -import com.tangem.datasource.local.token.UserTokensStoreMigrationRunner -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object UserTokensStoreModule { - - @Provides - @Singleton - fun provideUserTokensStore( - appPreferencesStore: AppPreferencesStore, - userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, - ): UserTokensStore { - return AppPreferencesUserTokensStore( - appPreferencesStore = appPreferencesStore, - userTokensStoreMigrationRunner = userTokensStoreMigrationRunner, - userWalletsStore = userWalletsStore, - dispatchers = dispatchers, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt deleted file mode 100644 index 80d70825bf..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* - -/** - * Implementation of [UserTokensStore] that based on [appPreferencesStore] - * - * @property appPreferencesStore application preference store - * -[REDACTED_AUTHOR] - */ -internal class AppPreferencesUserTokensStore( - private val appPreferencesStore: AppPreferencesStore, - private val userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, - private val userWalletsStore: UserWalletsStore, - private val dispatchers: CoroutineDispatcherProvider, -) : UserTokensStore { - - init { - runUserTokensMigrations() - } - - override fun get(key: UserWalletId): Flow { - return appPreferencesStore - .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) - .filterNotNull() - } - - override suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? { - return appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), - ) - } - - override suspend fun store(key: UserWalletId, value: UserTokensResponse) { - appPreferencesStore.storeObject( - key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), - value = value, - ) - } - - // TODO: delete in 5.15 (Mobile Sprint 161) [REDACTED_JIRA] - private fun runUserTokensMigrations() { - userWalletsStore.userWallets - .filter { it.isNotEmpty() } - .take(1) - .onEach { userWallets -> - userTokensStoreMigrationRunner.run(ids = userWallets.map { it.walletId.stringValue }) - } - .flowOn(dispatchers.io) - .launchIn(CoroutineScope(dispatchers.io)) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt deleted file mode 100644 index f3f12f026b..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -@Deprecated( - message = "Use AppPreferencesStore", - replaceWith = ReplaceWith( - expression = "AppPreferencesStore", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, -) -interface UserTokensStore { - - @Deprecated( - message = "Use getObject", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.getObject(userWalletId)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - fun get(key: UserWalletId): Flow - - @Deprecated( - message = "Use getObjectSyncOrNull", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.getObjectSyncOrNull(userWalletId)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? - - @Deprecated( - message = "Use storeObject", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.storeObject(userWalletId, response)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - suspend fun store(key: UserWalletId, value: UserTokensResponse) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt deleted file mode 100644 index dae09c0616..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.datasource.local.token - -import androidx.datastore.core.DataMigration -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject - -/** - * Migration of saving [UserTokensResponse] from file to [AppPreferencesStore] - * - * @param userWalletId user wallet id - * @param moshi moshi - * @property fileReader file reader - * -[REDACTED_AUTHOR] - */ -internal class UserTokensStoreMigration( - userWalletId: String, - moshi: Moshi, - private val fileReader: FileReader, -) : DataMigration { - - private val legacyFileName = "user_tokens_$userWalletId" - private val keyName = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId) - - @OptIn(ExperimentalStdlibApi::class) - private val adapter = moshi.adapter() - - override suspend fun shouldMigrate(currentData: AppPreferencesStore): Boolean = true - - override suspend fun migrate(currentData: AppPreferencesStore): AppPreferencesStore { - val currentKey = currentData.getObjectSyncOrNull(key = keyName) - - if (currentKey != null) return currentData - - val value = runCatching { - val json = fileReader.readFile(legacyFileName) - adapter.fromJson(json) - }.getOrNull() - - if (value != null) { - currentData.storeObject(key = keyName, value = value) - } - - return currentData - } - - override suspend fun cleanUp() { - fileReader.removeFile(legacyFileName) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt deleted file mode 100644 index eafb84aa79..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.datasource.local.token - -import com.squareup.moshi.Moshi -import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Runner that launch migrations of saving user tokens store - * - * @property appPreferencesStore application preference store - * @property fileReader file reader - * @property moshi moshi - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -@Singleton -class UserTokensStoreMigrationRunner @Inject constructor( - private val appPreferencesStore: AppPreferencesStore, - private val fileReader: FileReader, - @NetworkMoshi private val moshi: Moshi, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend fun run(ids: List) { - ids.forEach { id -> - coroutineScope { run(id) } - } - } - - private suspend fun run(id: String) { - withContext(dispatchers.io) { - val migration = UserTokensStoreMigration( - userWalletId = id, - moshi = moshi, - fileReader = fileReader, - ) - - migration.migrate(appPreferencesStore) - - migration.cleanUp() - } - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 59ddeeb01d..c37a3bc87f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -8,7 +8,6 @@ import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -28,7 +27,7 @@ internal object TokensDataModule { fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, - userTokensStore: UserTokensStore, + appPreferencesStore: AppPreferencesStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, expressAssetsStore: ExpressAssetsStore, @@ -38,7 +37,7 @@ internal object TokensDataModule { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, tangemExpressApi = tangemExpressApi, - userTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, expressAssetsStore = expressAssetsStore, @@ -71,7 +70,7 @@ internal object TokensDataModule { networksStatusesStore: NetworksStatusesStore, walletManagersFacade: WalletManagersFacade, userWalletsStore: UserWalletsStore, - userTokensStore: UserTokensStore, + appPreferencesStore: AppPreferencesStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): NetworksRepository { @@ -79,7 +78,7 @@ internal object TokensDataModule { networksStatusesStore = networksStatusesStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, - userTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ec3720fe1b..d1bc4dc436 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -18,8 +18,12 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation @@ -46,11 +50,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, - private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, private val expressAssetsStore: ExpressAssetsStore, private val cacheRegistry: CacheRegistry, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -86,7 +90,7 @@ internal class DefaultCurrenciesRepository( override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) { return withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, ) @@ -141,7 +145,7 @@ internal class DefaultCurrenciesRepository( override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, ) @@ -163,7 +167,7 @@ internal class DefaultCurrenciesRepository( override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) { return withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" }, ) @@ -276,9 +280,12 @@ internal class DefaultCurrenciesRepository( fetchTokensIfCacheExpired(userWallet, refresh) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWallet.walletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) } @@ -290,9 +297,12 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val response = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrency( currencyId = id, @@ -312,9 +322,12 @@ internal class DefaultCurrenciesRepository( fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) val blockchain = Blockchain.fromId(networkId.value) val blockchainNetworkId = blockchain.toNetworkId() val coinId = blockchain.toCoinId() @@ -335,7 +348,7 @@ internal class DefaultCurrenciesRepository( ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) launch(dispatchers.io) { - userTokensStore.get(userWalletId) + getSavedUserTokensResponse(userWalletId) .map { it.group == UserTokensResponse.GroupType.NETWORK } .collect(::send) } @@ -347,7 +360,7 @@ internal class DefaultCurrenciesRepository( ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) launch(dispatchers.io) { - userTokensStore.get(userWalletId) + getSavedUserTokensResponse(userWalletId) .map { it.sort == UserTokensResponse.SortType.BALANCE } .collect(::send) } @@ -461,9 +474,14 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) fetchTokensIfCacheExpired(userWallet, refresh = false) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), + ), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) return storedTokens.tokens.any { it.contractAddress != null && @@ -473,7 +491,7 @@ internal class DefaultCurrenciesRepository( } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { - return userTokensStore.get(userWallet.walletId).map { storedTokens -> + return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( response = storedTokens, scanResponse = userWallet.scanResponse, @@ -517,17 +535,26 @@ internal class DefaultCurrenciesRepository( .let { customTokensMerger.mergeIfPresented(userWalletId, response) } .let(userTokensBackwardCompatibility::applyCompatibilityAndGetUpdated) - userTokensStore.store(userWallet.walletId, compatibleUserTokensResponse) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWallet.walletId.stringValue), + value = compatibleUserTokensResponse, + ) + fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse) } private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean { - return demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(userWallet.walletId) == null + val response = getSavedUserTokensResponseSync(key = userWallet.walletId) + + return demoConfig.isDemoCardId(userWallet.cardId) && response == null } private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) - userTokensStore.store(userWalletId, compatibleUserTokensResponse) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) pushTokens(userWalletId, response) } @@ -561,8 +588,9 @@ internal class DefaultCurrenciesRepository( private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { val userWalletId = userWallet.walletId - val response = userTokensStore.getSyncOrNull(userWalletId) - ?: createDefaultUserTokensResponse(userWallet) + val response = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?: createDefaultUserTokensResponse(userWallet) if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId") @@ -622,4 +650,16 @@ internal class DefaultCurrenciesRepository( } private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" + + private fun getSavedUserTokensResponse(key: UserWalletId): Flow { + return appPreferencesStore + .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) + .filterNotNull() + } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index de6ad111ab..3762b4ddee 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -8,8 +8,11 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.network.NetworksStatusesStore -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.LceFlow @@ -33,7 +36,7 @@ internal class DefaultNetworksRepository( private val networksStatusesStore: NetworksStatusesStore, private val walletManagersFacade: WalletManagersFacade, private val userWalletsStore: UserWalletsStore, - private val userTokensStore: UserTokensStore, + private val appPreferencesStore: AppPreferencesStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : NetworksRepository { @@ -300,9 +303,14 @@ internal class DefaultNetworksRepository( } return if (userWallet.isMultiCurrency) { - val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val response = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() } else { From 2c29708284a5c2f4e9f82567a199182207d234d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 17:44:09 +0300 Subject: [PATCH 03/41] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../common/ui/userwallet/UserWalletItem.kt | 198 ++++++++++++ .../ui/userwallet/state/UserWalletItemUM.kt | 22 ++ core/res/src/main/res/values-de/strings.xml | 4 +- core/res/src/main/res/values-ja/strings.xml | 4 +- core/res/src/main/res/values-ru/strings.xml | 2 - .../src/main/res/values-uk-rUA/strings.xml | 4 +- core/ui/build.gradle.kts | 2 + .../core/ui/coil/RotationTransformation.kt | 22 ++ .../com/tangem/core/ui/components/Shimmers.kt | 9 +- .../block/information/InformationBlock.kt | 4 +- .../core/ui/components/buttons/SmallButton.kt | 23 +- .../core/ui/components/rows/ArrowRow.kt | 92 ++++-- .../core/ui/components/rows/BlockchainRow.kt | 7 +- .../tangem/core/ui/res/TangemThemePreview.kt | 4 + .../drawable/img_card_wallet_2_gray_22_36.xml | 13 + features/details/impl/build.gradle.kts | 1 + .../preview/PreviewUserWalletListComponent.kt | 7 +- .../details/entity/UserWalletListUM.kt | 17 +- .../details/model/UserWalletListModel.kt | 4 +- .../details/ui/UserWalletListBlock.kt | 106 +------ .../details/utils/UserWalletMappers.kt | 6 +- .../details/utils/UserWalletsFetcher.kt | 6 +- .../managetokens/ui/ManageTokensScreen.kt | 1 + features/markets/impl/build.gradle.kts | 2 + .../impl/model/MarketsTokenDetailsModel.kt | 2 + .../api/MarketsPortfolioComponent.kt | 17 + .../impl/DefaultMarketsPortfolioComponent.kt | 39 +++ .../portfolio/impl/di/ComponentModule.kt | 20 ++ .../markets/portfolio/impl/di/ModelModule.kt | 20 ++ .../impl/model/MarketsPortfolioModel.kt | 20 ++ .../impl/ui/AddToPortfolioBottomSheet.kt | 240 ++++++++++++++ .../markets/portfolio/impl/ui/MyPortfolio.kt | 174 +++++++++++ .../portfolio/impl/ui/PortfolioItem.kt | 292 ++++++++++++++++++ .../impl/ui/PortfolioQuickActions.kt | 214 +++++++++++++ .../impl/ui/TokenActionsBottomSheet.kt | 90 ++++++ .../PreviewAddToPortfolioBSContentProvider.kt | 60 ++++ .../preview/PreviewMyPortfolioUMProvider.kt | 50 +++ .../ui/state/AddToPortfolioBSContentUM.kt | 10 + .../portfolio/impl/ui/state/MyPortfolioUM.kt | 29 ++ .../impl/ui/state/PortfolioTokenUM.kt | 34 ++ .../portfolio/impl/ui/state/QuickActionUM.kt | 30 ++ .../impl/ui/state/SelectNetworkUM.kt | 13 + .../impl/ui/state/TokenActionsBSContent.kt | 28 ++ .../tangem/feature/swap/ui/StateBuilder.kt | 8 +- 45 files changed, 1776 insertions(+), 175 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt create mode 100644 core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index e57cd07bd1..40a779c416 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.coil) /** Deps */ implementation(deps.kotlin.immutable.collections) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt new file mode 100644 index 0000000000..baf96fd3cf --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -0,0 +1,198 @@ +package com.tangem.common.ui.userwallet + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.common.ui.R +import com.tangem.core.ui.coil.RotationTransformation +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun UserWalletItem(state: UserWalletItemUM, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + onClick = state.onClick, + enabled = state.isEnabled, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size68) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + CardImage(imageUrl = state.imageUrl) + NameAndInfo( + modifier = Modifier.weight(1f), + name = state.name, + information = state.information, + ) + + when (state.endIcon) { + UserWalletItemUM.EndIcon.None -> {} + UserWalletItemUM.EndIcon.Arrow -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + UserWalletItemUM.EndIcon.Checkmark -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) { + Column( + modifier = modifier.heightIn(min = TangemTheme.dimens.size40), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Text( + text = name.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + AnimatedContent( + targetState = information.resolveReference(), + label = "User wallet information", + ) { information -> + Text( + text = information, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) { + val imageModifier = modifier + .width(TangemTheme.dimens.size24) + .height(TangemTheme.dimens.size36) + .clip(TangemTheme.shapes.roundedCornersSmall) + + SubcomposeAsyncImage( + modifier = imageModifier, + model = ImageRequest.Builder(LocalContext.current) + .transformations(RotationTransformation(angle = 90f)) + .size( + width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, + height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, + ) + .data(imageUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { + RectangleShimmer( + modifier = imageModifier, + radius = TangemTheme.dimens.size2, + ) + }, + error = { + Image( + modifier = imageModifier, + imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36), + contentDescription = null, + ) + }, + contentDescription = null, + ) +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + val list = persistentListOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_1".encodeToByteArray()), + name = stringReference("My Wallet"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_2".encodeToByteArray()), + name = stringReference("Old wallet"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = true, + onClick = {}, + endIcon = UserWalletItemUM.EndIcon.Arrow, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = false, + endIcon = UserWalletItemUM.EndIcon.Checkmark, + onClick = {}, + ), + ) + + Column { + list.fastForEach { userWalletItemUM -> + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + state = userWalletItemUM, + ) + } + } + } +} + +private fun getInformation(cardCount: Int, totalBalance: String): TextReference { + val t1 = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + val divider = stringReference(value = " • ") + val t2 = stringReference(totalBalance) + + return TextReference.Combined(wrappedList(t1, divider, t2)) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt new file mode 100644 index 0000000000..f8e361a6f2 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -0,0 +1,22 @@ +package com.tangem.common.ui.userwallet.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.wallets.models.UserWalletId +import javax.annotation.concurrent.Immutable + +@Immutable +data class UserWalletItemUM( + val id: UserWalletId, + val name: TextReference, + val information: TextReference, + val imageUrl: String, + val isEnabled: Boolean, + val endIcon: EndIcon = EndIcon.None, + val onClick: () -> Unit, +) { + enum class EndIcon { + None, + Arrow, + Checkmark, + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 1e9ed93914..21258cdee1 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -835,6 +835,8 @@ Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. + Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. + Genehmigung in Arbeit Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. @@ -852,8 +854,6 @@ Netzwerk erfordert eine Mindesteinzahlung Der Swap wird nach Abschluss der Transaktion %s verfügbar sein. Du hast aktive Transaktion - Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. - Genehmigung in Arbeit Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt. Du hast keine %s Coins in deiner Liste Keine Token zum Tauschen verfügbar diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e5c43474a1..046d0156bc 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -823,6 +823,8 @@ ウォレット設定 Tangem %sを使用するか、カードをスキャンしてウォレットにアクセスしてください + スワップの承認は現在進行中で、まもなく完了する予定です。 + 承認が進行中 カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 @@ -840,8 +842,6 @@ ネットワークには最低残高が必要です スワップは、%s の取引完了後に利用可能となります。 アクティブな取引があります - スワップの承認は現在進行中で、まもなく完了する予定です。 - 承認が進行中 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 あなたのリストには、交換可能な %s トークンがありません。 スワップ可能なトークンがありません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e89bb61d95..fe433aaed9 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -859,8 +859,6 @@ Для работы с сетью необходим депозит Обмен будет доступен после завершения %s транзакции У вас есть активная транзакция - Разрешение обмена в процессе и будет скоро завершено - Разрешение в процессе Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. У вас в списке нет монет доступных для обмена с %s Нет доступных для обмена токенов diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index d352a7be3c..62d3baa6e6 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -844,6 +844,8 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця + Затвердження обміну триває і незабаром буде завершено + Затвердження в процесі Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. @@ -861,8 +863,6 @@ Для роботи з мережею вимагається депозит Обмін буде доступний після завершення %s транзакції У вас є активна транзакція - Затвердження обміну триває і незабаром буде завершено - Затвердження в процесі Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s. У вашому списку немає доступних монет для обміну %s Немає доступних токенів для обміну diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index cb8f7f15ee..4758911a25 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.ide.common.resources.generateLocaleList + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) diff --git a/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt new file mode 100644 index 0000000000..6a8019af1e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt @@ -0,0 +1,22 @@ +package com.tangem.core.ui.coil + +import android.graphics.Bitmap +import android.graphics.Matrix +import coil.size.Size +import coil.transform.Transformation + +class RotationTransformation(private val angle: Float) : Transformation { + + override val cacheKey: String = "rotate:$angle" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val matrix = Matrix().apply { + val centerX = input.width / 2f + val centerY = input.height / 2f + + postRotate(angle, centerX, centerY) + } + + return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 5d09690136..c0f97514dc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -101,7 +102,11 @@ fun TextShimmer( * Height and min width will be set automatically */ @Composable -fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) { +fun SmallButtonShimmer( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), + withIcon: Boolean = false, +) { PrimarySmallButton( config = SmallButtonConfig( text = stringReference("B"), @@ -113,7 +118,7 @@ fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) }, ), modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .clip(shape) .shimmer(LocalTangemShimmer.current), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 900aafe401..fb2230889c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -29,6 +30,7 @@ class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope fun InformationBlock( title: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, + contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12, action: (@Composable BoxScope.() -> Unit)? = null, content: (@Composable InformationBlockContentScope.() -> Unit)? = null, ) { @@ -72,7 +74,7 @@ fun InformationBlock( if (content != null) { Box( modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing12) + .padding(horizontal = contentHorizontalPadding) .fillMaxWidth(), ) { val scope = InformationBlockContentScope(scope = this) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt index e02ae97109..195fae0923 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -33,6 +33,7 @@ data class SmallButtonConfig( val text: TextReference, val onClick: () -> Unit, val icon: TangemButtonIconPosition = TangemButtonIconPosition.None, + val enabled: Boolean = true, ) /** @@ -57,6 +58,7 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie SmallButton(config = config, isPrimary = false, modifier = modifier) } +@Suppress("LongMethod") @Composable private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) { val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16) @@ -77,7 +79,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: color = backgroundColor, shape = shape, ) - .clickable(enabled = true, onClick = config.onClick) + .clickable(enabled = config.enabled, onClick = config.onClick) .padding( paddingValues = when (config.icon) { is TangemButtonIconPosition.None -> PaddingValues( @@ -100,7 +102,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: iconPosition = config.icon, text = { val textColor by animateColorAsState( - targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, + targetValue = when { + !config.enabled -> TangemTheme.colors.text.disabled + isPrimary -> TangemTheme.colors.text.primary2 + else -> TangemTheme.colors.text.primary1 + }, label = "Update text color", ) @@ -116,7 +122,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Icon( modifier = Modifier.size(TangemTheme.dimens.size16), painter = painterResource(id = iconResId), - tint = TangemTheme.colors.icon.secondary, + tint = if (config.enabled) { + TangemTheme.colors.icon.secondary + } else { + TangemTheme.colors.icon.inactive + }, contentDescription = null, ) }, @@ -174,5 +184,12 @@ private fun ButtonsSample() { icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), ), ) + SecondarySmallButton( + config = config.copy( + text = TextReference.Str(value = "Add token"), + icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), + enabled = false, + ), + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt index 0e594fbd91..67ef1ac47d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.* @@ -68,6 +69,7 @@ private class ChildArrowScope( @Composable fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr val figureWidth = TangemTheme.dimens.size40 val strokeColor = TangemTheme.colors.stroke.secondary @@ -86,18 +88,31 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { ) val arrowHeadRectDp = DpRect( origin = DpOffset( - x = figureWidth - arrowHeadSize.width, + x = if (isLtr) { + figureWidth - arrowHeadSize.width + } else { + 0.dp + }, y = figureRectDp.size.center.y - arrowHeadSize.center.y, ), size = arrowHeadSize, ) - val curvedArrowRectDp = DpRect( - top = figureRectDp.top, - left = TangemTheme.dimens.size18, - right = figureRectDp.right - arrowHeadRectDp.width, - bottom = figureRectDp.size.center.y, - ) + val curvedArrowRectDp = if (isLtr) { + DpRect( + top = figureRectDp.top, + left = TangemTheme.dimens.size18, + right = figureRectDp.right - arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + } else { + DpRect( + top = figureRectDp.top, + left = arrowHeadRectDp.width, + right = TangemTheme.dimens.size18 + arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + } Canvas( modifier = Modifier @@ -114,20 +129,26 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { drawScope = this, ) - scope.drawCurveArrow() - scope.drawArrowHead() + scope.drawCurveArrow(isLtr) + scope.drawArrowHead(isLtr) if (!isLastChild) { - scope.drawArrowLine() + scope.drawArrowLine(isLtr) } } } -private fun ChildArrowScope.drawArrowHead() { +private fun ChildArrowScope.drawArrowHead(isLtr: Boolean) { val arrowHeadPath = Path().apply { - moveTo(arrowHeadRect.centerRight) - lineTo(arrowHeadRect.topLeft) - lineTo(arrowHeadRect.bottomLeft) + if (isLtr) { + moveTo(arrowHeadRect.centerRight) + lineTo(arrowHeadRect.topLeft) + lineTo(arrowHeadRect.bottomLeft) + } else { + moveTo(arrowHeadRect.centerLeft) + lineTo(arrowHeadRect.topRight) + lineTo(arrowHeadRect.bottomRight) + } close() } val paint = Paint().apply { @@ -143,13 +164,21 @@ private fun ChildArrowScope.drawArrowHead() { } } -private fun ChildArrowScope.drawCurveArrow() { +private fun ChildArrowScope.drawCurveArrow(isLtr: Boolean) { val curveArrowPath = Path().apply { - moveTo(curvedArrowRect.topLeft) - quadraticBezierTo( - control = curvedArrowRect.bottomLeft, - end = curvedArrowRect.bottomRight, - ) + if (isLtr) { + moveTo(curvedArrowRect.topLeft) + quadraticBezierTo( + control = curvedArrowRect.bottomLeft, + end = curvedArrowRect.bottomRight, + ) + } else { + moveTo(curvedArrowRect.topRight) + quadraticBezierTo( + control = curvedArrowRect.bottomRight, + end = curvedArrowRect.bottomLeft, + ) + } } drawPath( path = curveArrowPath, @@ -158,11 +187,20 @@ private fun ChildArrowScope.drawCurveArrow() { ) } -private fun ChildArrowScope.drawArrowLine() { - drawLine( - color = strokeColor, - start = curvedArrowRect.topLeft, - end = Offset(curvedArrowRect.left, figureRect.bottom), - strokeWidth = arrowStrokeWidth, - ) +private fun ChildArrowScope.drawArrowLine(isLtr: Boolean) { + if (isLtr) { + drawLine( + color = strokeColor, + start = curvedArrowRect.topLeft, + end = Offset(curvedArrowRect.left, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) + } else { + drawLine( + color = strokeColor, + start = curvedArrowRect.topRight, + end = Offset(curvedArrowRect.right, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index 4f8edf75c4..035be68d2e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -29,8 +29,9 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni modifier = modifier .heightIn(min = TangemTheme.dimens.size52) .padding( - vertical = TangemTheme.dimens.spacing8, - horizontal = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing8, ), icon = { RowIcon( @@ -125,7 +126,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid BlockchainRow( model = state, action = { - TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true) + TangemSwitch(onCheckedChange = { }, checked = true) }, ) }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index 0347ca4893..dd15fd2212 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection import com.tangem.core.ui.windowsize.rememberWindowSizePreview @Composable @@ -14,12 +16,14 @@ fun TangemThemePreview( typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, alwaysShowBottomSheets: Boolean = true, + rtl: Boolean = false, content: @Composable () -> Unit, ) { val isDarkTheme = isDark ?: isSystemInDarkTheme() CompositionLocalProvider( LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets, + LocalLayoutDirection provides if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr, ) { BoxWithConstraints { TangemTheme( diff --git a/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml b/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml new file mode 100644 index 0000000000..977d693e60 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 82ca657a96..ff405d0d36 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics.models) implementation(projects.common.routing) + implementation(projects.common.ui) /* Project - Domain */ implementation(projects.domain.models) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 92b6f46de3..e2a4774961 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -17,7 +18,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { private val previewState = UserWalletListUM( userWallets = persistentListOf( - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_1".encodeToByteArray()), name = stringReference("My Wallet"), information = getInformation(3, "4 496,75 $"), @@ -25,7 +26,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { isEnabled = true, onClick = {}, ), - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_2".encodeToByteArray()), name = stringReference("Old wallet"), information = getInformation(3, "4 496,75 $"), @@ -33,7 +34,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { isEnabled = true, onClick = {}, ), - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_3".encodeToByteArray()), name = stringReference("Multi Card"), information = getInformation(3, "4 496,75 $"), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index 1569f48efb..a8ef5eb141 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -1,25 +1,14 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.ImmutableList @Immutable internal data class UserWalletListUM( - val userWallets: ImmutableList, + val userWallets: ImmutableList, val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, -) { - - @Immutable - data class UserWalletUM( - val id: UserWalletId, - val name: TextReference, - val information: TextReference, - val imageUrl: String, - val isEnabled: Boolean, - val onClick: () -> Unit, - ) -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 12f18cc1f4..a36e126509 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -1,12 +1,12 @@ package com.tangem.features.details.model +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.details.utils.UserWalletsFetcher @@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor( } private fun updateState( - userWallets: ImmutableList, + userWallets: ImmutableList, shouldSaveUserWallets: Boolean, isWalletSavingInProgress: Boolean, ) = state.update { value -> diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index 794ef632c3..b09124ca04 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -1,7 +1,6 @@ package com.tangem.features.details.ui import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -10,32 +9,25 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R -import com.tangem.features.details.ui.coil.RotationTransformation @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { BlockCard( modifier = modifier, ) { - state.userWallets.forEach { model -> - key(model.id) { + state.userWallets.forEach { state -> + key(state.id) { UserWalletItem( modifier = Modifier.fillMaxWidth(), - model = model, + state = state, ) } } @@ -47,96 +39,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M } } -@Composable -private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier, - onClick = model.onClick, - enabled = model.isEnabled, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size68) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Image(imageUrl = model.imageUrl) - NameAndInfo( - name = model.name, - information = model.information, - ) - } - } -} - -@Composable -private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) { - Column( - modifier = modifier.heightIn(min = TangemTheme.dimens.size40), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceEvenly, - ) { - Text( - text = name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - AnimatedContent( - targetState = information.resolveReference(), - label = "User wallet information", - ) { information -> - Text( - text = information, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -private fun Image(imageUrl: String, modifier: Modifier = Modifier) { - val imageModifier = modifier - .width(TangemTheme.dimens.size24) - .height(TangemTheme.dimens.size36) - .clip(TangemTheme.shapes.roundedCornersSmall) - - SubcomposeAsyncImage( - modifier = imageModifier, - model = ImageRequest.Builder(LocalContext.current) - .transformations(RotationTransformation(angle = 90f)) - .size( - width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, - height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, - ) - .data(imageUrl) - .crossfade(enable = true) - .allowHardware(enable = false) - .build(), - loading = { - RectangleShimmer( - modifier = imageModifier, - radius = TangemTheme.dimens.size2, - ) - }, - error = { - Image( - modifier = imageModifier, - painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36), - contentDescription = null, - ) - }, - contentDescription = null, - ) -} - @Composable private fun AddWalletButton( text: TextReference, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index a9782fc1d9..1a3efa0cf4 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -1,5 +1,6 @@ package com.tangem.features.details.utils +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -7,7 +8,6 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.ImmutableList @@ -19,7 +19,7 @@ internal fun List.toUiModels( balances: Map = emptyMap(), isLoading: Boolean = true, isBalancesHidden: Boolean = false, -): ImmutableList = this.map { model -> +): ImmutableList = this.map { model -> val balance = balances[model.walletId] model.toUiModel( @@ -37,7 +37,7 @@ private fun UserWallet.toUiModel( isLoading: Boolean, isBalanceHidden: Boolean, onClick: () -> Unit, -): UserWalletUM = UserWalletUM( +): UserWalletItemUM = UserWalletItemUM( id = walletId, name = stringReference(name), information = getInfo( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index e141c4034e..cd5013c005 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.utils import arrow.core.Either import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender @@ -22,7 +23,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -40,7 +40,7 @@ internal class UserWalletsFetcher @Inject constructor( ) { @OptIn(ExperimentalCoroutinesApi::class) - val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> + val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) combine( @@ -72,7 +72,7 @@ internal class UserWalletsFetcher @Inject constructor( maybeAppCurrency: Either, maybeBalances: Lce>, balanceHidingSettings: BalanceHidingSettings, - ): Lce> = lce { + ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, block = { maybeBalances.bindOrNull().orEmpty() }, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 9f9584fac9..3008c84d30 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -286,6 +286,7 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod isLastItem = index == currentItems.lastIndex, content = { BlockchainRow( + modifier = Modifier.padding(end = TangemTheme.dimens.spacing8), model = with(network) { BlockchainRowUM( name = name, diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 7d8b7d0667..2e1f37fd3e 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.domain.markets) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.wallets.models) /* Compose */ implementation(deps.compose.coil) @@ -46,6 +47,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) + /* Common */ implementation(projects.common.ui) implementation(projects.common.uiCharts) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 2a40b0c83e..0f2d2d1465 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.ui.charts.state.* +import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener @@ -43,6 +44,7 @@ import javax.inject.Inject @Suppress("LargeClass", "LongParameterList") @Stable +@ComponentScoped internal class MarketsTokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt new file mode 100644 index 0000000000..c4217765dd --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.markets.portfolio.api + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import kotlinx.serialization.Serializable + +@Stable +interface MarketsPortfolioComponent : ComposableContentComponent { + + @Serializable + data class Params( + val tokenId: String, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt new file mode 100644 index 0000000000..9b732e6e42 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.markets.portfolio.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel +import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: MarketsPortfolioComponent.Params, +) : AppComponentContext by context, MarketsPortfolioComponent { + + private val model: MarketsPortfolioModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + MyPortfolio( + modifier = modifier, + state = MyPortfolioUM.Loading, + ) + } + + @AssistedFactory + interface Factory : MarketsPortfolioComponent.Factory { + override fun create( + context: AppComponentContext, + params: MarketsPortfolioComponent.Params, + ): DefaultMarketsPortfolioComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..d011fbf799 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.di + +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindMarketsPortfolioComponent( + factory: DefaultMarketsPortfolioComponent.Factory, + ): MarketsPortfolioComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt new file mode 100644 index 0000000000..38ea8f8688 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(MarketsPortfolioModel::class) + fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt new file mode 100644 index 0000000000..75baa3fd50 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class MarketsPortfolioModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params = paramsContainer.require() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt new file mode 100644 index 0000000000..8a5a2ae45f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -0,0 +1,240 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerW6 +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.components.rows.ArrowRow +import com.tangem.core.ui.components.rows.BlockchainRow +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM + +@Composable +internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + titleText = resourceReference(R.string.markets_add_to_portfolio_button), + ) { + Content( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = TangemTheme.dimens.spacing16), + state = config.content as AddToPortfolioBSContentUM, + ) + } +} + +@Composable +private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + UserWalletItem(state.selectedWallet) + + NetworkSelection( + modifier = Modifier.fillMaxWidth(), + state = state.selectNetworkUM, + ) + + AnimatedVisibility( + visible = state.isScanCardNotificationVisible, + modifier = Modifier.fillMaxWidth(), + ) { + ScanWalletWarning(modifier = Modifier.fillMaxWidth()) + } + + PrimaryButtonIconEnd( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.common_continue), + iconResId = R.drawable.ic_tangem_24, + onClick = {}, + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.markets_select_network), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing14), + verticalAlignment = Alignment.CenterVertically, + ) { + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + url = state.iconUrl, + alpha = 1f, + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + SpacerW12() + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .alignByBaseline(), + text = state.tokenName, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + SpacerW6() + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .alignByBaseline(), + text = state.tokenCurrencySymbol, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } + + state.networks.fastForEachIndexed { index, network -> + ArrowRow( + isLastItem = index == state.networks.lastIndex, + content = { + BlockchainRow( + modifier = Modifier.padding( + end = TangemTheme.dimens.spacing4, + ), + model = with(network) { + BlockchainRowUM( + name = name, + type = type, + iconResId = iconResId, + isMainNetwork = isMainNetwork, + isSelected = isSelected, + ) + }, + action = { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = { + state.onNetworkSwitchClick(network, it) + }, + ) + }, + ) + }, + ) + } + } + } +} + +@Composable +private fun ScanWalletWarning(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background( + color = TangemTheme.colors.button.disabled, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), + ) { + Icon( + modifier = Modifier.requiredSize(TangemTheme.dimens.size20), + imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + contentDescription = null, + ) + Text( + text = stringResource(R.string.markets_generate_addresses_notification), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview { + AddToPortfolioBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + content = content, + onDismissRequest = {}, + ), + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun PreviewContent( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview { + Content( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth(), + state = content, + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun PreviewContentRtl( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview(rtl = true) { + Content( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth(), + state = content, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt new file mode 100644 index 0000000000..30cbcb453f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -0,0 +1,174 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM + +@Composable +internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + contentHorizontalPadding = 0.dp, + title = { + Text( + text = stringResource(R.string.markets_common_my_portfolio), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + action = { + if (state !is MyPortfolioUM.Tokens) return@InformationBlock + + when (state.buttonState) { + MyPortfolioUM.Tokens.AddButtonState.Loading -> { + SmallButtonShimmer( + modifier = Modifier.size(width = 63.dp, height = TangemTheme.dimens.size18), + shape = RoundedCornerShape(TangemTheme.dimens.radius3), + ) + } + else -> { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.markets_add_token), + icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), + onClick = state.onAddClick, + enabled = state.buttonState == MyPortfolioUM.Tokens.AddButtonState.Available, + ), + ) + } + } + }, + ) { + when (state) { + is MyPortfolioUM.Tokens -> TokenList(state = state) + is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state) + MyPortfolioUM.Loading -> LoadingPlaceholder() + MyPortfolioUM.Unavailable -> UnavailableContent() + } + } +} + +@Composable +private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) { + Column(modifier) { + state.tokens.fastForEachIndexed { index, token -> + PortfolioItem( + state = token, + lastInList = index == state.tokens.size - 1, + ) + } + } +} + +@Composable +private fun UnavailableContent(modifier: Modifier = Modifier) { + Text( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Text( + text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.markets_add_to_portfolio_button), + onClick = state.onAddClick, + ) + } +} + +@Composable +private fun LoadingPlaceholder(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.7f), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing8), + ) { + MyPortfolio(state) + } + } +} + +@Preview +@Composable +private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { + TangemThemePreview(rtl = true) { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing8), + ) { + MyPortfolio(state) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt new file mode 100644 index 0000000000..3a98293707 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -0,0 +1,292 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.utils.StringsSigns + +// TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement) +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { + val hapticManager = LocalHapticManager.current + + Column(modifier) { + Row( + modifier = Modifier + .combinedClickable( + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + state.onClick() + }, + onLongClick = { + hapticManager.perform(TangemHapticEffect.View.LongPress) + state.onLongTap() + }, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .padding( + vertical = TangemTheme.dimens.spacing15, + horizontal = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Content(state) + } + + PortfolioQuickActions( + modifier = Modifier.padding( + bottom = if (lastInList) { + TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing24 + }, + ), + isVisible = state.isQuickActionsShown, + onActionClick = state.onQuickActionClick, + ) + } +} + +@Composable +private fun RowScope.Content(state: PortfolioTokenUM) { + // TODO add custom token + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + url = state.iconUrl, + alpha = 1f, // TODO add disabled state + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + + Column( + modifier = Modifier.align(Alignment.CenterVertically), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + when (state.balanceContent) { + is PortfolioTokenUM.BalanceContent.Disabled -> { + Disabled( + state = state, + disabledText = state.balanceContent.text.resolveReference(), + ) + } + PortfolioTokenUM.BalanceContent.Loading -> { + Loading(state = state) + } + is PortfolioTokenUM.BalanceContent.TokenBalance -> { + TokenBalance( + state = state, + content = state.balanceContent, + ) + } + } + } +} + +@Composable +private fun ColumnScope.TokenBalance(state: PortfolioTokenUM, content: PortfolioTokenUM.BalanceContent.TokenBalance) { + val balance = if (content.hidden) { + StringsSigns.STARS + } else { + content.balance + } + val tokenAmount = if (content.hidden) { + StringsSigns.STARS + } else { + content.tokenAmount + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + modifier = Modifier.alignByBaseline(), + text = state.title, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.alignByBaseline(), + text = balance, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = state.subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = tokenAmount, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun ColumnScope.Loading(state: PortfolioTokenUM) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + modifier = Modifier.alignByBaseline(), + text = state.title, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size40) + .alignByBaseline(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = state.subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size40) + .alignByBaseline(), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } +} + +@Composable +private fun Disabled(state: PortfolioTokenUM, disabledText: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + Modifier.weight(1f), + ) { + Text( + text = state.title, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + + Text( + text = disabledText, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + var quickActionsShown by remember { mutableStateOf(false) } + var quickActionsShown2 by remember { mutableStateOf(false) } + val sampleToken = PreviewMyPortfolioUMProvider().sampleToken + + Box( + Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + Column { + PortfolioItem( + state = sampleToken + .copy( + onClick = { + if (quickActionsShown2) { + quickActionsShown2 = false + } + quickActionsShown = quickActionsShown.not() + }, + isQuickActionsShown = quickActionsShown, + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy( + onClick = { + if (quickActionsShown) { + quickActionsShown = false + } + quickActionsShown2 = quickActionsShown2.not() + }, + isQuickActionsShown = quickActionsShown2, + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy( + balanceContent = ( + sampleToken.balanceContent + as PortfolioTokenUM.BalanceContent.TokenBalance + ) + .copy(hidden = true), + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy( + balanceContent = PortfolioTokenUM.BalanceContent.Disabled( + stringReference("No Address"), + ), + ), + lastInList = true, + ) + PortfolioItem( + state = sampleToken + .copy(balanceContent = PortfolioTokenUM.BalanceContent.Loading), + lastInList = true, + ) + } + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt new file mode 100644 index 0000000000..1af8cde8ce --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt @@ -0,0 +1,214 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM + +@Composable +internal fun PortfolioQuickActions( + isVisible: Boolean, + onActionClick: (QuickActionUM) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isVisible, + enter = expandVertically(expandFrom = Alignment.Top), + exit = shrinkVertically(shrinkTowards = Alignment.Top), + ) { + Column(modifier = modifier) { + LineSeparator() + QuickActionItem( + state = QuickActionUM.Buy, + onClick = { onActionClick(QuickActionUM.Buy) }, + ) + LineSeparator() + QuickActionItem( + state = QuickActionUM.Exchange, + onClick = { onActionClick(QuickActionUM.Exchange) }, + ) + LineSeparator() + QuickActionItem( + state = QuickActionUM.Receive, + onClick = { onActionClick(QuickActionUM.Receive) }, + ) + } + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) { + val lineColor = TangemTheme.colors.stroke.primary + val strokeWidth = TangemTheme.dimens.size1 + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val verticalPadding = TangemTheme.dimens.spacing2 + val startPadding = TangemTheme.dimens.spacing28 + + val height = TangemTheme.dimens.size16 + verticalPadding * 2 + + Canvas( + modifier = modifier + .animateEnterExit( + enter = expandVertically( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + ), + expandFrom = Alignment.Top, + ) + fadeIn(), + exit = shrinkVertically( + spring( + stiffness = Spring.StiffnessLow, + ), + shrinkTowards = Alignment.Top, + ) + fadeOut(), + ) + .fillMaxWidth() + .height(height), + ) { + val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx() + + drawLine( + color = lineColor, + start = Offset(x, verticalPadding.toPx()), + end = Offset(x, size.height - verticalPadding.toPx()), + strokeWidth = strokeWidth.toPx(), + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AnimatedVisibilityScope.QuickActionItem( + state: QuickActionUM, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticManager = LocalHapticManager.current + + Row( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersMedium) + .clickable { + hapticManager.perform(TangemHapticEffect.View.SegmentTick) + onClick() + } + .padding( + vertical = TangemTheme.dimens.spacing2, + horizontal = TangemTheme.dimens.spacing12, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), + ) { + Box( + Modifier + .animateEnterExit( + enter = scaleIn(), + exit = scaleOut(), + ) + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ) + .size(TangemTheme.dimens.size32), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(TangemTheme.dimens.size16), + imageVector = ImageVector.vectorResource(id = state.icon), + contentDescription = null, + tint = TangemTheme.colors.button.primary, + ) + } + Column( + modifier = Modifier + .animateEnterExit( + enter = fadeIn(), + exit = fadeOut(), + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.description.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + var isVisible by remember { mutableStateOf(true) } + + Column( + modifier = Modifier + .fillMaxWidth() + .height(680.dp), + ) { + Button( + onClick = { isVisible = !isVisible }, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) { + Text(text = "Toggle") + } + SpacerH4() + Box( + modifier = Modifier.background(color = TangemTheme.colors.background.action), + ) { + PortfolioQuickActions( + isVisible = isVisible, + onActionClick = {}, + ) + } + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewRtl() { + TangemThemePreview(rtl = true) { + Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { + PortfolioQuickActions( + isVisible = true, + onActionClick = {}, + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt new file mode 100644 index 0000000000..cfc6c814d1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -0,0 +1,90 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.inputrow.InputRowChecked +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContent +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + title = { content -> + TangemBottomSheetTitle(content.title) + }, + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: TokenActionsBSContent) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + content.actions.forEachIndexed { index, action -> + val cornersToRound = when (index) { + 0 -> CornersToRound.TOP_2 + content.actions.lastIndex -> CornersToRound.BOTTOM_2 + else -> CornersToRound.ZERO + } + + DividerContainer( + modifier = Modifier + .clip(cornersToRound.getShape()) + .background(TangemTheme.colors.background.action) + .clickable { content.onActionClick(action) }, + showDivider = index != content.actions.lastIndex, + ) { + InputRowChecked( + text = action.text, + checked = false, + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 640) +@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview( + alwaysShowBottomSheets = true, + ) { + Box(Modifier.background(TangemTheme.colors.background.secondary)) { + TokenActionsBottomSheet( + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = TokenActionsBSContent( + title = "Wallet 1", + actions = TokenActionsBSContent.Action.entries.toImmutableList(), + onActionClick = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt new file mode 100644 index 0000000000..2b33545fd3 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -0,0 +1,60 @@ +package com.tangem.features.markets.portfolio.impl.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + AddToPortfolioBSContentUM( + selectedWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ), + selectNetworkUM = SelectNetworkUM( + tokenId = "etherium", + tokenName = "Etherium", + tokenCurrencySymbol = "ETH", + networks = persistentListOf( + BlockchainRowUM( + name = "Etherium", + type = "MAIN", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = true, + isSelected = true, + ), + BlockchainRowUM( + name = "Etherium 2", + type = "TEST", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = false, + ), + BlockchainRowUM( + name = "Etherium 3", + type = "TEST", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = false, + ), + ), + onNetworkSwitchClick = { _, _ -> }, + iconUrl = null, + ), + isScanCardNotificationVisible = true, + ), + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt new file mode 100644 index 0000000000..b607386001 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -0,0 +1,50 @@ +package com.tangem.features.markets.portfolio.impl.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken, sampleToken), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Available, + onAddClick = {}, + ), + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable, + onAddClick = {}, + ), + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading, + onAddClick = {}, + ), + MyPortfolioUM.AddFirstToken( + onAddClick = {}, + ), + MyPortfolioUM.Loading, + MyPortfolioUM.Unavailable, + ) + + val sampleToken = PortfolioTokenUM( + id = "", + networkId = "", + iconUrl = "", + balanceContent = PortfolioTokenUM.BalanceContent.TokenBalance( + balance = "486,65 \$", + tokenAmount = "733,71097 MATIC", + hidden = false, + ), + title = "My wallet", + subtitle = "XRP Ledger token", + onClick = {}, + onLongTap = {}, + isQuickActionsShown = false, + onQuickActionClick = {}, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt new file mode 100644 index 0000000000..ad733c39cf --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class AddToPortfolioBSContentUM( + val selectedWallet: UserWalletItemUM, + val selectNetworkUM: SelectNetworkUM, + val isScanCardNotificationVisible: Boolean, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt new file mode 100644 index 0000000000..7fa4c11a4a --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class MyPortfolioUM { + + data class Tokens( + val tokens: ImmutableList, + val buttonState: AddButtonState, + val onAddClick: () -> Unit, + ) : MyPortfolioUM() { + + enum class AddButtonState { + Loading, + Available, + Unavailable, + } + } + + data class AddFirstToken( + val onAddClick: () -> Unit, + ) : MyPortfolioUM() + + data object Loading : MyPortfolioUM() + + data object Unavailable : MyPortfolioUM() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt new file mode 100644 index 0000000000..f7db3362b2 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt @@ -0,0 +1,34 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +internal data class PortfolioTokenUM( + val id: String, + val networkId: String, + val iconUrl: String, + val title: String, + val subtitle: String, + val balanceContent: BalanceContent, + val onClick: () -> Unit, + val onLongTap: () -> Unit, + val isQuickActionsShown: Boolean, + val onQuickActionClick: (QuickActionUM) -> Unit, +) { + + // TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement) + @Immutable + sealed class BalanceContent { + data class TokenBalance( // TODO Add stacking ([REDACTED_TASK_KEY] [Markets] Add staking info to portfolio token item) + val balance: String, + val tokenAmount: String, + val hidden: Boolean, + ) : BalanceContent() + + data class Disabled( + val text: TextReference, + ) : BalanceContent() + + data object Loading : BalanceContent() + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt new file mode 100644 index 0000000000..b079b5ce50 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -0,0 +1,30 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R + +@Immutable +internal enum class QuickActionUM( + val title: TextReference, + val description: TextReference, + @DrawableRes val icon: Int, +) { + Buy( + title = resourceReference(R.string.common_buy), + description = resourceReference(R.string.buy_token_description), + icon = R.drawable.ic_plus_24, + ), + Exchange( + title = resourceReference(R.string.common_exchange), + description = resourceReference(R.string.exсhange_token_description), + icon = R.drawable.ic_exchange_vertical_24, + ), + Receive( + title = resourceReference(R.string.common_receive), + description = resourceReference(R.string.receive_token_description), + icon = R.drawable.ic_arrow_down_24, + ), +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt new file mode 100644 index 0000000000..90830679ca --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt @@ -0,0 +1,13 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import kotlinx.collections.immutable.ImmutableList + +internal data class SelectNetworkUM( + val tokenId: String, + val iconUrl: String?, + val tokenName: String, + val tokenCurrencySymbol: String, + val networks: ImmutableList, + val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt new file mode 100644 index 0000000000..2252b38f70 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt @@ -0,0 +1,28 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.ImmutableList + +internal data class TokenActionsBSContent( + val title: String, + val actions: ImmutableList, + val onActionClick: (Action) -> Unit, +) : TangemBottomSheetConfigContent { + + @Immutable + enum class Action( + val text: TextReference, + ) { + CopyAddress(text = resourceReference(R.string.common_copy_address)), + Receive(text = resourceReference(R.string.common_receive)), + Sell(text = resourceReference(R.string.common_sell)), + Buy(text = resourceReference(R.string.common_buy)), + Send(text = resourceReference(R.string.common_send)), + Exchange(text = resourceReference(R.string.common_exchange)), + Stake(text = resourceReference(R.string.common_stake)), + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 517c795db6..3d80ba6721 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -363,8 +363,8 @@ internal class StateBuilder( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), + title = stringReference("//TODO"), + description = stringReference("//TODO"), ), ) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { @@ -1009,8 +1009,8 @@ internal class StateBuilder( warnings.add( 0, SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), + title = stringReference("//TODO"), + description = stringReference("//TODO"), ), ) return uiState.copy( From e6416ab4c6003af89a657d9ebb3d8fe7b4870305 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 20:03:55 +0300 Subject: [PATCH 04/41] Updated on 2026-08-14 --- .../DefaultMarketsTokenDetailsComponent.kt | 26 +++++++++++++++++++ .../impl/model/MarketsTokenDetailsModel.kt | 10 +++++++ .../impl/model/state/TokenNetworksState.kt | 12 +++++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 5 ++++ .../ui/components/TokenMarketDetailsBody.kt | 25 +++++++++++++----- .../api/MarketsPortfolioComponent.kt | 9 ++++--- .../impl/DefaultMarketsPortfolioComponent.kt | 4 +++ .../impl/model/MarketsPortfolioModel.kt | 10 +++++++ 8 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index 402356487a..74451ad79c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -6,24 +6,47 @@ import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel +import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch @Stable internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: MarketsTokenDetailsComponent.Params, @Assisted private val onBack: () -> Unit, + portfolioComponentFactory: MarketsPortfolioComponent.Factory, ) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { private val model: MarketsTokenDetailsModel = getOrCreateModel(params) + private val portfolioComponent = portfolioComponentFactory.create( + context = child("my_portfolio"), + params = MarketsPortfolioComponent.Params(params.token.id), + ) + + init { + componentScope.launch { + model.networksState.collectLatest { + when (it) { + is TokenNetworksState.NetworksAvailable -> portfolioComponent.setTokenNetworks(it.networks) + TokenNetworksState.NoNetworksAvailable -> portfolioComponent.setNoNetworksAvailable() + else -> {} + } + } + } + } + @Composable override fun BottomSheetContent( bottomSheetState: State, @@ -48,6 +71,9 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( state = state, onBackClick = onBack, onHeaderSizeChange = onHeaderSizeChange, + portfolioBlock = { modifier -> + portfolioComponent.Content(modifier) + }, modifier = modifier, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 0f2d2d1465..0e731a5227 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -26,6 +26,7 @@ import com.tangem.features.markets.details.impl.model.formatter.* import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval +import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R @@ -119,6 +120,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) val isVisibleOnScreen = MutableStateFlow(false) + val networksState = MutableStateFlow(TokenNetworksState.Loading) val state = MutableStateFlow( MarketsTokenDetailsUM( @@ -294,6 +296,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } + val networks = result.networks + + networksState.value = if (networks.isNullOrEmpty()) { + TokenNetworksState.NoNetworksAvailable + } else { + TokenNetworksState.NetworksAvailable(networks) + } + chartDataProducer.runTransaction { updateLook { it.copy( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt new file mode 100644 index 0000000000..dbe01ddcdc --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets.details.impl.model.state + +import com.tangem.domain.markets.TokenMarketInfo + +internal sealed class TokenNetworksState { + + data object Loading : TokenNetworksState() + + data object NoNetworksAvailable : TokenNetworksState() + + data class NetworksAvailable(val networks: List) : TokenNetworksState() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index ecfc715abd..001c81cf27 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -51,6 +51,7 @@ internal fun MarketsTokenDetailsContent( state: MarketsTokenDetailsUM, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + portfolioBlock: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, ) { Content( @@ -58,6 +59,7 @@ internal fun MarketsTokenDetailsContent( state = state, onBackClick = onBackClick, onHeaderSizeChange = onHeaderSizeChange, + portfolioBlock = portfolioBlock, ) InfoBottomSheet(config = state.infoBottomSheet) @@ -69,6 +71,7 @@ private fun Content( state: MarketsTokenDetailsUM, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + portfolioBlock: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, ) { val backgroundColor = LocalMainBottomSheetColor.current.value @@ -129,6 +132,7 @@ private fun Content( tokenMarketDetailsBody( state = state.body, + portfolioBlock = portfolioBlock, ) } } @@ -288,6 +292,7 @@ private fun Preview() { ), onHeaderSizeChange = {}, onBackClick = {}, + portfolioBlock = {}, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 40cc2fc2f5..620e182622 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -11,16 +11,31 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData -internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) { +internal fun LazyListScope.tokenMarketDetailsBody( + state: MarketsTokenDetailsUM.Body, + portfolioBlock: @Composable (Modifier) -> Unit, +) { when (state) { MarketsTokenDetailsUM.Body.Loading -> { - loading() + item("description-loading") { + DescriptionPlaceholder(modifier = Modifier.blockPaddings()) + } + + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } + + loadingInfoBlocks() } is MarketsTokenDetailsUM.Body.Content -> { if (state.description != null) { description(state.description) } + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } + infoBlocksList(state.infoBlocks) } is MarketsTokenDetailsUM.Body.Error -> { @@ -106,11 +121,7 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati } } -private fun LazyListScope.loading() { - item("description-loading") { - DescriptionPlaceholder(modifier = Modifier.blockPaddings()) - } - +private fun LazyListScope.loadingInfoBlocks() { item("insights-loading") { InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt index c4217765dd..e23fee1426 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt @@ -3,15 +3,18 @@ package com.tangem.features.markets.portfolio.api import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.markets.TokenMarketInfo import kotlinx.serialization.Serializable @Stable interface MarketsPortfolioComponent : ComposableContentComponent { @Serializable - data class Params( - val tokenId: String, - ) + data class Params(val tokenId: String) + + fun setTokenNetworks(networks: List) + + fun setNoNetworksAvailable() interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt index 9b732e6e42..be951a14a5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio @@ -21,6 +22,9 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( private val model: MarketsPortfolioModel = getOrCreateModel(params) + override fun setTokenNetworks(networks: List) = model.setTokenNetworks(networks) + override fun setNoNetworksAvailable() = model.setNoNetworksAvailable() + @Composable override fun Content(modifier: Modifier) { MyPortfolio( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 75baa3fd50..ba1f20d5c9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject @@ -17,4 +18,13 @@ internal class MarketsPortfolioModel @Inject constructor( @Suppress("UnusedPrivateMember") private val params = paramsContainer.require() + + @Suppress("UnusedPrivateMember") + fun setTokenNetworks(networks: List) { + // TODO [REDACTED_TASK_KEY] + } + + fun setNoNetworksAvailable() { + // TODO [REDACTED_TASK_KEY] + } } \ No newline at end of file From cba5086e704db93fbf7128778219e96648b117ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 18:09:38 +0400 Subject: [PATCH 05/41] Updated on 2026-08-14 --- .../ui/cardsettings/CardSettingsViewModel.kt | 9 ++--- .../feedback/converters/CardInfoConverter.kt | 6 ++-- ...sponseExtensions.kt => ScanResponseExt.kt} | 36 +++++++++++++++++-- .../domain/common/util/UserWalletExt.kt | 17 +++++++++ .../details/utils/UserWalletMappers.kt | 12 ++----- .../wallet/domain/UserWalletExt.kt | 18 ---------- .../domain/WalletAdditionalInfoFactory.kt | 1 + .../wallet/domain/WalletImageResolver.kt | 1 + .../SetBalancesAndLimitsTransformer.kt | 2 +- .../SetTokenListErrorTransformer.kt | 2 +- .../UpdateWalletCardsCountTransformer.kt | 2 +- .../MultiWalletCardStateConverter.kt | 2 +- .../SingleWalletCardStateConverter.kt | 2 +- .../viewmodels/WalletsUpdateActionResolver.kt | 2 +- 14 files changed, 65 insertions(+), 47 deletions(-) rename domain/legacy/src/main/java/com/tangem/domain/common/util/{ScanResponseExtensions.kt => ScanResponseExt.kt} (73%) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 7f972b948d..4abe4b37cc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder @@ -173,13 +174,7 @@ internal class CardSettingsViewModel @Inject constructor( userWalletId = userWalletId, cardId = card.cardId, isActiveBackupStatus = card.backupStatus?.isActive == true, - backupCardsCount = when (val status = card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked, - CardDTO.BackupStatus.NoBackup, - null, - -> 0 - }, + backupCardsCount = scanResponse.getBackupCardsCount() ?: 0, ), ) } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt index cb2081979e..c1855597a2 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -1,6 +1,7 @@ package com.tangem.data.feedback.converters import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -20,10 +21,7 @@ internal object CardInfoConverter : Converter { CardInfo( userWalletId = createUserWalletId(scanResponse = value), cardId = card.cardId, - cardsCount = when (val status = value.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount.toString() - else -> "0" - }, + cardsCount = value.getBackupCardsCount()?.toString() ?: "0", firmwareVersion = card.firmwareVersion.stringValue, cardBlockchain = walletData?.blockchain, signedHashesList = card.wallets.map { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt similarity index 73% rename from domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt rename to domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt index 1c3519f606..94727f02cb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt @@ -12,6 +12,7 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.configs.Wallet2CardConfig +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet @@ -32,8 +33,6 @@ val UserWallet.cardTypesResolver: CardTypesResolver get() = scanResponse.cardTypesResolver fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null -fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed -fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean { return hasDerivation(blockchain, DerivationPath(rawDerivationPath)) @@ -66,4 +65,37 @@ private fun ScanResponse.hasDerivation(curve: EllipticCurve, derivationPath: Der val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false val extendedPublicKey = extendedPublicKeysMap[derivationPath] return extendedPublicKey != null +} + +/** + * Get total cards count in wallets set for this [ScanResponse] card + * + * @return null if wallet is not multi-currency or total cards count + */ +fun ScanResponse.getCardsCount(): Int? { + if (!cardTypesResolver.isMultiwalletAllowed()) return null + + return when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + 1 + is CardDTO.BackupStatus.NoBackup, + is CardDTO.BackupStatus.CardLinked, + null, // Multi-currency wallet without backup function. Example, 4.12 + -> 1 + } +} + +/** + * Get backup cards count for this [ScanResponse] card + * + * @return null if wallet is not multi-currency or total cards count + */ +fun ScanResponse.getBackupCardsCount(): Int? { + return if (cardTypesResolver.isMultiwalletAllowed()) { + when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + else -> 0 + } + } else { + null + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt new file mode 100644 index 0000000000..0b5ef6f2fd --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.common.util + +import com.tangem.domain.wallets.models.UserWallet + +/** + * Get total cards count in wallets set for a card that was saved in [UserWallet] + * + * @return null if wallet is not multi-currency or total cards count + */ +fun UserWallet.getCardsCount(): Int? = scanResponse.getCardsCount() + +/** + * Get backup cards count for a card that was saved in [UserWallet] + * + * @return null if wallet is not multi-currency or total cards count + */ +fun UserWallet.getBackupCardsCount(): Int? = scanResponse.getBackupCardsCount() \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index 1a3efa0cf4..f51b383fbd 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -59,7 +59,7 @@ private fun UserWallet.getInfo( ): TextReference { val dividerRef = stringReference(value = " • ") - val cardCount = getCardCount() + val cardCount = getCardsCount() ?: 1 val cardCountRef = TextReference.PluralRes( id = R.plurals.card_label_card_count, count = cardCount, @@ -99,12 +99,4 @@ private fun getBalanceInfo( } else { combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) } -} - -private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount.inc() - is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc() - is CardDTO.BackupStatus.NoBackup, - null, - -> 1 } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt deleted file mode 100644 index a78ded4bf0..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.wallets.models.UserWallet - -fun UserWallet.getCardsCount(): Int? { - return if (isMultiCurrency) { - when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount + 1 - is CardDTO.BackupStatus.NoBackup, - is CardDTO.BackupStatus.CardLinked, - -> 1 - null -> 1 // Multi-currency wallet without backup function. Example, 4.12 - } - } else { - null - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 1f34e460f2..3de82a88d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 722f7de8f5..2bd5ddb371 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain import androidx.annotation.DrawableRes import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.demo.DemoConfig import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index d42d27f165..d4f7d3db34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -4,9 +4,9 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index fd70fb3cfa..a13bd46297 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 1994185f73..59531e1ece 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import timber.log.Timber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt index 6a914a5c2a..0ff9942daf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index 775957b478..8ea62cf3fe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 26d3d0c945..835a480b48 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -1,10 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import arrow.core.getOrElse +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState From 826eaece1333475f32121364f8e6da2791d174fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:55:10 +0400 Subject: [PATCH 06/41] Updated on 2026-08-14 --- .../components/SettingsAlertDialog.kt | 6 +-- .../components/SettingsSelectorDialog.kt | 4 +- .../details/ui/resetcard/ResetCardScreen.kt | 6 +-- .../EnrollBiometricsDialogConent.kt | 8 +-- .../welcome/ui/components/WarningDialog.kt | 8 +-- .../GiveTxPermisssionBottomSheet.kt | 2 +- .../com/tangem/core/ui/components/Dialogs.kt | 52 ++++++++++--------- .../impl/presentation/ui/SendEventEffect.kt | 12 ++--- .../feature/swap/ui/SwapScreenContent.kt | 2 +- .../ui/components/TokenDetailsDialogs.kt | 6 +-- .../model/WalletSettingsModel.kt | 6 +-- .../walletsettings/ui/RenameWalletDialog.kt | 10 ++-- .../presentation/wallet/ui/WalletAlert.kt | 20 +++---- 13 files changed, 73 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index b7b5b15da2..06e802b5e0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory @@ -20,12 +20,12 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) { title = dialog.title.resolveReference(), message = dialog.description.resolveReference(), isDismissable = false, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = dialog.confirmText.resolveReference(), warning = true, onClick = dialog.onConfirm, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = dialog.onDismiss, ), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt index 41b568b7ed..59af4ac24b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.SelectorDialog import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview @@ -21,7 +21,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { title = dialog.title.resolveReference(), selectedItemIndex = dialog.selectedItemIndex, items = dialog.items.map { it.resolveReference() }.toImmutableList(), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_cancel), onClick = dialog.onDismiss, ), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 1b52a50fd1..6f35968117 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -190,11 +190,11 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) { BasicDialog( title = stringResource(dialog.titleResId), message = stringResource(dialog.messageResId), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = dialog.onDismiss, ), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.card_settings_action_sheet_reset), warning = true, onClick = dialog.onConfirmClick, @@ -208,7 +208,7 @@ private fun CompletedResetDialog(dialog: ResetCardDialog) { BasicDialog( title = stringResource(id = dialog.titleResId), message = stringResource(id = dialog.messageResId), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = dialog.onConfirmClick, ), diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt index 115bd8d273..1f3b36e57d 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt @@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @Composable @@ -19,11 +19,11 @@ fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) { BasicDialog( title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title), message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_enable), onClick = dialog.onEnroll, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( onClick = dialog.onCancel, ), onDismissDialog = dialog.onCancel, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt index a6315d5b1e..57f226a9d7 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.wallet.R @@ -27,7 +27,7 @@ internal fun WarningDialog(warning: WarningModel?) { }, ), onDismissDialog = warning.onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), @@ -38,7 +38,7 @@ internal fun WarningDialog(warning: WarningModel?) { title = stringResource(id = R.string.common_attention), message = stringResource(id = R.string.key_invalidated_warning_description), onDismissDialog = warning.onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), @@ -50,7 +50,7 @@ internal fun WarningDialog(warning: WarningModel?) { message = stringResource(id = R.string.biometric_unavailable_warning), onDismissDialog = warning.onDismiss, isDismissable = false, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 73c62d311c..f10c86f973 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -59,7 +59,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { BasicDialog( message = content.data.dialogText.resolveReference(), title = stringResource(id = R.string.common_approve), - confirmButton = DialogButton { isPermissionAlertShow = false }, + confirmButton = DialogButtonUM { isPermissionAlertShow = false }, onDismissDialog = {}, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 97a3b692cc..e741b087fe 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -49,10 +49,10 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun BasicDialog( message: String, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -72,7 +72,7 @@ fun BasicDialog( fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) { TangemDialog( type = DialogType.Message(message), - confirmButton = DialogButton(onClick = onDismissDialog), + confirmButton = DialogButtonUM(onClick = onDismissDialog), onDismissDialog = onDismissDialog, title = null, dismissButton = null, @@ -97,12 +97,12 @@ fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) { @Composable fun TextInputDialog( fieldValue: TextFieldValue, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, onValueChange: (TextFieldValue) -> Unit, - textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, + textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() }, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -125,12 +125,12 @@ fun TextInputDialog( @Composable fun TextInputDialog( fieldValue: String, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, onValueChange: (String) -> Unit, - textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, + textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() }, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -154,7 +154,7 @@ fun TextInputDialog( fun SelectorDialog( selectedItemIndex: Int, items: ImmutableList, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onSelect: (index: Int) -> Unit, onDismissDialog: () -> Unit, title: String? = null, @@ -180,7 +180,7 @@ fun SelectorDialog( * @param enabled If false button will be disabled * @param onClick Button click callback */ -data class DialogButton( +data class DialogButtonUM( val title: String? = null, val warning: Boolean = false, val enabled: Boolean = true, @@ -190,7 +190,7 @@ data class DialogButton( /** * Additional params for dialog text field */ -data class AdditionalTextInputDialogParams( +data class AdditionalTextInputDialogUM( val label: String? = null, val placeholder: String? = null, val caption: String? = null, @@ -203,10 +203,10 @@ data class AdditionalTextInputDialogParams( @Composable private fun TangemDialog( type: DialogType, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, properties: DialogProperties = DialogProperties(), ) { Dialog(properties = properties, onDismissRequest = onDismissDialog) { @@ -304,7 +304,11 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { } @Composable -private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) { +private fun DialogButtons( + confirmButton: DialogButtonUM, + dismissButton: DialogButtonUM?, + modifier: Modifier = Modifier, +) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy( @@ -413,13 +417,13 @@ private sealed class DialogType { data class TextInput( val value: TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, - val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), + val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(), ) : DialogType() data class SimpleTextInput( val value: String, val onValueChange: (String) -> Unit, - val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), + val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(), ) : DialogType() data class Selector( @@ -445,8 +449,8 @@ private fun BasicDialogPreview() { message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " + "password to work with the app", title = "Attention", - confirmButton = DialogButton {}, - dismissButton = DialogButton {}, + confirmButton = DialogButtonUM {}, + dismissButton = DialogButtonUM {}, onDismissDialog = {}, ) } @@ -478,8 +482,8 @@ private fun WarningBasicDialogSample(modifier: Modifier = Modifier) { message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " + "password to work with the app", title = "Attention", - confirmButton = DialogButton(warning = true) {}, - dismissButton = DialogButton {}, + confirmButton = DialogButtonUM(warning = true) {}, + dismissButton = DialogButtonUM {}, onDismissDialog = {}, ) } @@ -502,10 +506,10 @@ private fun TextInputDialogSample(modifier: Modifier = Modifier) { TextInputDialog( fieldValue = TextFieldValue(text = ""), title = "Rename Wallet", - confirmButton = DialogButton {}, + confirmButton = DialogButtonUM {}, onDismissDialog = {}, onValueChange = {}, - textFieldParams = AdditionalTextInputDialogParams( + textFieldParams = AdditionalTextInputDialogUM( label = "Wallet name", ), ) @@ -530,7 +534,7 @@ private fun SelectorDialogPreview(@PreviewParameter(SelctorDialogParamsProvider: title = param.title, items = param.items, selectedItemIndex = param.selectedItemIndex, - confirmButton = DialogButton(title = "Cancel", onClick = {}), + confirmButton = DialogButtonUM(title = "Cancel", onClick = {}), onSelect = {}, onDismissDialog = {}, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt index 53378ca7f7..dee53dccc8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference @@ -45,24 +45,24 @@ internal fun SendEventEffect(event: StateEvent, snackbarHostState: Sn @Composable internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = { onActionClick() onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = onDismiss, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index f3004d1c73..ea5ce81e01 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -110,7 +110,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi BasicDialog( title = state.alert.title?.resolveReference(), message = message, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = state.alert.onClick, ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt index ed54c9459b..5a4f590fe7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -2,7 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components import androidx.compose.runtime.Composable import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig @@ -19,7 +19,7 @@ internal fun TokenDetailsDialogs(state: TokenDetailsState) { private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { BasicDialog( message = config.content.message.resolveReference(), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = config.content.confirmButtonConfig.text.resolveReference(), warning = config.content.confirmButtonConfig.warning, onClick = config.content.confirmButtonConfig.onClick, @@ -27,7 +27,7 @@ private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { onDismissDialog = config.onDismissRequest, title = config.content.title?.resolveReference(), dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> - DialogButton( + DialogButtonUM( title = cancelButtonConfig.text.resolveReference(), warning = cancelButtonConfig.warning, onClick = cancelButtonConfig.onClick, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index a9e5b42932..fcb721457e 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -13,7 +13,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage @@ -93,7 +93,7 @@ internal class WalletSettingsModel @Inject constructor( BasicDialog( message = stringResource(R.string.user_wallet_list_delete_prompt), onDismissDialog = onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_delete), warning = true, onClick = { @@ -101,7 +101,7 @@ internal class WalletSettingsModel @Inject constructor( onDismiss() }, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(R.string.common_cancel), onClick = onDismiss, ), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index 5ff04fe2e7..8cbce3fc72 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -6,8 +6,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.AdditionalTextInputDialogParams -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.AdditionalTextInputDialogUM +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.walletsettings.component.preview.PreviewRenameWalletComponent @@ -21,18 +21,18 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { TextInputDialog( title = stringResource(id = R.string.user_wallet_list_rename_popup_title), fieldValue = value, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), enabled = model.isConfirmEnabled, onClick = model.onConfirm, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ), onDismissDialog = onDismiss, onValueChange = model.updateValue, - textFieldParams = AdditionalTextInputDialogParams( + textFieldParams = AdditionalTextInputDialogUM( label = stringResource(id = R.string.user_wallet_list_rename_popup_placeholder), ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index 48e959ad54..ca9ee260f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.compose.runtime.* import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.AdditionalTextInputDialogParams +import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.impl.R @@ -21,12 +21,12 @@ internal fun WalletAlert(state: WalletAlertState, onDismiss: () -> Unit) { @Composable private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), warning = state.isWarningConfirmButton, onClick = { @@ -34,12 +34,12 @@ private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), warning = state.isWarningConfirmButton, onClick = onDismiss, @@ -62,7 +62,7 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U TextInputDialog( fieldValue = value, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), enabled = value.text.isNotEmpty() && value.text != state.text && @@ -75,8 +75,8 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U onDismissDialog = onDismiss, onValueChange = { value = it }, title = state.title.resolveReference(), - dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), - textFieldParams = AdditionalTextInputDialogParams( + dismissButton = DialogButtonUM(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), + textFieldParams = AdditionalTextInputDialogUM( label = state.label.resolveReference(), isError = state.errorTextProvider(value.text) != null, caption = state.errorTextProvider(value.text)?.resolveReference(), From a83754481784c70c3e6f00341ef334813674c23a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:56:34 +0400 Subject: [PATCH 07/41] Updated on 2026-08-14 --- .../currency/icon/CurrencyIconState.kt | 28 +++++++++++++++++-- .../ui/src/main/res/drawable/ic_tether_24.xml | 14 ++++++++++ .../ui/src/main/res/drawable/ic_tether_28.xml | 17 ----------- .../feature/referral/ui/ReferralScreen.kt | 4 +-- 4 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_tether_24.xml delete mode 100644 core/ui/src/main/res/drawable/ic_tether_28.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 85e49532fc..f19975ab9e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -38,7 +38,7 @@ sealed class CurrencyIconState { * Represents a token icon. * * @property url The URL where the token icon can be fetched from. May be `null` if not found. - * @property topBadgeIconResId The drawable resource ID for the network badge. + * @property topBadgeIconResId The drawable resource ID for the network badge. May be `null`. * @property isGrayscale Specifies whether to show the icon in grayscale. * @property showCustomBadge Specifies whether to show the custom token badge. * @property fallbackTint The color to be used for tinting the fallback icon. @@ -46,7 +46,7 @@ sealed class CurrencyIconState { */ data class TokenIcon( val url: String?, - @DrawableRes override val topBadgeIconResId: Int, + @DrawableRes override val topBadgeIconResId: Int?, override val isGrayscale: Boolean, override val showCustomBadge: Boolean, val fallbackTint: Color, @@ -81,4 +81,28 @@ sealed class CurrencyIconState { override val showCustomBadge: Boolean = false override val topBadgeIconResId: Int? = null } + + fun copySealed( + isGrayscale: Boolean = this.isGrayscale, + showCustomBadge: Boolean = this.showCustomBadge, + topBadgeIconResId: Int? = this.topBadgeIconResId, + ): CurrencyIconState = when (this) { + is CoinIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + ) + is CustomTokenIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + topBadgeIconResId = topBadgeIconResId ?: this.topBadgeIconResId, + ) + is TokenIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + topBadgeIconResId = topBadgeIconResId, + ) + is Loading, + is Locked, + -> this + } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_tether_24.xml b/core/ui/src/main/res/drawable/ic_tether_24.xml new file mode 100644 index 0000000000..3c53fbab8e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tether_24.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_tether_28.xml b/core/ui/src/main/res/drawable/ic_tether_28.xml deleted file mode 100644 index e3f94b5dbb..0000000000 --- a/core/ui/src/main/res/drawable/ic_tether_28.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 7d6614a7aa..9fe33b7bd6 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -195,7 +195,7 @@ private fun ReferralInfo( } is ReferralInfoState.Loading -> { - LoadingCondition(iconResId = R.drawable.ic_tether_28) + LoadingCondition(iconResId = R.drawable.ic_tether_24) SpacerH32() LoadingCondition(iconResId = R.drawable.ic_discount_28) } @@ -211,7 +211,7 @@ private fun Conditions(state: ReferralInfoContentState) { @Composable private fun ConditionForYou(state: ReferralInfoContentState) { - Condition(iconResId = R.drawable.ic_tether_28) { + Condition(iconResId = R.drawable.ic_tether_24) { when (state) { is ReferralInfoState.ParticipantContent -> InfoForYou( award = state.award, From 09f69d5c32ed5be28b6b86c4dae82f6ec1637225 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:58:25 +0400 Subject: [PATCH 08/41] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/home/HomeFragment.kt | 2 +- .../java/com/tangem/tap/features/home/HomeViewModel.kt | 2 +- .../java/com/tangem/tap/routing/utils/ChildFactory.kt | 8 +------- .../main/kotlin/com/tangem/common/routing/AppRoute.kt | 4 ++-- .../managetokens/component/ManageTokensComponent.kt | 4 ++-- features/wallet-settings/impl/build.gradle.kts | 1 + .../preview/PreviewWalletSettingsComponent.kt | 4 ++++ .../feature/walletsettings/utils/ItemsBuilder.kt | 10 ++++++++++ .../wallet/presentation/router/DefaultWalletRouter.kt | 4 ++-- .../wallet/presentation/router/InnerWalletRouter.kt | 2 +- .../viewmodels/intents/WalletContentClickIntents.kt | 2 +- 11 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 508c8a2f4e..292b7b1bf6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -98,7 +98,7 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber { viewModel.onSearchClick() } else { Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } store.dispatch(TokensAction.SetArgs.ReadAccess) } }, diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt index c545f2f83b..0299b8832d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt @@ -61,7 +61,7 @@ internal class HomeViewModel @Inject constructor( analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) store.dispatch(TokensAction.SetArgs.ReadAccess) - store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } } private fun scanCard() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index af1bebc286..6815e3f6c9 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -126,13 +126,7 @@ internal class ChildFactory @Inject constructor( if (manageTokensToggles.isFeatureEnabled) { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = ManageTokensComponent.Params( - mode = if (route.readOnlyContent) { - ManageTokensComponent.Mode.READ_ONLY - } else { - ManageTokensComponent.Mode.MANAGE - }, - ), + params = ManageTokensComponent.Params(route.userWalletId), componentFactory = manageTokensComponentFactory, ) } else { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 6c03778967..4f53e068c8 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -177,8 +177,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( - val readOnlyContent: Boolean, - ) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams { + val userWalletId: UserWalletId? = null, + ) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) } diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index a38a96267c..c327e0ac05 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -2,11 +2,11 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface ManageTokensComponent : ComposableContentComponent { - data class Params(val mode: Mode) + data class Params(val userWalletId: UserWalletId?) - enum class Mode { READ_ONLY, MANAGE, } interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 14885a740b..e472817d13 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Project - API */ implementation(projects.features.walletSettings.api) + implementation(projects.features.manageTokens.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 4dba365556..4c4d0e3f25 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -8,6 +8,7 @@ import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.ui.WalletSettingsScreen import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.features.managetokens.ManageTokensToggles internal class PreviewWalletSettingsComponent : WalletSettingsComponent { @@ -15,6 +16,9 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { popBack = {}, items = ItemsBuilder( router = DummyRouter(), + manageTokensToggles = object : ManageTokensToggles { + override val isFeatureEnabled: Boolean = true + }, ).buildItems( userWalletId = UserWalletId("011"), userWalletName = "My Wallet", diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 941f1c53f2..4a7d294e77 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R +import com.tangem.features.managetokens.ManageTokensToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -17,6 +18,7 @@ import javax.inject.Inject @ComponentScoped internal class ItemsBuilder @Inject constructor( private val router: Router, + private val manageTokensToggles: ManageTokensToggles, ) { @Suppress("LongParameterList") @@ -50,6 +52,14 @@ internal class ItemsBuilder @Inject constructor( id = "card", description = resourceReference(R.string.settings_card_settings_footer), blocks = buildList { + if (manageTokensToggles.isFeatureEnabled) { + BlockUM( + text = resourceReference(R.string.add_tokens_title), + iconRes = R.drawable.ic_tether_24, + onClick = { router.push(AppRoute.ManageTokens(userWalletId)) }, + ).let(::add) + } + if (isLinkMoreCardsAvailable) { BlockUM( text = resourceReference(R.string.details_row_title_create_backup), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index a70acf53cd..d91ca3fb45 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -139,8 +139,8 @@ internal class DefaultWalletRouter( return router.stack.lastOrNull() is AppRoute.Wallet } - override fun openManageTokensScreen() { - router.push(AppRoute.ManageTokens(readOnlyContent = false)) + override fun openManageTokensScreen(userWalletId: UserWalletId) { + router.push(AppRoute.ManageTokens(userWalletId = userWalletId)) } override fun openScanFailedDialog(onTryAgain: () -> Unit) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index ee4229c970..afb3ce979a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -54,7 +54,7 @@ internal interface InnerWalletRouter : WalletRouter { fun isWalletLastScreen(): Boolean /** Open manage tokens screen */ - fun openManageTokensScreen() + fun openManageTokensScreen(userWalletId: UserWalletId) /** Open scan failed dialog */ fun openScanFailedDialog(onTryAgain: () -> Unit) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 5c192d9a44..6b0d74852e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -90,7 +90,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onManageTokensClick() { analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens) reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) - router.openManageTokensScreen() + router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } override fun onOrganizeTokensClick() { From e14e82a284aab4d16cb4c5d0f17d2ef4f203e370 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:59:32 +0400 Subject: [PATCH 09/41] Updated on 2026-08-14 --- .../ui/components/list/InfiniteListHandler.kt | 26 +++++++++++++++++++ .../ui/components/MarketsListLazyColumn.kt | 23 +--------------- 2 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt new file mode 100644 index 0000000000..9d5c548b93 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt @@ -0,0 +1,26 @@ +package com.tangem.core.ui.components.list + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* + +@Composable +fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) { + val loadMore by remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + + lastVisibleItemIndex > totalItemsNumber - buffer + } + } + + val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } + var emitted by remember(totalItemsCount) { mutableStateOf(false) } + + LaunchedEffect(loadMore) { + if (loadMore && !emitted) { + emitted = onLoadMore() + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 0da161d62a..2674b90648 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme @@ -196,26 +197,4 @@ private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { state.visibleIdsChanged(visibleItems) } } -} - -@Composable -fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) { - val loadMore by remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - - lastVisibleItemIndex > totalItemsNumber - buffer - } - } - - val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } - var emitted by remember(totalItemsCount) { mutableStateOf(false) } - - LaunchedEffect(loadMore) { - if (loadMore && !emitted) { - emitted = onLoadMore() - } - } } \ No newline at end of file From 5ef69ea4b65ae1c3fc954ed69cb06471f039f95b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 18:00:50 +0400 Subject: [PATCH 10/41] Updated on 2026-08-14 --- features/manage-tokens/impl/build.gradle.kts | 4 +- .../preview/PreviewManageTokensComponent.kt | 55 +++++++++---------- .../managetokens/entity/CurrencyItemUM.kt | 21 ++++--- .../managetokens/entity/ManageTokensUM.kt | 31 +++++++++-- .../ui/CustomTokenNetworkSelectorContent.kt | 2 +- 5 files changed, 70 insertions(+), 43 deletions(-) diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 1305e90261..681f347d02 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -22,8 +22,10 @@ dependencies { implementation(projects.core.featuretoggles) /* Project - Domain */ - implementation(projects.domain.wallets.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /* AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 980afcb895..89103d7f03 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -9,11 +9,14 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.* +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.ManageTokensTopBarUM +import com.tangem.features.managetokens.entity.ManageTokensUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.ManageTokensScreen import kotlinx.collections.immutable.mutate @@ -47,8 +50,10 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { onActiveChange = ::toggleSearchBar, ), hasChanges = false, - isLoading = false, - onSaveClick = {}, + isInitialBatchLoading = false, + isNextBatchLoading = true, + loadMore = { false }, + saveChanges = {}, ), ) @@ -58,7 +63,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { initItems() } else { items.filter { currency -> - currency.model.name.contains(query, ignoreCase = true) + currency.name.contains(query, ignoreCase = true) }.toPersistentList() } @@ -96,34 +101,28 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { }.toPersistentList() private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( - id = index.toString(), - model = ChainRowUM( - name = "Custom token $index", - type = "CT$index", - icon = CurrencyIconState.CustomTokenIcon( - tint = Color.White, - background = Color.Black, - topBadgeIconResId = R.drawable.img_eth_22, - isGrayscale = false, - showCustomBadge = true, - ), - showCustom = true, + id = ManagedCryptoCurrency.ID(index.toString()), + name = "Custom token $index", + symbol = "CT$index", + icon = CurrencyIconState.CustomTokenIcon( + tint = Color.White, + background = Color.Black, + topBadgeIconResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = true, ), onRemoveClick = {}, ) private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( - id = index.toString(), - model = ChainRowUM( - name = "Currency $index", - type = "C$index", - icon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_btc_22, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, + id = ManagedCryptoCurrency.ID(index.toString()), + name = "Currency $index", + symbol = "C$index", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + showCustomBadge = false, ), networks = if (index == 2) { CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt index 0cfc505545..eec4f853bc 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt @@ -1,18 +1,23 @@ package com.tangem.features.managetokens.entity import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class CurrencyItemUM { - abstract val id: String - abstract val model: ChainRowUM + abstract val id: ManagedCryptoCurrency.ID + abstract val name: String + abstract val symbol: String + abstract val icon: CurrencyIconState data class Basic( - override val id: String, - override val model: ChainRowUM, + override val id: ManagedCryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val icon: CurrencyIconState, val networks: NetworksUM, val onExpandClick: () -> Unit, ) : CurrencyItemUM() { @@ -29,8 +34,10 @@ internal sealed class CurrencyItemUM { } data class Custom( - override val id: String, - override val model: ChainRowUM, + override val id: ManagedCryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val icon: CurrencyIconState, val onRemoveClick: () -> Unit, ) : CurrencyItemUM() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt index d53e7e5e4d..bd8e4f27bf 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt @@ -8,26 +8,32 @@ import kotlinx.collections.immutable.ImmutableList internal sealed class ManageTokensUM { abstract val popBack: () -> Unit - abstract val isLoading: Boolean + abstract val isInitialBatchLoading: Boolean + abstract val isNextBatchLoading: Boolean abstract val items: ImmutableList abstract val topBar: ManageTokensTopBarUM abstract val search: SearchBarUM + abstract val loadMore: () -> Boolean data class ReadContent( override val popBack: () -> Unit, - override val isLoading: Boolean, + override val isInitialBatchLoading: Boolean, + override val isNextBatchLoading: Boolean, override val items: ImmutableList, override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, + override val loadMore: () -> Boolean, ) : ManageTokensUM() data class ManageContent( override val popBack: () -> Unit, - override val isLoading: Boolean, + override val isInitialBatchLoading: Boolean, + override val isNextBatchLoading: Boolean, override val items: ImmutableList, override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, - val onSaveClick: () -> Unit, + override val loadMore: () -> Boolean, + val saveChanges: () -> Unit, val hasChanges: Boolean, ) : ManageTokensUM() @@ -35,10 +41,23 @@ internal sealed class ManageTokensUM { search: SearchBarUM = this.search, items: ImmutableList = this.items, hasChanges: Boolean = this is ManageContent && this.hasChanges, + isInitialBatchLoading: Boolean = this.isInitialBatchLoading, + isNextBatchLoading: Boolean = this.isNextBatchLoading, ): ManageTokensUM { return when (this) { - is ManageContent -> copy(search = search, items = items, hasChanges = hasChanges) - is ReadContent -> copy(search = search, items = items) + is ManageContent -> copy( + search = search, + items = items, + hasChanges = hasChanges, + isInitialBatchLoading = isInitialBatchLoading, + isNextBatchLoading = isNextBatchLoading, + ) + is ReadContent -> copy( + search = search, + items = items, + isInitialBatchLoading = isInitialBatchLoading, + isNextBatchLoading = isNextBatchLoading, + ) } } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt index 57abdb15b5..2363fff9c9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt @@ -102,7 +102,7 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) icon = CurrencyIconState.CoinIcon( url = null, fallbackResId = model.iconResId, - isGrayscale = false, + isGrayscale = !model.isSelected, showCustomBadge = false, ), showCustom = false, From 68842dcadc0fdc2cf689bb2610c616a47f0e4ce0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 18:02:42 +0400 Subject: [PATCH 11/41] Updated on 2026-08-14 --- .../impl/DefaultManageTokensComponent.kt | 3 + .../managetokens/ui/ManageTokensScreen.kt | 212 +++++++++++------- .../ui/dialog/HasLinkedTokensWarning.kt | 30 +++ .../ui/dialog/HideTokenWarning.kt | 29 +++ 4 files changed, 197 insertions(+), 77 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index e89139cf5f..7fc4940bea 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.managetokens.component.impl +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -24,6 +25,8 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + BackHandler(onBack = state.popBack) + ManageTokensScreen( modifier = modifier, state = state, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 3008c84d30..00a647c307 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -1,7 +1,6 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration -import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi @@ -10,6 +9,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon @@ -20,6 +20,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -33,14 +39,17 @@ import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM @@ -51,18 +60,30 @@ import kotlinx.collections.immutable.ImmutableList private const val CHEVRON_ROTATION_EXPANDED = 180f private const val CHEVRON_ROTATION_COLLAPSED = 0f +private const val LOAD_ITEMS_BUFFER = 10 @Composable internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { - BackHandler(onBack = state.popBack) + val keyboardController = LocalSoftwareKeyboardController.current + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + keyboardController?.hide() + + return super.onPreScroll(available, source) + } + } + } Scaffold( - modifier = modifier, + modifier = modifier.nestedScroll(nestedScrollConnection), containerColor = TangemTheme.colors.background.primary, + contentWindowInsets = WindowInsetsZero, topBar = { ManageTokensTopBar( modifier = Modifier.statusBarsPadding(), topBar = state.topBar, + search = state.search, ) }, content = { innerPadding -> @@ -70,10 +91,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi modifier = Modifier .padding(innerPadding) .fillMaxSize(), - search = state.search, - items = state.items, - isLoading = state.isLoading, - hasChanges = state is ManageTokensUM.ManageContent && state.hasChanges, + state = state, ) }, floatingActionButtonPosition = FabPosition.Center, @@ -81,10 +99,11 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi if (state is ManageTokensUM.ManageContent) { SaveChangesButton( modifier = Modifier + .navigationBarsPadding() .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), isVisible = state.hasChanges, - onClick = state.onSaveClick, + onClick = state.saveChanges, ) } }, @@ -92,16 +111,26 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi } @Composable -private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, modifier: Modifier = Modifier) { - TangemTopAppBar( - modifier = modifier, - title = topBar.title.resolveReference(), - startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick), - endButton = when (topBar) { - is ManageTokensTopBarUM.ManageContent -> topBar.endButton - is ManageTokensTopBarUM.ReadContent -> null - }, - ) +private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + TangemTopAppBar( + title = topBar.title.resolveReference(), + startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick), + endButton = when (topBar) { + is ManageTokensTopBarUM.ManageContent -> topBar.endButton + is ManageTokensTopBarUM.ReadContent -> null + }, + ) + SearchBar( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing12) + .padding(horizontal = TangemTheme.dimens.spacing16), + state = search, + ) + } } @Composable @@ -122,80 +151,59 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: } @Composable -private fun LoadingContent() { - Box( - modifier = Modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.primary), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(color = TangemTheme.colors.icon.accent) - } -} - -@Composable -private fun Content( - search: SearchBarUM, - items: ImmutableList, - isLoading: Boolean, - hasChanges: Boolean, - modifier: Modifier = Modifier, -) { +private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { Box(modifier = modifier) { Currencies( modifier = Modifier.fillMaxSize(), - items = items, - search = search, + items = state.items, + showLoadingItem = state.isNextBatchLoading, + onLoadMore = state.loadMore, + isEditable = state is ManageTokensUM.ManageContent, ) - AnimatedVisibility( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth(), - visible = hasChanges, - label = "bottom_fade_visibility", - ) { - BottomFade() - } + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) } - Crossfade(targetState = isLoading, label = "ManageTokensLoadingContent") { - if (it) { - LoadingContent() + Crossfade(targetState = state.isInitialBatchLoading, label = "ManageTokensLoadingContent") { isVisible -> + if (isVisible) { + ProgressIndicator( + modifier = Modifier.fillMaxSize(), + ) } } } @OptIn(ExperimentalFoundationApi::class) @Composable -private fun Currencies(items: ImmutableList, search: SearchBarUM, modifier: Modifier = Modifier) { +private fun Currencies( + items: ImmutableList, + showLoadingItem: Boolean, + isEditable: Boolean, + onLoadMore: () -> Boolean, + modifier: Modifier = Modifier, +) { + val bottomBarHeight = with(LocalDensity.current) { + WindowInsets.systemBars.getBottom(density = this).toDp() + } + val listState = rememberLazyListState() + LazyColumn( modifier = modifier, + state = listState, + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing76 + bottomBarHeight, + ), ) { - stickyHeader(key = "search") { - Column( - modifier = Modifier - .background(TangemTheme.colors.background.primary) - .padding( - top = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, - ) - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) { - SearchBar(state = search) - } - } - items( items = items, - key = CurrencyItemUM::id, + key = { it.id.value }, ) { item -> when (item) { is CurrencyItemUM.Basic -> { BasicCurrencyItem( modifier = Modifier.fillMaxWidth(), item = item, + isEditable = isEditable, ) } is CurrencyItemUM.Custom -> { @@ -206,6 +214,34 @@ private fun Currencies(items: ImmutableList, search: SearchBarUM } } } + + item(key = "loading_indicator") { + AnimatedVisibility( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + visible = showLoadingItem, + label = "loading_indicator_visibility", + ) { + ProgressIndicator() + } + } + } + + InfiniteListHandler( + listState = listState, + buffer = LOAD_ITEMS_BUFFER, + onLoadMore = onLoadMore, + ) +} + +@Composable +private fun ProgressIndicator(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background(color = TangemTheme.colors.background.primary), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = TangemTheme.colors.icon.informative) } } @@ -213,7 +249,14 @@ private fun Currencies(items: ImmutableList, search: SearchBarUM private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = Modifier) { ChainRow( modifier = modifier, - model = item.model, + model = with(item) { + ChainRowUM( + name = name, + type = symbol, + icon = icon, + showCustom = true, + ) + }, action = { SecondarySmallButton( config = SmallButtonConfig( @@ -226,13 +269,20 @@ private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = } @Composable -private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = Modifier) { +private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, isEditable: Boolean, modifier: Modifier = Modifier) { val isExpanded = item.networks is NetworksUM.Expanded Column(modifier = modifier) { ChainRow( modifier = Modifier.clickable(onClick = item.onExpandClick), - model = item.model, + model = with(item) { + ChainRowUM( + name = name, + type = symbol, + icon = icon, + showCustom = false, + ) + }, action = { val rotation by animateFloatAsState( targetValue = if (isExpanded) { @@ -260,13 +310,19 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M end = TangemTheme.dimens.spacing8, ), networks = item.networks, - currencyId = item.id, + currencyId = item.id.value, + isEditable = isEditable, ) } } @Composable -private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) { +private fun NetworksList( + networks: NetworksUM, + currencyId: String, + isEditable: Boolean, + modifier: Modifier = Modifier, +) { AnimatedVisibility( modifier = modifier, visible = networks is NetworksUM.Expanded, @@ -297,10 +353,12 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod ) }, action = { - TangemSwitch( - checked = network.isSelected, - onCheckedChange = network.onSelectedStateChange, - ) + if (isEditable) { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = network.onSelectedStateChange, + ) + } }, ) }, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt new file mode 100644 index 0000000000..848d654e08 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt @@ -0,0 +1,30 @@ +package com.tangem.features.managetokens.ui.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun HasLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network, onDismiss: () -> Unit) { + BasicDialog( + title = stringResource( + R.string.token_details_unable_hide_alert_title, + currency.name, + ), + message = stringResource( + R.string.token_details_unable_hide_alert_message, + currency.name, + currency.symbol, + network.name, + ), + confirmButton = DialogButtonUM( + title = stringResource(R.string.common_ok), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt new file mode 100644 index 0000000000..46b9cc6a39 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt @@ -0,0 +1,29 @@ +package com.tangem.features.managetokens.ui.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun HideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit, onDismiss: () -> Unit) { + BasicDialog( + title = stringResource( + R.string.token_details_hide_alert_title, + currency.name, + ), + message = stringResource(R.string.token_details_hide_alert_message), + confirmButton = DialogButtonUM( + title = stringResource(R.string.token_details_hide_alert_hide), + warning = true, + onClick = onConfirm, + ), + dismissButton = DialogButtonUM( + title = stringResource(R.string.common_cancel), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file From 5b4f0214d28684d5a8712945920499611dea1e84 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 18:03:03 +0400 Subject: [PATCH 12/41] Updated on 2026-08-14 --- .../managetokens/model/ManageTokensModel.kt | 299 +++++++++--------- .../utils/list/ChangedCurrenciesManager.kt | 46 +++ .../utils/list/ManageTokensListManager.kt | 206 ++++++++++++ .../utils/list/ManageTokensListState.kt | 43 +++ .../utils/list/ManageTokensUiManager.kt | 200 ++++++++++++ .../utils/mapper/CurrencyItemMapper.kt | 77 +++++ .../utils/mapper/CurrencyNetworksMapper.kt | 46 +++ .../utils/ui/CurrencyItemOperations.kt | 66 ++++ .../utils/ui/CurrencyNetworkOperations.kt | 21 ++ 9 files changed, 848 insertions(+), 156 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 6141538abd..2d2ce96fe0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -1,53 +1,78 @@ package com.tangem.features.managetokens.model -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.tokens.model.Network +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.* +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.ManageTokensTopBarUM +import com.tangem.features.managetokens.entity.ManageTokensUM import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.list.ChangedCurrencies +import com.tangem.features.managetokens.utils.list.ManageTokensListManager +import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.mutate -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @ComponentScoped internal class ManageTokensModel @Inject constructor( - paramsContainer: ParamsContainer, - private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val manageTokensListManager: ManageTokensListManager, + private val messageSender: UiMessageSender, + paramsContainer: ParamsContainer, ) : Model() { private val params: ManageTokensComponent.Params = paramsContainer.require() - private val changedItemsIds: MutableSet = mutableSetOf() - private var items = initItems() - val state: MutableStateFlow = MutableStateFlow(value = getInitialState(mode = params.mode)) + val state: MutableStateFlow = MutableStateFlow(getInitialState(params.userWalletId)) - private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM { - return when (mode) { - ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel() - ManageTokensComponent.Mode.MANAGE -> createManageContentModel() + init { + manageTokensListManager.uiItems + .onEach { items -> updateItems(items) } + .launchIn(modelScope) + + manageTokensListManager.paginationStatus + .onEach { status -> updatePaginationStatus(status) } + .launchIn(modelScope) + + combine( + manageTokensListManager.currenciesToAdd, + manageTokensListManager.currenciesToRemove, + ::updateChangedItems, + ).launchIn(modelScope) + + modelScope.launch { + manageTokensListManager.launch(params.userWalletId) + } + } + + private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { + return if (userWalletId == null) { + createReadContentModel() + } else { + createManageContentModel() } } private fun createReadContentModel(): ManageTokensUM.ReadContent { return ManageTokensUM.ReadContent( popBack = router::pop, - isLoading = false, - items = initItems(), + isInitialBatchLoading = true, + isNextBatchLoading = false, + items = getInitialItems(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_search_tokens), onBackButtonClick = router::pop, @@ -59,14 +84,16 @@ internal class ManageTokensModel @Inject constructor( isActive = false, onActiveChange = ::toggleSearchBar, ), + loadMore = ::loadMoreItems, ) } private fun createManageContentModel(): ManageTokensUM.ManageContent { return ManageTokensUM.ManageContent( popBack = router::pop, - isLoading = false, - items = initItems(), + isInitialBatchLoading = true, + isNextBatchLoading = false, + items = getInitialItems(), topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, @@ -82,11 +109,85 @@ internal class ManageTokensModel @Inject constructor( isActive = false, onActiveChange = ::toggleSearchBar, ), - onSaveClick = ::onSaveClick, hasChanges = false, + saveChanges = ::onSaveClick, + loadMore = ::loadMoreItems, ) } + private fun updateItems(items: ImmutableList) { + state.update { state -> + state.copySealed( + items = items, + ) + } + } + + private fun updatePaginationStatus(status: PaginationStatus<*>) { + state.update { state -> + when (status) { + is PaginationStatus.InitialLoading -> { + if (state.search.isActive) { + state + } else { + state.copySealed( + isInitialBatchLoading = true, + ) + } + } + is PaginationStatus.NextBatchLoading -> state.copySealed( + isNextBatchLoading = true, + ) + is PaginationStatus.InitialLoadingError -> { + val message = SnackbarMessage( + message = status.throwable.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + is PaginationStatus.None, + is PaginationStatus.Paginating, + is PaginationStatus.EndOfPagination, + -> state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + } + } + + private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { + state.update { state -> + state.copySealed( + hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), + ) + } + } + + private fun loadMoreItems(): Boolean { + val state = state.value + if (state.isInitialBatchLoading) return false + + modelScope.launch { + manageTokensListManager.loadMore( + userWalletId = params.userWalletId, + query = state.search.query, + ) + } + + return true + } + + private fun getInitialItems(): ImmutableList { + return persistentListOf() + } + private fun onAddCustomToken() { // TODO: [REDACTED_JIRA] } @@ -95,149 +196,35 @@ internal class ManageTokensModel @Inject constructor( // TODO: [REDACTED_JIRA] } - @Suppress("UnusedPrivateMember") private fun searchCurrencies(query: String) { - // TODO: [REDACTED_JIRA] - val newItems = if (query.isBlank()) { - initItems() - } else { - state.value.items.filter { currency -> - currency.model.name.contains(query, ignoreCase = true) - }.toPersistentList() - } state.update { state -> - state.copySealed(search = state.search.copy(query = query), items = newItems) + state.copySealed( + search = state.search.copy( + query = query, + isActive = true, + ), + ) + } + + modelScope.launch { + manageTokensListManager.search(params.userWalletId, query) } } private fun toggleSearchBar(isActive: Boolean) { state.update { state -> state.copySealed( - search = state.search.copy(isActive = isActive), + search = state.search.copy( + query = if (isActive) state.search.query else "", + isActive = isActive, + ), ) } - } - private fun initItems() = List(size = 30) { index -> - if (index < 2) { - getCustomItem(index) - } else { - getBasicItem(index) - } - }.toPersistentList() - - private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( - id = index.toString(), - model = ChainRowUM( - name = "Custom token $index", - type = "CT$index", - icon = CurrencyIconState.CustomTokenIcon( - tint = Color.White, - background = Color.Black, - topBadgeIconResId = R.drawable.img_eth_22, - isGrayscale = false, - showCustomBadge = true, - ), - showCustom = true, - ), - onRemoveClick = {}, - ) - - private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( - id = index.toString(), - model = ChainRowUM( - name = "Currency $index", - type = "C$index", - icon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_btc_22, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, - ), - networks = if (index == 2) { - CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) - } else { - CurrencyItemUM.Basic.NetworksUM.Collapsed - }, - onExpandClick = { toggleCurrency(index) }, - ) - - private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> - CurrencyNetworkUM( - id = Network.ID(networkIndex.toString()), - name = "NETWORK$networkIndex", - type = "N$networkIndex", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = networkIndex == 0, - isSelected = false, - onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, - ) - }.toImmutableList() - - private fun toggleCurrency(index: Int) { - val updatedItem = when (val item = items[index]) { - is CurrencyItemUM.Basic -> item.copy( - networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) { - CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) - } else { - CurrencyItemUM.Basic.NetworksUM.Collapsed - }, - ) - is CurrencyItemUM.Custom -> return - } - - state.update { state -> - items = items.mutate { - it[index] = updatedItem + modelScope.launch { + if (!isActive) { + manageTokensListManager.reload(params.userWalletId) } - state.copySealed(items = items) - } - } - - private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) { - val updatedItem = when (val item = items[currencyIndex]) { - is CurrencyItemUM.Basic -> { - val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) - ?.copy( - networks = item.networks.networks.toPersistentList().mutate { - it.fastForEachIndexed { index, network -> - if (index == networkIndex) { - it[index] = network.copy( - iconResId = if (isSelected) { - R.drawable.img_eth_22 - } else { - R.drawable.ic_eth_16 - }, - isSelected = isSelected, - ) - } - } - }, - ) - ?: return - - item.copy(networks = updatedNetworks) - } - is CurrencyItemUM.Custom -> return - } - - val id = "${currencyIndex}_$networkIndex" - if (changedItemsIds.contains(id)) { - changedItemsIds.remove(id) - } else { - changedItemsIds.add(id) - } - - state.update { state -> - items = items.mutate { - it[currencyIndex] = updatedItem - } - state.copySealed( - items = items, - hasChanges = changedItemsIds.isNotEmpty(), - ) } } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt new file mode 100644 index 0000000000..c991cf95bb --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt @@ -0,0 +1,46 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal typealias ChangedCurrencies = Map> + +internal interface ChangedCurrenciesManager { + + val currenciesToAdd: MutableStateFlow + val currenciesToRemove: MutableStateFlow + + fun updateChangedItems( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + removeFromIfPresent: MutableStateFlow, + addToIfNotPresent: MutableStateFlow, + ) { + val present = removeFromIfPresent.value[currencyId].orEmpty() + + if (networkId in present) { + removeFromIfPresent.update { items -> + items.toMutableMap().apply { + val ids = present - networkId + + if (ids.isEmpty()) { + remove(currencyId) + } else { + set(currencyId, ids) + } + } + } + } else { + addToIfNotPresent.update { items -> + val alreadyAdded = items[currencyId] ?: emptySet() + if (networkId in alreadyAdded) { + return@update items + } + + items + (currencyId to alreadyAdded + networkId) + } + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt new file mode 100644 index 0000000000..ebf3369bce --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -0,0 +1,206 @@ +package com.tangem.features.managetokens.utils.list + +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.CheckHasLinkedTokensUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.impl.R +import com.tangem.pagination.BatchAction +import com.tangem.pagination.BatchListState +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class ManageTokensListManager @Inject constructor( + private val getManagedTokensUseCase: GetManagedTokensUseCase, + private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, + private val messageSender: UiMessageSender, + private val dispatchers: CoroutineDispatcherProvider, +) : ChangedCurrenciesManager, + ManageTokensUiManager( + messageSender = messageSender, + dispatchers = dispatchers, + ) { + + override lateinit var scope: CoroutineScope + + private val jobHolder = JobHolder() + private val actionsFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + override val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) + + val paginationStatus: MutableStateFlow> = MutableStateFlow(PaginationStatus.None) + + override val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) + override val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) + + @OptIn(ExperimentalCoroutinesApi::class) + val uiItems: Flow> = state + .mapLatest { state -> + state.uiBatches.asSequence() + .flatMap { it.data } + .toImmutableList() + } + .distinctUntilChanged() + + suspend fun launch(userWalletId: UserWalletId?) = coroutineScope { + scope = this + + val batchFlow = getManagedTokensUseCase( + context = ManageTokensListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = this, + ), + ) + + batchFlow.state + .onEach { state -> updateState(state, userWalletId) } + .flowOn(dispatchers.default) + .launchIn(scope = this) + .saveIn(jobHolder) + + // Initial load + reload(userWalletId) + } + + suspend fun reload(userWalletId: UserWalletId?) { + actionsFlow.emit( + BatchAction.Reload( + requestParams = ManageTokensListConfig(userWalletId, searchText = null), + ), + ) + } + + suspend fun loadMore(userWalletId: UserWalletId?, query: String) { + actionsFlow.emit( + BatchAction.LoadMore( + requestParams = ManageTokensListConfig(userWalletId, query), + ), + ) + } + + suspend fun search(userWalletId: UserWalletId?, query: String) { + state.value = ManageTokensListState() + actionsFlow.emit( + BatchAction.Reload( + requestParams = ManageTokensListConfig( + userWalletId = userWalletId, + searchText = query, + ), + ), + ) + } + + private fun updateState( + batchListState: BatchListState>, + userWalletId: UserWalletId?, + ) { + paginationStatus.value = batchListState.status + + state.update { state -> + val newBatches = batchListState.data + val currentBatches = state.currencyBatches + + // Distinct until changed + if (newBatches.size == currentBatches.size && + newBatches.map { it.key } == currentBatches.map { it.key } && + newBatches.flatMap { it.data } == currentBatches.flatMap { it.data } + ) { + return + } + + val canEditItems = userWalletId != null + state.copy( + userWalletId = userWalletId, + currencyBatches = newBatches, + uiBatches = getUiBatches(newBatches, canEditItems), + canEditItems = canEditItems, + ) + } + } + + override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + + sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true) + } + + override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + + sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false) + } + + override fun checkNeedToShowRemoveNetworkWarning( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + ): Boolean = networkId !in currenciesToRemove.value[currencyId].orEmpty() && + networkId !in currenciesToAdd.value[currencyId].orEmpty() + + private fun sendSelectCurrencyAction( + batchKey: Int, + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + isSelected: Boolean, + ) { + val request = ManageTokensUpdateAction.AddCurrency( + currencyId = currencyId, + networkId = networkId, + isSelected = isSelected, + ) + val action = BatchAction.UpdateBatches( + keys = setOf(batchKey), + async = true, + updateRequest = request, + ) + + actionsFlow.tryEmit(action) + } + + override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean { + return checkHasLinkedTokensUseCase(userWalletId, network).getOrElse { + Timber.e( + it, + """ + Failed to check linked tokens + |- User wallet ID: $userWalletId + |- Network ID: ${network.id} + """.trimIndent(), + ) + + val message = SnackbarMessage( + message = it.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + false + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt new file mode 100644 index 0000000000..20ba2a7e55 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt @@ -0,0 +1,43 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction + +internal typealias ManageTokensBatchAction = BatchAction + +internal data class ManageTokensListState( + val userWalletId: UserWalletId? = null, + val uiBatches: List>> = mutableListOf(), + val currencyBatches: List>> = mutableListOf(), + val canEditItems: Boolean = true, +) { + + fun batchIndexByCurrencyId(currencyId: ManagedCryptoCurrency.ID): Int { + return currencyBatches + .indexOfFirst { batch -> batch.data.any { it.id == currencyId } } + .takeIf { it != -1 } + ?: error("Batch with currency '$currencyId' not found") + } + + fun updateUiBatchesItem( + indexToBatch: Pair>>, + indexToItem: Pair, + ): ManageTokensListState { + val updatedUiBatch = indexToBatch.second.copy( + data = indexToBatch.second.data.toMutableList().apply { + set(indexToItem.first, indexToItem.second) + }, + ) + + return copy( + uiBatches = uiBatches.toMutableList().apply { + set(indexToBatch.first, updatedUiBatch) + }, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt new file mode 100644 index 0000000000..f52bd4970d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -0,0 +1,200 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning +import com.tangem.features.managetokens.ui.dialog.HideTokenWarning +import com.tangem.features.managetokens.utils.mapper.toUiModel +import com.tangem.features.managetokens.utils.ui.toggleExpanded +import com.tangem.features.managetokens.utils.ui.update +import com.tangem.pagination.Batch +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal abstract class ManageTokensUiManager( + private val messageSender: UiMessageSender, + private val dispatchers: CoroutineDispatcherProvider, +) { + + abstract val scope: CoroutineScope + abstract val state: MutableStateFlow + + protected fun getUiBatches( + newCurrencyBatches: List>>, + canEditItems: Boolean, + ): List>> { + val currentUiBatches = state.value.uiBatches + val batches = currentUiBatches.toMutableList() + + newCurrencyBatches.forEach { (key, data) -> + val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key } + + if (indexToUpdate == -1) { + val newBatch = Batch( + key = key, + data = data.map { item -> + item.toUiModel( + isEditable = canEditItems, + onRemoveCustomCurrencyClick = ::removeCustomCurrency, + onExpandNetworksClick = ::toggleCurrencyNetworksVisibility, + ) + }, + ) + + batches.add(newBatch) + } else { + val uiBatchToUpdate = currentUiBatches[indexToUpdate] + + if (uiBatchToUpdate.data == data) { + return@forEach + } + + val currentCurrencyBatches = state.value.currencyBatches + val currencyBatch = currentCurrencyBatches[indexToUpdate] + val updatedBatch = uiBatchToUpdate.copy( + data = data.mapIndexed { index, item -> + if (item == currencyBatch.data[index]) { + return@mapIndexed uiBatchToUpdate.data[index] + } + + val previousUiItem = uiBatchToUpdate.data.getOrNull(index) + if (previousUiItem == null || previousUiItem.id != item.id) { + item.toUiModel( + isEditable = canEditItems, + onRemoveCustomCurrencyClick = ::removeCustomCurrency, + onExpandNetworksClick = ::toggleCurrencyNetworksVisibility, + ) + } else { + previousUiItem.update(item) + } + }, + ) + + batches[indexToUpdate] = updatedBatch + } + } + + return batches + } + + private fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) = scope.launch(dispatchers.default) { + showRemoveNetworkWarning( + currency = currency, + network = currency.network, + isCoin = currency is ManagedCryptoCurrency.Custom.Coin, + onConfirm = { + // TODO: [REDACTED_JIRA] + }, + ) + } + + private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch( + dispatchers.default, + ) { + state.update { batches -> + val batchIndex = batches.batchIndexByCurrencyId(currency.id) + val currencyBatch = batches.currencyBatches[batchIndex] + val currencyIndex = currencyBatch.currencyIndexById(currency.id) + + val uiBatch = batches.uiBatches[batchIndex] + val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded( + currency = currencyBatch.data[currencyIndex], + isEditable = batches.canEditItems, + onSelectCurrencyNetwork = { networkId, isSelected -> + selectNetwork(currencyBatch.key, currency, networkId, isSelected) + }, + ) + + batches.updateUiBatchesItem( + indexToBatch = batchIndex to uiBatch, + indexToItem = currencyIndex to updatedUiItem, + ) + } + } + + private fun selectNetwork( + batchKey: Int, + currency: ManagedCryptoCurrency, + source: ManagedCryptoCurrency.SourceNetwork, + isSelected: Boolean, + ) = scope.launch(dispatchers.default) { + if (currency !is ManagedCryptoCurrency.Token) return@launch + + if (isSelected) { + addCurrency(batchKey, currency.id, source.id) + } else { + if (checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { + showRemoveNetworkWarning( + currency = currency, + network = source.network, + isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, + onConfirm = { + removeCurrency(batchKey, currency.id, source.id) + }, + ) + } else { + removeCurrency(batchKey, currency.id, source.id) + } + } + } + + protected abstract fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + protected abstract fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + protected abstract fun checkNeedToShowRemoveNetworkWarning( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + ): Boolean + + private suspend fun showRemoveNetworkWarning( + currency: ManagedCryptoCurrency, + network: Network, + isCoin: Boolean, + onConfirm: () -> Unit, + ) { + val userWalletId = state.value.userWalletId + val hasLinkedTokens = if (userWalletId == null || !isCoin) { + false + } else { + checkHasLinkedTokens(userWalletId, network) + } + + val message = ContentMessage { onDismiss -> + if (hasLinkedTokens) { + HasLinkedTokensWarning( + currency = currency, + network = network, + onDismiss = onDismiss, + ) + } else { + HideTokenWarning( + currency = currency, + onConfirm = { + onConfirm() + onDismiss() + }, + onDismiss = onDismiss, + ) + } + } + + messageSender.send(message) + } + + protected abstract suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean + + private fun Batch>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int { + return data + .indexOfFirst { it.id == id } + .takeIf { it != -1 } + ?: error("Currency with currency '$id' not found in batch #$key") + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt new file mode 100644 index 0000000000..f2bbfb18be --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt @@ -0,0 +1,77 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.utils.ui.getIconRes + +internal fun ManagedCryptoCurrency.toUiModel( + isEditable: Boolean, + onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit, + onRemoveCustomCurrencyClick: (ManagedCryptoCurrency.Custom) -> Unit, +): CurrencyItemUM = when (this) { + is ManagedCryptoCurrency.Custom -> toUiModel(onRemoveCustomCurrencyClick) + is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onExpandNetworksClick) +} + +private fun ManagedCryptoCurrency.Custom.toUiModel( + onRemoveCustomCurrency: (ManagedCryptoCurrency.Custom) -> Unit, +): CurrencyItemUM = CurrencyItemUM.Custom( + id = id, + name = name, + symbol = symbol, + icon = when (this) { + is ManagedCryptoCurrency.Custom.Coin -> { + CurrencyIconState.CoinIcon( + url = iconUrl, + fallbackResId = network.id.getIconRes(isColored = true), + isGrayscale = false, + showCustomBadge = true, + ) + } + is ManagedCryptoCurrency.Custom.Token -> { + val background = tryGetBackgroundForTokenIcon(contractAddress) + + CurrencyIconState.TokenIcon( + url = iconUrl, + fallbackBackground = background, + fallbackTint = getTintForTokenIcon(background), + topBadgeIconResId = network.id.getIconRes(isColored = true), + isGrayscale = false, + showCustomBadge = true, + ) + } + }, + onRemoveClick = { + onRemoveCustomCurrency(this) + }, +) + +private fun ManagedCryptoCurrency.Token.toUiModel( + isEditable: Boolean, + onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit, +): CurrencyItemUM { + val background = TangemColorPalette.Black + + return CurrencyItemUM.Basic( + id = id, + name = name, + symbol = symbol, + icon = CurrencyIconState.TokenIcon( + url = iconUrl, + topBadgeIconResId = null, + isGrayscale = if (isEditable) !isAdded else false, + showCustomBadge = false, + fallbackTint = getTintForTokenIcon(background), + fallbackBackground = background, + ), + networks = NetworksUM.Collapsed, + onExpandClick = { + onExpandNetworksClick(this) + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt new file mode 100644 index 0000000000..7b210d5b19 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -0,0 +1,46 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.utils.ui.getIconRes +import kotlinx.collections.immutable.toImmutableList + +internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( + isExpanded: Boolean, + isItemsEditable: Boolean, + onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, +): NetworksUM { + return if (isExpanded) { + NetworksUM.Expanded( + networks = availableNetworks.map { + it.toUiModel( + isSelected = it.id in addedIn, + isEditable = isItemsEditable, + onSelectedStateChange = onSelectedStateChange, + ) + }.toImmutableList(), + ) + } else { + NetworksUM.Collapsed + } +} + +private fun SourceNetwork.toUiModel( + isSelected: Boolean, + isEditable: Boolean, + onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, +): CurrencyNetworkUM { + return CurrencyNetworkUM( + id = id, + name = network.name.uppercase(), + iconResId = id.getIconRes(isColored = isSelected || !isEditable), + isSelected = isSelected || !isEditable, + type = typeName, + isMainNetwork = this is SourceNetwork.Main, + onSelectedStateChange = { selected -> + onSelectedStateChange(this, selected) + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt new file mode 100644 index 0000000000..20e1c72a6e --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt @@ -0,0 +1,66 @@ +package com.tangem.features.managetokens.utils.ui + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.utils.mapper.toUiNetworksModel +import kotlinx.collections.immutable.toImmutableList + +internal fun CurrencyItemUM.toggleExpanded( + currency: ManagedCryptoCurrency, + isEditable: Boolean, + onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit, +): CurrencyItemUM { + if (currency !is ManagedCryptoCurrency.Token) return this + + return when (this) { + is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Basic -> { + val isExpanded = networks !is NetworksUM.Expanded + + copy( + icon = icon.copySealed( + isGrayscale = if (isEditable) !currency.isAdded && !isExpanded else false, + ), + networks = currency.toUiNetworksModel( + isExpanded = isExpanded, + isItemsEditable = isEditable, + onSelectedStateChange = onSelectCurrencyNetwork, + ), + ) + } + } +} + +internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyItemUM { + return when (this) { + is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Basic -> { + if (currency !is ManagedCryptoCurrency.Token) { + return this + } + + copy( + icon = icon.copySealed( + isGrayscale = networks is NetworksUM.Collapsed && !currency.isAdded, + ), + networks = updateNetworks(currency), + ) + } + } +} + +private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency.Token): NetworksUM = when (networks) { + is NetworksUM.Collapsed -> networks + is NetworksUM.Expanded -> networks.copy( + networks = networks.networks.map { network -> + val isSelected = network.id in currency.addedIn + + network.copy( + iconResId = network.id.getIconRes(isSelected), + isSelected = isSelected, + ) + }.toImmutableList(), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt new file mode 100644 index 0000000000..290c1a756f --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt @@ -0,0 +1,21 @@ +package com.tangem.features.managetokens.utils.ui + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.entity.CurrencyNetworkUM + +internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM { + return copy( + iconResId = id.getIconRes(isSelected), + isSelected = isSelected, + ) +} + +@DrawableRes +internal fun Network.ID.getIconRes(isColored: Boolean): Int = if (isColored) { + getActiveIconRes(value) +} else { + getGreyedOutIconRes(value) +} \ No newline at end of file From fe09323d6459bfb14cf5098ecf740fd89a107a77 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 13:09:15 +0400 Subject: [PATCH 13/41] Updated on 2026-08-14 --- .../managetokens/model/ManageTokensModel.kt | 13 ++--- .../managetokens/ui/ManageTokensScreen.kt | 18 +++---- .../utils/list/ChangedCurrenciesManager.kt | 21 ++++++-- .../utils/list/ManageTokensListManager.kt | 53 ++++++++++--------- .../utils/list/ManageTokensListState.kt | 2 + .../utils/list/ManageTokensUiActions.kt | 16 ++++++ .../utils/list/ManageTokensUiManager.kt | 49 +++++++++-------- 7 files changed, 103 insertions(+), 69 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 2d2ce96fe0..c56ae2b5b9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -55,7 +55,7 @@ internal class ManageTokensModel @Inject constructor( ).launchIn(modelScope) modelScope.launch { - manageTokensListManager.launch(params.userWalletId) + manageTokensListManager.launchPagination(params.userWalletId) } } @@ -110,7 +110,7 @@ internal class ManageTokensModel @Inject constructor( onActiveChange = ::toggleSearchBar, ), hasChanges = false, - saveChanges = ::onSaveClick, + saveChanges = ::saveChanges, loadMore = ::loadMoreItems, ) } @@ -126,7 +126,9 @@ internal class ManageTokensModel @Inject constructor( private fun updatePaginationStatus(status: PaginationStatus<*>) { state.update { state -> when (status) { - is PaginationStatus.InitialLoading -> { + is PaginationStatus.None, + is PaginationStatus.InitialLoading, + -> { if (state.search.isActive) { state } else { @@ -151,7 +153,6 @@ internal class ManageTokensModel @Inject constructor( isNextBatchLoading = false, ) } - is PaginationStatus.None, is PaginationStatus.Paginating, is PaginationStatus.EndOfPagination, -> state.copySealed( @@ -172,7 +173,7 @@ internal class ManageTokensModel @Inject constructor( private fun loadMoreItems(): Boolean { val state = state.value - if (state.isInitialBatchLoading) return false + if (state.isInitialBatchLoading || state.isNextBatchLoading) return false modelScope.launch { manageTokensListManager.loadMore( @@ -192,7 +193,7 @@ internal class ManageTokensModel @Inject constructor( // TODO: [REDACTED_JIRA] } - private fun onSaveClick() { + private fun saveChanges() { // TODO: [REDACTED_JIRA] } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 00a647c307..2104369a82 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -3,7 +3,6 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -173,7 +172,6 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun Currencies( items: ImmutableList, @@ -215,15 +213,13 @@ private fun Currencies( } } - item(key = "loading_indicator") { - AnimatedVisibility( - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - visible = showLoadingItem, - label = "loading_indicator_visibility", - ) { - ProgressIndicator() + if (showLoadingItem) { + item(key = "loading_item") { + ProgressIndicator( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt index c991cf95bb..3e2705f9e6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt @@ -7,12 +7,25 @@ import kotlinx.coroutines.flow.update internal typealias ChangedCurrencies = Map> -internal interface ChangedCurrenciesManager { +internal class ChangedCurrenciesManager { - val currenciesToAdd: MutableStateFlow - val currenciesToRemove: MutableStateFlow + val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) + val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) - fun updateChangedItems( + fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + } + + fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + } + + fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean { + return networkId in currenciesToAdd.value[currencyId].orEmpty() || + networkId in currenciesToRemove.value[currencyId].orEmpty() + } + + private fun updateChangedItems( currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID, removeFromIfPresent: MutableStateFlow, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index ebf3369bce..df07878c62 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -19,11 +19,11 @@ import com.tangem.features.managetokens.impl.R import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchListState import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow @@ -38,37 +38,37 @@ internal class ManageTokensListManager @Inject constructor( private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, -) : ChangedCurrenciesManager, - ManageTokensUiManager( - messageSender = messageSender, - dispatchers = dispatchers, - ) { +) : ManageTokensUiActions { - override lateinit var scope: CoroutineScope + private lateinit var scope: CoroutineScope private val jobHolder = JobHolder() - private val actionsFlow = MutableSharedFlow( + private val actionsFlow: MutableSharedFlow = MutableSharedFlow( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - override val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) + private val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) - val paginationStatus: MutableStateFlow> = MutableStateFlow(PaginationStatus.None) + private val changedCurrenciesManager = ChangedCurrenciesManager() + private val uiManager = ManageTokensUiManager( + state = state, + messageSender = messageSender, + dispatchers = dispatchers, + actions = this, + scopeProvider = Provider { scope }, + ) - override val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) - override val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) + val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd + val currenciesToRemove: StateFlow = changedCurrenciesManager.currenciesToRemove @OptIn(ExperimentalCoroutinesApi::class) - val uiItems: Flow> = state - .mapLatest { state -> - state.uiBatches.asSequence() - .flatMap { it.data } - .toImmutableList() - } + val paginationStatus: Flow> = state + .mapLatest { it.status } .distinctUntilChanged() + val uiItems: Flow> = uiManager.items - suspend fun launch(userWalletId: UserWalletId?) = coroutineScope { + suspend fun launchPagination(userWalletId: UserWalletId?) = coroutineScope { scope = this val batchFlow = getManagedTokensUseCase( @@ -120,7 +120,11 @@ internal class ManageTokensListManager @Inject constructor( batchListState: BatchListState>, userWalletId: UserWalletId?, ) { - paginationStatus.value = batchListState.status + state.update { state -> + state.copy( + status = batchListState.status, + ) + } state.update { state -> val newBatches = batchListState.data @@ -138,20 +142,20 @@ internal class ManageTokensListManager @Inject constructor( state.copy( userWalletId = userWalletId, currencyBatches = newBatches, - uiBatches = getUiBatches(newBatches, canEditItems), + uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), canEditItems = canEditItems, ) } } override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + changedCurrenciesManager.addCurrency(currencyId, networkId) sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true) } override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + changedCurrenciesManager.removeCurrency(currencyId, networkId) sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false) } @@ -159,8 +163,7 @@ internal class ManageTokensListManager @Inject constructor( override fun checkNeedToShowRemoveNetworkWarning( currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID, - ): Boolean = networkId !in currenciesToRemove.value[currencyId].orEmpty() && - networkId !in currenciesToAdd.value[currencyId].orEmpty() + ): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId) private fun sendSelectCurrencyAction( batchKey: Int, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt index 20ba2a7e55..ac6732fd19 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt @@ -7,10 +7,12 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.pagination.Batch import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus internal typealias ManageTokensBatchAction = BatchAction internal data class ManageTokensListState( + val status: PaginationStatus<*> = PaginationStatus.None, val userWalletId: UserWalletId? = null, val uiBatches: List>> = mutableListOf(), val currencyBatches: List>> = mutableListOf(), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt new file mode 100644 index 0000000000..9d73ea1e76 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -0,0 +1,16 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +internal interface ManageTokensUiActions { + + fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean + + suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index f52bd4970d..ffb78bccf2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -4,7 +4,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning import com.tangem.features.managetokens.ui.dialog.HideTokenWarning @@ -12,21 +11,36 @@ import com.tangem.features.managetokens.utils.mapper.toUiModel import com.tangem.features.managetokens.utils.ui.toggleExpanded import com.tangem.features.managetokens.utils.ui.update import com.tangem.pagination.Batch +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -internal abstract class ManageTokensUiManager( +internal class ManageTokensUiManager( + private val state: MutableStateFlow, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, + private val scopeProvider: Provider, + private val actions: ManageTokensUiActions, ) { - abstract val scope: CoroutineScope - abstract val state: MutableStateFlow + private val scope: CoroutineScope + get() = scopeProvider() - protected fun getUiBatches( + @OptIn(ExperimentalCoroutinesApi::class) + val items: Flow> = state + .mapLatest { state -> + state.uiBatches.asSequence() + .flatMap { it.data } + .toImmutableList() + } + .distinctUntilChanged() + + fun createOrUpdateUiBatches( newCurrencyBatches: List>>, canEditItems: Boolean, ): List>> { @@ -128,32 +142,23 @@ internal abstract class ManageTokensUiManager( if (currency !is ManagedCryptoCurrency.Token) return@launch if (isSelected) { - addCurrency(batchKey, currency.id, source.id) + actions.addCurrency(batchKey, currency.id, source.id) } else { - if (checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { + if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { showRemoveNetworkWarning( currency = currency, network = source.network, isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, onConfirm = { - removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency.id, source.id) }, ) } else { - removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency.id, source.id) } } } - protected abstract fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) - - protected abstract fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) - - protected abstract fun checkNeedToShowRemoveNetworkWarning( - currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, - ): Boolean - private suspend fun showRemoveNetworkWarning( currency: ManagedCryptoCurrency, network: Network, @@ -164,7 +169,7 @@ internal abstract class ManageTokensUiManager( val hasLinkedTokens = if (userWalletId == null || !isCoin) { false } else { - checkHasLinkedTokens(userWalletId, network) + actions.checkHasLinkedTokens(userWalletId, network) } val message = ContentMessage { onDismiss -> @@ -189,8 +194,6 @@ internal abstract class ManageTokensUiManager( messageSender.send(message) } - protected abstract suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean - private fun Batch>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int { return data .indexOfFirst { it.id == id } From fd7ac00fd43c2b3c86bed31c3053059868e9511e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 20:08:13 +0400 Subject: [PATCH 14/41] Updated on 2026-08-14 --- .../tangem/domain/common/CardTypesResolver.kt | 46 ---- .../domain/common/TangemCardTypesResolver.kt | 79 ------ features/wallet/impl/build.gradle.kts | 4 + .../presentation/wallet/domain/NoteImage.kt | 31 +++ .../wallet/domain/Wallet2CobrandImage.kt | 161 ++++++++++++ .../wallet/domain/WalletImageResolver.kt | 232 ++---------------- ...6.webp => ill_trillant_card2_120_106.webp} | Bin ...6.webp => ill_trillant_card3_120_106.webp} | Bin .../wallet/domain/NoteImageTest.kt | 26 ++ .../wallet/domain/Wallet2CobrandImageTest.kt | 22 ++ 10 files changed, 268 insertions(+), 333 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt rename features/wallet/impl/src/main/res/drawable/{ill_traillant_card2_120_106.webp => ill_trillant_card2_120_106.webp} (100%) rename features/wallet/impl/src/main/res/drawable/{ill_traillant_card3_120_106.webp => ill_trillant_card3_120_106.webp} (100%) create mode 100644 features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt create mode 100644 features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 55cc073069..29ce08c107 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -10,54 +10,8 @@ interface CardTypesResolver { fun isTangemWallet(): Boolean - fun isWhiteWallet2(): Boolean - - fun isAvroraWallet(): Boolean - - fun isTraillantWallet(): Boolean - fun isShibaWallet(): Boolean - fun isTronWallet(): Boolean - - fun isKaspaWallet(): Boolean - - fun isKaspa2Wallet(): Boolean - - fun isKaspaResellerWallet(): Boolean - - fun isBadWallet(): Boolean - - fun isJrWallet(): Boolean - - fun isGrimWallet(): Boolean - - fun isSatoshiFriendsWallet(): Boolean - - fun isBitcoinPizzaDayWallet(): Boolean - - fun isVeChainWallet(): Boolean - - fun isNewWorldEliteWallet(): Boolean - - fun isRedPandaWallet(): Boolean - - fun isCryptoSethWallet(): Boolean - - fun isKishuInuWallet(): Boolean - - fun isBabyDogeWallet(): Boolean - - fun isCOQWallet(): Boolean - - fun isCoinMetricaWallet(): Boolean - - fun isVoltInuWallet(): Boolean - - fun isVividWallet(): Boolean - - fun isPastelWallet(): Boolean - fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 6a4afdcfd9..85f6165a23 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -26,60 +26,10 @@ internal class TangemCardTypesResolver( card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable } - override fun isWhiteWallet2(): Boolean = card.batchId == WHITE_WALLET2_BATCH_ID - - override fun isAvroraWallet(): Boolean = card.batchId == AVRORA_WALLET_BATCH_ID - - override fun isTraillantWallet(): Boolean = card.batchId == TRILLIANT_WALLET_BATCH_ID - override fun isShibaWallet(): Boolean { return card.firmwareVersion.compareTo(FirmwareVersion.KeysImportAvailable) == 0 } - override fun isTronWallet(): Boolean = card.batchId == TRON_WALLET_BATCH_ID - - override fun isKaspaWallet(): Boolean = card.batchId == KASPA_WALLET_BATCH_ID - - override fun isKaspa2Wallet(): Boolean = card.batchId == KASPA2_WALLET_BATCH_ID - - override fun isKaspaResellerWallet(): Boolean = card.batchId == KASPA_RESELLER_WALLET_BATCH_ID - - override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID - - override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID - - override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID - - override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID - - override fun isBitcoinPizzaDayWallet(): Boolean = card.batchId == BITCOIN_PIZZA_DAY_WALLET_BATCH_ID - - override fun isVeChainWallet(): Boolean = card.batchId == VECHAIN_WALLET_BATCH_ID - - override fun isNewWorldEliteWallet(): Boolean = card.batchId == NEW_WORLD_ELITE_WALLET_BATCH_ID - - override fun isRedPandaWallet(): Boolean = card.batchId == RED_PANDA_WALLET_BATCH_ID - - override fun isCryptoSethWallet(): Boolean = card.batchId == CRYPTO_SETH_WALLET_BATCH_ID - - override fun isKishuInuWallet(): Boolean = card.batchId == KISHU_INU_WALLET_BATCH_ID - - override fun isBabyDogeWallet(): Boolean = card.batchId == BABY_DOGE_WALLET_BATCH_ID - - override fun isCOQWallet(): Boolean = card.batchId == COQ_WALLET_BATCH_ID - - override fun isCoinMetricaWallet(): Boolean = card.batchId == COIN_METRICA_WALLET_BATCH_ID - - override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID - - override fun isVividWallet(): Boolean = card.batchId == VIVID_LEMON_WALLET_BATCH_ID || - card.batchId == VIVID_AQUA_WALLET_BATCH_ID || - card.batchId == VIVID_GRAPEFRUIT_WALLET_BATCH_ID - - override fun isPastelWallet(): Boolean = card.batchId == PASTEL_PEACH_WALLET_BATCH_ID || - card.batchId == PASTEL_GRASS_WALLET_BATCH_ID || - card.batchId == PASTEL_AIR_WALLET_BATCH_ID - override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -183,34 +133,5 @@ internal class TangemCardTypesResolver( private companion object { const val DEV_KIT_CARD_BATCH_ID = "CB83" - const val TRON_WALLET_BATCH_ID = "AF07" - const val KASPA_WALLET_BATCH_ID = "AF08" - const val KASPA2_WALLET_BATCH_ID = "AF25" - const val KASPA_RESELLER_WALLET_BATCH_ID = "AF31" - const val BAD_WALLET_BATCH_ID = "AF09" - const val JR_WALLET_BATCH_ID = "AF14" - const val GRIM_WALLET_BATCH_ID = "AF13" - const val SATOSHI_WALLET_BATCH_ID = "AF19" - const val WHITE_WALLET2_BATCH_ID = "AF15" - const val TRILLIANT_WALLET_BATCH_ID = "AF16" - const val AVRORA_WALLET_BATCH_ID = "AF18" - const val BITCOIN_PIZZA_DAY_WALLET_BATCH_ID = "AF33" - const val VECHAIN_WALLET_BATCH_ID = "AF29" - const val NEW_WORLD_ELITE_WALLET_BATCH_ID = "AF26" - const val RED_PANDA_WALLET_BATCH_ID = "AF34" - const val CRYPTO_SETH_WALLET_BATCH_ID = "AF32" - const val KISHU_INU_WALLET_BATCH_ID = "AF52" - const val BABY_DOGE_WALLET_BATCH_ID = "AF51" - const val COQ_WALLET_BATCH_ID = "AF28" - const val COIN_METRICA_WALLET_BATCH_ID = "AF27" - const val VOLT_INU_WALLET_BATCH_ID = "AF35" - // VIVID WALLETS - const val VIVID_LEMON_WALLET_BATCH_ID = "AF40" - const val VIVID_AQUA_WALLET_BATCH_ID = "AF41" - const val VIVID_GRAPEFRUIT_WALLET_BATCH_ID = "AF42" - // PASTEL WALLETS - const val PASTEL_PEACH_WALLET_BATCH_ID = "AF43" - const val PASTEL_AIR_WALLET_BATCH_ID = "AF44" - const val PASTEL_GRASS_WALLET_BATCH_ID = "AF45" } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 9d6ff75448..4cdbb44b40 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -92,4 +92,8 @@ dependencies { implementation(projects.features.details.api) implementation(projects.features.pushNotifications.api) implementation(projects.features.markets.api) + + /** Test libraries */ + implementation(deps.test.junit) + implementation(deps.test.truth) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt new file mode 100644 index 0000000000..44d5b5b268 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import androidx.annotation.DrawableRes +import com.tangem.blockchain.common.Blockchain +import com.tangem.feature.wallet.impl.R + +/** + * Note image model + * + * @property blockchain blockchain + * @property imageResId image res id + * +[REDACTED_AUTHOR] + */ +internal enum class NoteImage( + val blockchain: Blockchain, + @DrawableRes val imageResId: Int, +) { + + Bitcoin(blockchain = Blockchain.Bitcoin, imageResId = R.drawable.ill_note_btc_120_106), + + Ethereum(blockchain = Blockchain.Ethereum, imageResId = R.drawable.ill_note_ethereum_120_106), + + Binance(blockchain = Blockchain.BSC, imageResId = R.drawable.ill_note_binance_120_106), + + Dogecoin(blockchain = Blockchain.Dogecoin, imageResId = R.drawable.ill_note_doge_120_106), + + Cardano(blockchain = Blockchain.Cardano, imageResId = R.drawable.ill_note_cardano_120_106), + + XRP(blockchain = Blockchain.XRP, imageResId = R.drawable.ill_note_xrp_120_106), +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt new file mode 100644 index 0000000000..82d07f7000 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import androidx.annotation.DrawableRes +import com.tangem.feature.wallet.impl.R + +/** + * Wallet2 cobrand image. + * To integrate a new cobrand, just implement new enum object and that all. + * + * @property cards2ResId image resource id for wallet set with 2 cards + * @property cards3ResId image resource id for wallet set with 3 cards + * @property batchIds set of unique batch ids for this cobrand + * +[REDACTED_AUTHOR] + */ +internal enum class Wallet2CobrandImage( + @DrawableRes val cards2ResId: Int, + @DrawableRes val cards3ResId: Int, + val batchIds: Set, +) { + + Avrora( + cards2ResId = R.drawable.ill_avrora_card2_120_106, + cards3ResId = R.drawable.ill_avrora_card3_120_106, + batchIds = setOf("AF18"), + ), + + BabyDoge( + cards2ResId = R.drawable.ill_baby_doge_card2_120_106, + cards3ResId = R.drawable.ill_baby_doge_card3_120_106, + batchIds = setOf("AF51"), + ), + + Bad( + cards2ResId = R.drawable.ill_bad_card2_120_106, + cards3ResId = R.drawable.ill_bad_card3_120_106, + batchIds = setOf("AF09"), + ), + + BitcoinPizzaDay( + cards2ResId = R.drawable.ill_pizza_day_card2_120_106, + cards3ResId = R.drawable.ill_pizza_day_card3_120_106, + batchIds = setOf("AF33"), + ), + + CoinMetrica( + cards2ResId = R.drawable.ill_coin_metrica_card2_120_106, + cards3ResId = R.drawable.ill_coin_metrica_card3_120_106, + batchIds = setOf("AF27"), + ), + + COQ( + cards2ResId = R.drawable.ill_coq_card2_120_106, + cards3ResId = R.drawable.ill_coq_card3_120_106, + batchIds = setOf("AF28"), + ), + + CryptoSeth( + cards2ResId = R.drawable.ill_crypto_seth_card2_120_106, + cards3ResId = R.drawable.ill_crypto_seth_card3_120_106, + batchIds = setOf("AF32"), + ), + + Grim( + cards2ResId = R.drawable.ill_grim_card2_120_106, + cards3ResId = R.drawable.ill_grim_card3_120_106, + batchIds = setOf("AF13"), + ), + + Jr( + cards2ResId = R.drawable.ill_jr_card2_120_106, + cards3ResId = R.drawable.ill_jr_card3_120_106, + batchIds = setOf("AF14"), + ), + + Kaspa( + cards2ResId = R.drawable.ill_kaspa_card2_120_106, + cards3ResId = R.drawable.ill_kaspa_card3_120_106, + batchIds = setOf("AF08"), + ), + + Kaspa2( + cards2ResId = R.drawable.ill_kaspa2_card2_120_106, + cards3ResId = R.drawable.ill_kaspa2_card3_120_106, + batchIds = setOf("AF25"), + ), + + KaspaReseller( + cards2ResId = R.drawable.ill_kaspa_reseller_card2_120_106, + cards3ResId = R.drawable.ill_kaspa_reseller_card3_120_106, + batchIds = setOf("AF31"), + ), + + KishuInu( + cards2ResId = R.drawable.ill_kishu_inu_card2_120_106, + cards3ResId = R.drawable.ill_kishu_inu_card3_120_106, + batchIds = setOf("AF52"), + ), + + NewWorldElite( + cards2ResId = R.drawable.ill_nwe_card2_120_106, + cards3ResId = R.drawable.ill_nwe_card3_120_106, + batchIds = setOf("AF26"), + ), + + // for multicolored cards use image of 3 cards in all cases + Pastel( + cards2ResId = R.drawable.ill_pastel_cards3_120_106, + cards3ResId = R.drawable.ill_pastel_cards3_120_106, + batchIds = setOf("AF43", "AF44", "AF45"), + ), + + RedPanda( + cards2ResId = R.drawable.ill_red_panda_card2_120_106, + cards3ResId = R.drawable.ill_red_panda_card3_120_106, + batchIds = setOf("AF34"), + ), + + SatoshiFriends( + cards2ResId = R.drawable.ill_satoshi_card2_120_106, + cards3ResId = R.drawable.ill_satoshi_card3_120_106, + batchIds = setOf("AF19"), + ), + + Trillant( + cards2ResId = R.drawable.ill_trillant_card2_120_106, + cards3ResId = R.drawable.ill_trillant_card3_120_106, + batchIds = setOf("AF16"), + ), + + Tron( + cards2ResId = R.drawable.ill_tron_card2_120_106, + cards3ResId = R.drawable.ill_tron_card3_120_106, + batchIds = setOf("AF07"), + ), + + VeChain( + cards2ResId = R.drawable.ill_vechain_card2_120_106, + cards3ResId = R.drawable.ill_vechain_card3_120_106, + batchIds = setOf("AF29"), + ), + + // for multicolored cards use image of 3 cards in all cases + Vivid( + cards2ResId = R.drawable.ill_vivid_cards3_120_106, + cards3ResId = R.drawable.ill_vivid_cards3_120_106, + batchIds = setOf("AF40", "AF41", "AF42"), + ), + + VoltInu( + cards2ResId = R.drawable.ill_volt_inu_card2_120_106, + cards3ResId = R.drawable.ill_volt_inu_card3_120_106, + batchIds = setOf("AF35"), + ), + + WhiteTangem( + cards2ResId = R.drawable.ill_white_card2_120_106, + cards3ResId = R.drawable.ill_white_card3_120_106, + batchIds = setOf("AF15"), + ), +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 2bd5ddb371..711cdaac37 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain import androidx.annotation.DrawableRes -import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.demo.DemoConfig @@ -13,7 +12,6 @@ import com.tangem.feature.wallet.impl.R * [REDACTED_AUTHOR] */ -// TODO: make flexible to integrate cobrands ([REDACTED_JIRA]) internal object WalletImageResolver { private const val WALLET_WITHOUT_BACKUP_COUNT = 1 @@ -25,42 +23,42 @@ internal object WalletImageResolver { @DrawableRes fun resolve(userWallet: UserWallet): Int? { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + + val cobrandImage = Wallet2CobrandImage.entries.firstOrNull { + it.batchIds.contains(userWallet.scanResponse.card.batchId) + } + + val noteImage by lazy { + val noteBlockchain = cardTypesResolver.getBlockchain() + + NoteImage.entries.firstOrNull { it.blockchain == noteBlockchain } + } + return when { cardTypesResolver.isDevKit() -> R.drawable.ill_dev_120_106 - cardTypesResolver.isWhiteWallet2() -> userWallet.resolveWhiteWallet2() - cardTypesResolver.isAvroraWallet() -> userWallet.resolveAvroraWallet() - cardTypesResolver.isTraillantWallet() -> userWallet.resolveTraillantWallet() - cardTypesResolver.isTronWallet() -> userWallet.resolveTronWallet() - cardTypesResolver.isKaspaWallet() -> userWallet.resolveKaspaWallet() - cardTypesResolver.isKaspa2Wallet() -> userWallet.resolveKaspa2Wallet() - cardTypesResolver.isKaspaResellerWallet() -> userWallet.resolveKaspaResellerWallet() - cardTypesResolver.isBadWallet() -> userWallet.resolveBadWallet() - cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() - cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() - cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet() - cardTypesResolver.isBitcoinPizzaDayWallet() -> userWallet.resolveBitcoinPizzaDayWallet() - cardTypesResolver.isVeChainWallet() -> userWallet.resolveVeChainWallet() - cardTypesResolver.isNewWorldEliteWallet() -> userWallet.resolveNewWorldEliteWallet() - cardTypesResolver.isRedPandaWallet() -> userWallet.resolveRedPandaWallet() - cardTypesResolver.isCryptoSethWallet() -> userWallet.resolveCryptoSethWallet() - cardTypesResolver.isKishuInuWallet() -> userWallet.resolveKishuInuWallet() - cardTypesResolver.isBabyDogeWallet() -> userWallet.resolveBabyDogeWallet() - cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet() - cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet() - cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet() - cardTypesResolver.isVividWallet() -> userWallet.resolveVividWallet() - cardTypesResolver.isPastelWallet() -> userWallet.resolvePastelWallet() + cobrandImage != null -> userWallet.resolveWallet2Cobrand(image = cobrandImage) cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() cardTypesResolver.isWhiteWallet() -> R.drawable.ill_wallet_old_white_120_106 cardTypesResolver.isTangemTwins() -> R.drawable.ill_twins_120_106 cardTypesResolver.isStart2Coin() -> R.drawable.ill_start2coin_120_106 - cardTypesResolver.isTangemNote() -> resolveNote(blockchain = cardTypesResolver.getBlockchain()) + cardTypesResolver.isTangemNote() -> noteImage?.imageResId else -> null } } + private fun UserWallet.resolveWallet2Cobrand(image: Wallet2CobrandImage): Int? { + return resolveWallet2(oneBackupResId = image.cards2ResId, twoBackupResId = image.cards3ResId) + } + + private fun UserWallet.resolveShibaWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_shiba_card2_120_106, + twoBackupResId = R.drawable.ill_shiba_card3_120_106, + ) + } + private fun UserWallet.resolveWallet2( @DrawableRes oneBackupResId: Int = R.drawable.ill_wallet2_cards2_120_106, @DrawableRes twoBackupResId: Int = R.drawable.ill_wallet2_cards3_120_106, @@ -76,176 +74,6 @@ internal object WalletImageResolver { } } - private fun UserWallet.resolveTronWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_tron_card2_120_106, - twoBackupResId = R.drawable.ill_tron_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspa2Wallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa2_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa2_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspaResellerWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa_reseller_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa_reseller_card3_120_106, - ) - } - - private fun UserWallet.resolveBadWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_bad_card2_120_106, - twoBackupResId = R.drawable.ill_bad_card3_120_106, - ) - } - - private fun UserWallet.resolveJrWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_jr_card2_120_106, - twoBackupResId = R.drawable.ill_jr_card3_120_106, - ) - } - - private fun UserWallet.resolveGrimWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_grim_card2_120_106, - twoBackupResId = R.drawable.ill_grim_card3_120_106, - ) - } - - private fun UserWallet.resolveSatoshiWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_satoshi_card2_120_106, - twoBackupResId = R.drawable.ill_satoshi_card3_120_106, - ) - } - - private fun UserWallet.resolveShibaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_shiba_card2_120_106, - twoBackupResId = R.drawable.ill_shiba_card3_120_106, - ) - } - - private fun UserWallet.resolveWhiteWallet2(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_white_card2_120_106, - twoBackupResId = R.drawable.ill_white_card3_120_106, - ) - } - - private fun UserWallet.resolveAvroraWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_avrora_card2_120_106, - twoBackupResId = R.drawable.ill_avrora_card3_120_106, - ) - } - - private fun UserWallet.resolveTraillantWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_traillant_card2_120_106, - twoBackupResId = R.drawable.ill_traillant_card3_120_106, - ) - } - - private fun UserWallet.resolveBitcoinPizzaDayWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_pizza_day_card2_120_106, - twoBackupResId = R.drawable.ill_pizza_day_card3_120_106, - ) - } - - private fun UserWallet.resolveVeChainWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_vechain_card2_120_106, - twoBackupResId = R.drawable.ill_vechain_card3_120_106, - ) - } - - private fun UserWallet.resolveNewWorldEliteWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_nwe_card2_120_106, - twoBackupResId = R.drawable.ill_nwe_card3_120_106, - ) - } - - private fun UserWallet.resolveRedPandaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_red_panda_card2_120_106, - twoBackupResId = R.drawable.ill_red_panda_card3_120_106, - ) - } - - private fun UserWallet.resolveCryptoSethWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_crypto_seth_card2_120_106, - twoBackupResId = R.drawable.ill_crypto_seth_card3_120_106, - ) - } - - private fun UserWallet.resolveKishuInuWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kishu_inu_card2_120_106, - twoBackupResId = R.drawable.ill_kishu_inu_card3_120_106, - ) - } - - private fun UserWallet.resolveBabyDogeWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_baby_doge_card2_120_106, - twoBackupResId = R.drawable.ill_baby_doge_card3_120_106, - ) - } - - private fun UserWallet.resolveCOQWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_coq_card2_120_106, - twoBackupResId = R.drawable.ill_coq_card3_120_106, - ) - } - - private fun UserWallet.resolveCoinMetricaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_coin_metrica_card2_120_106, - twoBackupResId = R.drawable.ill_coin_metrica_card3_120_106, - ) - } - - private fun UserWallet.resolveVoltInuWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_volt_inu_card2_120_106, - twoBackupResId = R.drawable.ill_volt_inu_card3_120_106, - ) - } - - private fun UserWallet.resolveVividWallet(): Int? { - // for multicolored cards use image of 3 cards in all cases - return resolveWallet2( - oneBackupResId = R.drawable.ill_vivid_cards3_120_106, - twoBackupResId = R.drawable.ill_vivid_cards3_120_106, - ) - } - - private fun UserWallet.resolvePastelWallet(): Int? { - // for multicolored cards use image of 3 cards in all cases - return resolveWallet2( - oneBackupResId = R.drawable.ill_pastel_cards3_120_106, - twoBackupResId = R.drawable.ill_pastel_cards3_120_106, - ) - } - private fun UserWallet.resolveWallet1(): Int? { return resolveWalletWithBackups { count -> when (count) { @@ -262,16 +90,4 @@ internal object WalletImageResolver { return if (count != null) resolve(count) else null } - - private fun resolveNote(blockchain: Blockchain): Int? { - return when (blockchain) { - Blockchain.Bitcoin -> R.drawable.ill_note_btc_120_106 - Blockchain.Ethereum -> R.drawable.ill_note_ethereum_120_106 - Blockchain.BSC -> R.drawable.ill_note_binance_120_106 - Blockchain.Dogecoin -> R.drawable.ill_note_doge_120_106 - Blockchain.Cardano -> R.drawable.ill_note_cardano_120_106 - Blockchain.XRP -> R.drawable.ill_note_xrp_120_106 - else -> null - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ill_traillant_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_trillant_card2_120_106.webp similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ill_traillant_card2_120_106.webp rename to features/wallet/impl/src/main/res/drawable/ill_trillant_card2_120_106.webp diff --git a/features/wallet/impl/src/main/res/drawable/ill_traillant_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_trillant_card3_120_106.webp similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ill_traillant_card3_120_106.webp rename to features/wallet/impl/src/main/res/drawable/ill_trillant_card3_120_106.webp diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt new file mode 100644 index 0000000000..c6da034569 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NoteImageTest { + + @Test + fun check() { + // check uniqueness + Truth.assertThat(NoteImage.entries.map { it.blockchain }).containsNoDuplicates() + Truth.assertThat(NoteImage.entries.map { it.imageResId }).containsNoDuplicates() + + // check blockchain matching + Truth.assertThat(NoteImage.Bitcoin.blockchain).isEqualTo(Blockchain.Bitcoin) + Truth.assertThat(NoteImage.Ethereum.blockchain).isEqualTo(Blockchain.Ethereum) + Truth.assertThat(NoteImage.Binance.blockchain).isEqualTo(Blockchain.BSC) + Truth.assertThat(NoteImage.Dogecoin.blockchain).isEqualTo(Blockchain.Dogecoin) + Truth.assertThat(NoteImage.Cardano.blockchain).isEqualTo(Blockchain.Cardano) + Truth.assertThat(NoteImage.XRP.blockchain).isEqualTo(Blockchain.XRP) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt new file mode 100644 index 0000000000..13d88a97fd --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.google.common.truth.Truth +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class Wallet2CobrandImageTest { + + @Test + fun checkUniqueness() { + val excluded = listOf(Wallet2CobrandImage.Pastel, Wallet2CobrandImage.Vivid) + (Wallet2CobrandImage.entries - excluded).forEach { + Truth.assertThat(it.cards2ResId != it.cards3ResId).isTrue() + } + + Truth.assertThat(Wallet2CobrandImage.entries.map { it.cards2ResId }).containsNoDuplicates() + Truth.assertThat(Wallet2CobrandImage.entries.map { it.cards3ResId }).containsNoDuplicates() + Truth.assertThat(Wallet2CobrandImage.entries.flatMap { it.batchIds }).containsNoDuplicates() + } +} \ No newline at end of file From ddecdf444dc1b20120f80c339df4afbaa15fe794 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 14:12:58 +0300 Subject: [PATCH 15/41] Updated on 2026-08-14 --- .../models/request/ActionRequestBody.kt | 12 +++++++-- .../tron/TronStakeKitTransaction.kt | 10 +++++++ .../data/staking/DefaultStakingRepository.kt | 27 ++++++++++++++++++- .../data/staking/di/StakingDataModule.kt | 2 ++ gradle/dependencies.toml | 2 +- 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index d6c09517c1..9063ff0b74 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -48,7 +48,7 @@ data class ActionRequestBodyArgs( @Json(name = "ledgerWalletAPICompatible") val ledgerWalletAPICompatible: Boolean? = null, @Json(name = "tronResource") - val tronResource: String? = null, + val tronResource: TronResource? = null, @Json(name = "signatureVerification") val signatureVerification: SignatureVerification? = null, @Json(name = "inputToken") @@ -60,4 +60,12 @@ data class SignatureVerification( val message: String, @Json(name = "signed") val signed: String, -) \ No newline at end of file +) + +enum class TronResource { + @Json(name = "ENERGY") + ENERGY, + + @Json(name = "BANDWIDTH") + BANDWIDTH, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt new file mode 100644 index 0000000000..f12f5fe5ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction.tron + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TronStakeKitTransaction( + @Json(name = "raw_data_hex") + val rawDataHex: String, +) \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 541762c34d..c4b9b8fad1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,10 +2,13 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.raise.catch +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey @@ -22,6 +25,7 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectListSync @@ -66,6 +70,7 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggle: StakingFeatureToggles, private val walletManagersFacade: WalletManagersFacade, + private val moshi: Moshi, ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() @@ -103,6 +108,9 @@ internal class DefaultStakingRepository( value = emptyMap(), ) + private val tronStakeKitTransactionAdapter: JsonAdapter = + moshi.adapter(TronStakeKitTransaction::class.java) + override fun isStakingSupported(integrationKey: String): Boolean { return integrationIdMap.containsKey(integrationKey) } @@ -509,6 +517,8 @@ internal class DefaultStakingRepository( amount = params.amount.toPlainString(), inputToken = tokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, + validatorAddresses = listOf(params.validatorAddress), // check on other networks + tronResource = getTronResource(network), ), ) } @@ -567,6 +577,11 @@ internal class DefaultStakingRepository( -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) Blockchain.Ethereum, -> TransactionData.Compiled.Data.RawString(unsignedTransaction) + Blockchain.Tron -> { + val tronStakeKitTransaction = tronStakeKitTransactionAdapter.fromJson(unsignedTransaction) + ?: error("Failed to parse Tron StakeKit transaction") + TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex) + } else -> error("Unsupported blockchain") } } @@ -598,6 +613,16 @@ internal class DefaultStakingRepository( private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId) + private fun getTronResource(network: Network): TronResource? { + val blockchain = Blockchain.fromNetworkId(network.backendId) + + return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) { + TronResource.ENERGY + } else { + null + } + } + private companion object { const val YIELDS_STORE_KEY = "yields" @@ -622,7 +647,7 @@ internal class DefaultStakingRepository( Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, - // Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, + Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index d261cdece4..32a9a1cdae 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -37,6 +37,7 @@ internal object StakingDataModule { stakingFeatureToggle: StakingFeatureToggles, cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, + @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, @@ -47,6 +48,7 @@ internal object StakingDataModule { cacheRegistry = cacheRegistry, stakingFeatureToggle = stakingFeatureToggle, walletManagersFacade = walletManagersFacade, + moshi = moshi, ) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 74b8993062..dbe4ee3f6d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-736" +tangemBlockchainSdk = "develop-740" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 78e022c1d209eb8c29de5673f40493bafa6841c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 16:35:04 +0500 Subject: [PATCH 16/41] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 2 + .../alerts/SendTransactionAlertConverter.kt | 49 ++++++++++++ .../ui/alerts/models/AlertDemoModeUM.kt | 13 +++ .../alerts/models/AlertTransactionErrorUM.kt | 21 +++++ .../tangem/common/ui/alerts/models/AlertUM.kt | 12 +++ .../transaction/error/SendTransactionError.kt | 4 +- .../usecase/SendTransactionUseCase.kt | 4 +- features/send/impl/build.gradle.kts | 1 + .../state/SendTransactionAlertConverter.kt | 4 +- features/staking/impl/build.gradle.kts | 1 + .../presentation/state/StakingAlertState.kt | 24 ------ .../state/StakingStateController.kt | 12 +++ .../impl/presentation/state/StakingUiState.kt | 1 + .../state/events/StakingAlertUM.kt | 29 +++++++ .../state/{ => events}/StakingEvent.kt | 5 +- .../state/events/StakingEventFactory.kt | 44 +++++++++++ .../state/stub/StakingClickIntentsStub.kt | 3 + .../presentation/ui/StakingEventEffect.kt | 79 +++++++++++++++++++ .../impl/presentation/ui/StakingScreen.kt | 8 ++ .../viewmodel/StakingClickIntents.kt | 2 + .../viewmodel/StakingViewModel.kt | 45 ++++++++--- 21 files changed, 317 insertions(+), 46 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt rename domain/transaction/{src/main/java => models/src/main/kotlin}/com/tangem/domain/transaction/error/SendTransactionError.kt (81%) delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/{ => events}/StakingEvent.kt (54%) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 40a779c416..2d6b9e12d6 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.transaction.models) + implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt new file mode 100644 index 0000000000..2cb03f4f16 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.common.ui.alerts + +import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.utils.converter.Converter + +class SendTransactionAlertConverter( + private val popBackStack: () -> Unit, + private val onFailedTxEmailClick: (String) -> Unit, +) : Converter { + override fun convert(value: SendTransactionError): AlertUM? { + return when (value) { + is SendTransactionError.DemoCardError -> AlertDemoModeUM( + onConfirmClick = popBackStack, + ) + is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM( + code = value.code.toString(), + cause = null, + causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), + onConfirmClick = { onFailedTxEmailClick(value.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM( + code = value.code.toString(), + cause = value.message, + onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> AlertTransactionErrorUM( + code = "", + cause = value.message, + onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> AlertTransactionErrorUM( + code = value.code.orEmpty(), + cause = value.message.orEmpty(), + onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> AlertTransactionErrorUM( + code = "", + cause = value.ex?.localizedMessage, + onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, + ) + else -> null + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt new file mode 100644 index 0000000000..e63ccb0229 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt @@ -0,0 +1,13 @@ +package com.tangem.common.ui.alerts.models + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference + +data class AlertDemoModeUM( + override val onConfirmClick: () -> Unit, +) : AlertUM { + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) + override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt new file mode 100644 index 0000000000..5fd83163be --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt @@ -0,0 +1,21 @@ +package com.tangem.common.ui.alerts.models + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList + +data class AlertTransactionErrorUM( + val code: String, + val cause: String?, + val causeTextReference: TextReference? = null, + override val onConfirmClick: (() -> Unit)? = null, +) : AlertUM { + override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title) + override val message: TextReference = resourceReference( + id = R.string.send_alert_transaction_failed_text, + formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), + ) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_support) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt new file mode 100644 index 0000000000..1cf955bebe --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt @@ -0,0 +1,12 @@ +package com.tangem.common.ui.alerts.models + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +interface AlertUM { + val title: TextReference? + val message: TextReference + val confirmButtonText: TextReference + val onConfirmClick: (() -> Unit)? +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt similarity index 81% rename from domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt rename to domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt index 3f5f7485d4..afa825d759 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -1,7 +1,5 @@ package com.tangem.domain.transaction.error -import com.tangem.core.ui.extensions.TextReference - sealed class SendTransactionError { data object DemoCardError : SendTransactionError() @@ -16,7 +14,7 @@ sealed class SendTransactionError { data class CreateAccountUnderfunded(val amount: String) : SendTransactionError() - data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError() + data class TangemSdkError(val code: Int, val messageRes: Int, val args: List) : SendTransactionError() data class UnknownError(val ex: Exception? = null) : SendTransactionError() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 96b5db01fa..c6b5e9258c 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -10,7 +10,6 @@ import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin @@ -133,8 +132,7 @@ class SendTransactionUseCase( val resource = tangemError.localizedDescriptionRes() val resId = resource.resId ?: R.string.common_unknown_error val resArgs = resource.args.map { it.value } - val textReference = resourceReference(resId, wrappedList(resArgs)) - SendTransactionError.TangemSdkError(tangemError.code, textReference) + SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) } is BlockchainSdkError.WrappedTangemError -> { parseWrappedError(tangemError) // todo remove when sdk errors are revised diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 9a3de6d8cb..14e47173a5 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.card) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt index d37954025b..7d184f5227 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.state +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.converter.Converter @@ -15,7 +17,7 @@ internal class SendTransactionAlertConverter( is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError( code = value.code.toString(), cause = null, - causeTextReference = value.messageReference, + causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) }, ) is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError( diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 15213f79d6..9d23634914 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory) /** Common */ diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt deleted file mode 100644 index c133fe00ca..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.staking.impl.R - -@Immutable -internal sealed class StakingAlertState { - - abstract val title: TextReference? - abstract val message: TextReference - open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - open val onConfirmClick: (() -> Unit)? = null - - data class GenericError( - override val title: TextReference? = TODO(), - override val onConfirmClick: () -> Unit, - ) : StakingAlertState() { - override val message: TextReference = resourceReference(R.string.common_unknown_error) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 1bf70095ff..8bb0aef58a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -3,8 +3,10 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer @@ -46,6 +48,16 @@ internal class StakingStateController @Inject constructor() { mutableUiState.update(function = titleTransformer::transform) } + fun updateEvent(event: StakingEvent?) { + mutableUiState.update { + it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent()) + } + } + + private fun dismissAlert() { + mutableUiState.update { it.copy(event = consumedEvent()) } + } + private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index e9b4c44faa..4b000bc130 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt new file mode 100644 index 0000000000..f89f99f6a0 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.staking.impl.presentation.state.events + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.staking.impl.R + +@Immutable +internal sealed class StakingAlertUM : AlertUM { + + data class GenericError( + override val onConfirmClick: () -> Unit, + ) : StakingAlertUM() { + override val title: TextReference = resourceReference(R.string.common_error) + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) + } + + data class StakingError( + val code: String, + override val onConfirmClick: () -> Unit, + ) : StakingAlertUM() { + override val title: TextReference = resourceReference(R.string.common_error) + override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code)) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt similarity index 54% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt index dbc800ea01..7e7700595e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt @@ -1,6 +1,7 @@ -package com.tangem.features.staking.impl.presentation.state +package com.tangem.features.staking.impl.presentation.state.events import androidx.compose.runtime.Immutable +import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference @Immutable @@ -8,5 +9,5 @@ internal sealed class StakingEvent { data class ShowSnackBar(val text: TextReference) : StakingEvent() - data class ShowAlert(val alert: StakingAlertState) : StakingEvent() + data class ShowAlert(val alert: AlertUM) : StakingEvent() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt new file mode 100644 index 0000000000..173975c66c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -0,0 +1,44 @@ +package com.tangem.features.staking.impl.presentation.state.events + +import com.tangem.common.ui.alerts.SendTransactionAlertConverter +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.staking.impl.presentation.state.StakingStateController + +internal class StakingEventFactory( + private val stateController: StakingStateController, + private val popBackStack: () -> Unit, + private val onFailedTxEmailClick: (String) -> Unit, +) { + + fun createGenericErrorAlert(error: String) { + val alert = StakingEvent.ShowAlert( + StakingAlertUM.GenericError( + onConfirmClick = { onFailedTxEmailClick(error) }, + ), + ) + stateController.updateEvent(alert) + } + + fun createSendTransactionErrorAlert(error: SendTransactionError?) { + val alert = error?.let { + SendTransactionAlertConverter( + popBackStack = popBackStack, + onFailedTxEmailClick = onFailedTxEmailClick, + ).convert(error) + }?.let { + StakingEvent.ShowAlert(it) + } + stateController.updateEvent(alert) + } + + fun createStakingErrorAlert(error: StakingError) { + val alert = StakingEvent.ShowAlert( + StakingAlertUM.StakingError( + code = error.toString(), + onConfirmClick = { onFailedTxEmailClick(error.toString()) }, + ), + ) + stateController.updateEvent(alert) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index c8e5a5d35a..55d130824f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -8,6 +8,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList +@Suppress("TooManyFunctions") object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} @@ -46,5 +47,7 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onShareClick() {} + override fun onFailedTxEmailClick(errorMessage: String) {} + override fun onActiveStake(activeStake: BalanceState) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt new file mode 100644 index 0000000000..7f1d1455ed --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt @@ -0,0 +1,79 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent + +@Composable +internal fun StakingEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { + val resources = LocalContext.current.resources + var alertConfig by remember { mutableStateOf(value = null) } + + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(key1 = alertConfig) { + keyboardController?.hide() + } + + alertConfig?.let { + StakingAlert(state = it, onDismiss = { alertConfig = null }) + } + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is StakingEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is StakingEvent.ShowAlert -> { + alertConfig = value.alert + } + } + }, + ) +} + +@Composable +internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { + val confirmButton: DialogButton + val dismissButton: DialogButton? + + val onActionClick = state.onConfirmClick + if (onActionClick != null) { + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = { + onActionClick() + onDismiss() + }, + ) + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ) + } else { + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = onDismiss, + ) + dismissButton = null + } + + BasicDialog( + message = state.message.resolveReference(), + confirmButton = confirmButton, + onDismissDialog = onDismiss, + title = state.title?.resolveReference(), + dismissButton = dismissButton, + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index ef94fa0c1d..2bbb05b0c2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,6 +33,8 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { + val snackbarHostState = remember { SnackbarHostState() } + BackHandler(onBack = uiState.clickIntents::onPrevClick) Column( modifier = Modifier @@ -58,6 +61,11 @@ internal fun StakingScreen(uiState: StakingUiState) { ) StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) } + + StakingEventEffect( + event = uiState.event, + snackbarHostState = snackbarHostState, + ) } @Composable diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index db14a50094..f8fb30cfa9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -43,4 +43,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onExploreClick() fun onShareClick() + + fun onFailedTxEmailClick(errorMessage: String) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 09221fb03d..209c6c9304 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -43,6 +43,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer @@ -64,7 +65,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @HiltViewModel internal class StakingViewModel @Inject constructor( private val stateController: StakingStateController, @@ -122,6 +123,13 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() + private val stakingEventFactory: StakingEventFactory + get() = StakingEventFactory( + stateController = stateController, + popBackStack = stakingStateRouter::onBackClick, + onFailedTxEmailClick = ::onFailedTxEmailClick, + ) + private var stakingApproval: StakingApproval = StakingApproval.Empty private val allowanceTaskScheduler = SingleTaskScheduler() @@ -164,6 +172,8 @@ internal class StakingViewModel @Inject constructor( val amountState = value.amountState as? AmountState.Data ?: error("No amount provided") val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") + val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address") val stakingTransaction = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -172,15 +182,16 @@ internal class StakingViewModel @Inject constructor( actionCommonType = value.actionType, integrationId = yield.id, amount = amountValue, - address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: error("No available address"), + address = defaultAddress, validatorAddress = validatorState.chosenValidator.address, token = yield.token, passthrough = pendingAction?.passthrough, type = pendingAction?.type, ), ).getOrElse { - error(it) + Timber.e(it.toString()) + stakingEventFactory.createStakingErrorAlert(it) + return@launch } stakingTransaction @@ -190,8 +201,11 @@ internal class StakingViewModel @Inject constructor( networkId = cryptoCurrencyStatus.currency.network.id.value, fee = fee, transactionId = transaction.id, - ).getOrNull() ?: error("No constructed transaction") - + ).getOrElse { + Timber.e(it.toString()) + stakingEventFactory.createStakingErrorAlert(it) + return@launch + } sendStakingTransaction( transactionId = constructedTransaction.id, gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"), @@ -296,8 +310,7 @@ internal class StakingViewModel @Inject constructor( userWallet = userWallet, cryptoCurrency = cryptoCurrencyStatus.currency, ).getOrElse { - // TODO staking error - return + return stakingEventFactory.createGenericErrorAlert(it.toString()) } stateController.update( @@ -407,7 +420,7 @@ internal class StakingViewModel @Inject constructor( fee = TransactionFee.Single(fee), ), ) - // TODO staking error + stakingEventFactory.createGenericErrorAlert(error.message ?: error.toString()) return@launch }, ifRight = { it }, @@ -427,7 +440,7 @@ internal class StakingViewModel @Inject constructor( fee = TransactionFee.Single(fee), ), ) - // TODO staking error + stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { stateController.update(SetApprovalInProgressTransformer) @@ -489,6 +502,10 @@ internal class StakingViewModel @Inject constructor( // TODO staking [REDACTED_TASK_KEY] } + override fun onFailedTxEmailClick(errorMessage: String) { + // TODO staking sending feedback email [REDACTED_TASK_KEY] + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -505,7 +522,8 @@ internal class StakingViewModel @Inject constructor( userWallet = wallet }, ifLeft = { - // TODO staking error + Timber.e(it.toString()) + stakingEventFactory.createGenericErrorAlert(it.toString()) }, ) getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( @@ -530,7 +548,8 @@ internal class StakingViewModel @Inject constructor( ) }, ifLeft = { - // TODO staking error + Timber.e(it.toString()) + stakingEventFactory.createGenericErrorAlert(it.toString()) }, ) } @@ -579,7 +598,7 @@ internal class StakingViewModel @Inject constructor( pendingActionList = pendingActionList, ), ) - // todo add error dialog + stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { txHash -> submitHash(transactionId, txHash) From 18e5829fbecfdd1a448be8c05fd8b67804928404 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 16:58:01 +0500 Subject: [PATCH 17/41] Updated on 2026-08-14 --- .../impl/presentation/ui/StakingEventEffect.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt index 7f1d1455ed..80ba0d9029 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference @@ -45,24 +45,24 @@ internal fun StakingEventEffect(event: StateEvent, snackbarHostSta @Composable internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = { onActionClick() onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = onDismiss, ) From e51c82e6156f0f1a3fc9549108ade4b495af0adc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 15:13:03 +0300 Subject: [PATCH 18/41] Updated on 2026-08-14 --- .../common/ui/userwallet/UserWalletItem.kt | 19 +- .../components/buttons/common/TangemButton.kt | 57 +++++- .../impl/ui/AddToPortfolioBottomSheet.kt | 184 +++++++++++++++--- .../PreviewAddToPortfolioBSContentProvider.kt | 60 ++++-- .../ui/state/AddToPortfolioBSContentUM.kt | 2 + 5 files changed, 262 insertions(+), 60 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index baf96fd3cf..91f837f146 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -1,8 +1,11 @@ package com.tangem.common.ui.userwallet +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CardColors import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,6 +29,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.common.ui.R import com.tangem.core.ui.coil.RotationTransformation +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemThemePreview @@ -33,9 +37,14 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.persistentListOf @Composable -fun UserWalletItem(state: UserWalletItemUM, modifier: Modifier = Modifier) { +fun UserWalletItem( + state: UserWalletItemUM, + modifier: Modifier = Modifier, + blockColors: CardColors = TangemBlockCardColors, +) { BlockCard( modifier = modifier, + colors = blockColors, onClick = state.onClick, enabled = state.isEnabled, ) { @@ -142,6 +151,7 @@ private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview() { TangemThemePreview { @@ -174,7 +184,12 @@ private fun Preview() { ), ) - Column { + Column( + Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { list.fastForEach { userWalletItemUM -> UserWalletItem( modifier = Modifier.fillMaxWidth(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 11fcaeb099..3033f2221a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -1,5 +1,10 @@ package com.tangem.core.ui.components.buttons.common +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable @@ -36,6 +41,7 @@ fun TangemButton( textStyle: TextStyle = TangemTheme.typography.button, shape: Shape = size.toShape(), iconPadding: Dp = size.toIconPadding(), + animateContentChange: Boolean = false, ) { val multipleClickPreventer = remember { MultipleClickPreventer.get() } @@ -56,6 +62,7 @@ fun TangemButton( buttonIcon = icon, iconPadding = iconPadding, showProgress = showProgress, + animateContentChange = animateContentChange, progressIndicator = { CircularProgressIndicator( modifier = Modifier.buttonContentSize(maxContentSize), @@ -108,24 +115,54 @@ private inline fun RowScope.ButtonContentContainer( buttonIcon: TangemButtonIconPosition, iconPadding: Dp, showProgress: Boolean, + animateContentChange: Boolean, progressIndicator: @Composable RowScope.() -> Unit, - text: @Composable RowScope.() -> Unit, - icon: @Composable RowScope.(Int) -> Unit, + crossinline text: @Composable RowScope.() -> Unit, + crossinline icon: @Composable RowScope.(Int) -> Unit, additionalText: @Composable () -> Unit, ) { if (showProgress) { progressIndicator() } else { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Row(horizontalArrangement = Arrangement.Center) { - if (buttonIcon is TangemButtonIconPosition.Start) { - icon(buttonIcon.iconResId) - Spacer(modifier = Modifier.requiredWidth(iconPadding)) + if (animateContentChange) { + AnimatedContent( + targetState = buttonIcon, + transitionSpec = { + fadeIn(tween(durationMillis = 220)) togetherWith + fadeOut(tween(durationMillis = 220)) + }, + label = "button text with icon", + ) { iconState -> + Row(horizontalArrangement = Arrangement.Center) { + when (iconState) { + is TangemButtonIconPosition.Start -> { + icon(iconState.iconResId) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + text() + } + is TangemButtonIconPosition.End -> { + text() + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + icon(iconState.iconResId) + } + is TangemButtonIconPosition.None -> { + text() + } + } + } } - text() - if (buttonIcon is TangemButtonIconPosition.End) { - Spacer(modifier = Modifier.requiredWidth(iconPadding)) - icon(buttonIcon.iconResId) + } else { + Row(horizontalArrangement = Arrangement.Center) { + if (buttonIcon is TangemButtonIconPosition.Start) { + icon(buttonIcon.iconResId) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + } + text() + if (buttonIcon is TangemButtonIconPosition.End) { + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + icon(buttonIcon.iconResId) + } } } additionalText() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 8a5a2ae45f..e9b760f529 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -2,29 +2,36 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.util.fastForEachIndexed import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.SpacerW6 -import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow @@ -36,18 +43,18 @@ import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import kotlinx.coroutines.delay @Composable internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, containerColor = TangemTheme.colors.background.tertiary, + addBottomInsets = false, titleText = resourceReference(R.string.markets_add_to_portfolio_button), ) { Content( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing16), + modifier = Modifier.fillMaxWidth(), state = config.content as AddToPortfolioBSContentUM, ) } @@ -55,33 +62,101 @@ internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - UserWalletItem(state.selectedWallet) + var continueButtonAreaHeight by remember { mutableIntStateOf(0) } + val density = LocalDensity.current + val scrollState = rememberScrollState() - NetworkSelection( - modifier = Modifier.fillMaxWidth(), - state = state.selectNetworkUM, - ) - - AnimatedVisibility( - visible = state.isScanCardNotificationVisible, - modifier = Modifier.fillMaxWidth(), + Box(modifier = modifier) { + Column( + modifier = Modifier + .verticalScroll(state = scrollState) + .padding(horizontal = TangemTheme.dimens.spacing16), ) { - ScanWalletWarning(modifier = Modifier.fillMaxWidth()) + UserWalletItem( + state = state.selectedWallet, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) + + SpacerH12() + + NetworkSelection( + modifier = Modifier.fillMaxWidth(), + state = state.selectNetworkUM, + ) + + SpacerH12() + + AnimatedVisibility( + visible = state.isScanCardNotificationVisible, + modifier = Modifier.fillMaxWidth(), + ) { + Column { + ScanWalletWarning(modifier = Modifier.fillMaxWidth()) + SpacerH12() + } + + // Scroll to the bottom when the notification appears and the scroll is at the bottom + LaunchedEffect(Unit) { + if (scrollState.canScrollForward.not()) { + delay(timeMillis = 500) + scrollState.animateScrollTo(scrollState.maxValue) + } + } + } + + SpacerH(with(density) { continueButtonAreaHeight.toDp() }) } - PrimaryButtonIconEnd( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.common_continue), - iconResId = R.drawable.ic_tangem_24, - onClick = {}, + AnimatedVisibility( + visible = scrollState.canScrollForward, + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter), + ) { + BottomFade(Modifier.align(Alignment.BottomCenter)) + } + + ContinueButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .onGloballyPositioned { + continueButtonAreaHeight = it.size.height + }, + enabled = state.continueButtonEnabled, + onClick = state.onContinueButtonClick, ) } } +@Composable +private fun ContinueButton(enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemButton( + enabled = enabled, + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .navigationBarsPadding() + .fillMaxWidth(), + text = stringResource(R.string.common_continue), + icon = if (enabled) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + }, + showProgress = false, + size = TangemButtonSize.Default, + colors = TangemButtonsDefaults.primaryButtonColors, + onClick = onClick, + animateContentChange = true, + ) +} + @Suppress("LongMethod") @Composable private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { @@ -95,10 +170,7 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi ) }, ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()), - ) { + Column { Row( modifier = Modifier .fillMaxWidth() @@ -180,6 +252,7 @@ private fun ScanWalletWarning(modifier: Modifier = Modifier) { Icon( modifier = Modifier.requiredSize(TangemTheme.dimens.size20), imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + tint = TangemTheme.colors.icon.primary1, contentDescription = null, ) Text( @@ -237,4 +310,53 @@ private fun PreviewContentRtl( state = content, ) } +} + +// For on device testing +@Composable +@Preview +private fun PreviewContentTestOnDevice( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview( + alwaysShowBottomSheets = false, + ) { + var isShow by remember { mutableStateOf(false) } + + var contentState by remember { + mutableStateOf(content) + } + + LaunchedEffect(Unit) { + contentState = content.copy( + onContinueButtonClick = { + contentState = contentState.copy( + isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, + ) + }, + continueButtonEnabled = true, + selectedWallet = content.selectedWallet.copy( + onClick = { + contentState = contentState.copy( + continueButtonEnabled = !contentState.continueButtonEnabled, + ) + }, + ), + ) + } + + AddToPortfolioBottomSheet( + config = TangemBottomSheetConfig( + isShow = isShow, + content = contentState, + onDismissRequest = { isShow = false }, + ), + ) + + Button( + onClick = { isShow = !isShow }, + ) { + Text(text = "Toggle") + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index 2b33545fd3..d184d3aac6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -12,6 +12,14 @@ import kotlinx.collections.immutable.persistentListOf internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { + private val blockchainRow = BlockchainRowUM( + name = "Etherium 3", + type = "TEST", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = false, + ) + override val values: Sequence get() = sequenceOf( AddToPortfolioBSContentUM( @@ -29,32 +37,50 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider tokenName = "Etherium", tokenCurrencySymbol = "ETH", networks = persistentListOf( - BlockchainRowUM( - name = "Etherium", + blockchainRow.copy( type = "MAIN", - iconResId = R.drawable.ic_eth_16, isMainNetwork = true, isSelected = true, ), - BlockchainRowUM( - name = "Etherium 2", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ), - BlockchainRowUM( - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ), + blockchainRow, + blockchainRow, ), onNetworkSwitchClick = { _, _ -> }, iconUrl = null, ), isScanCardNotificationVisible = true, + continueButtonEnabled = true, + onContinueButtonClick = {}, + ), + AddToPortfolioBSContentUM( + selectedWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ), + selectNetworkUM = SelectNetworkUM( + tokenId = "etherium", + tokenName = "Etherium", + tokenCurrencySymbol = "ETH", + networks = persistentListOf( + blockchainRow.copy( + type = "MAIN", + isMainNetwork = true, + isSelected = true, + ), + *Array(25) { blockchainRow }, + ), + + onNetworkSwitchClick = { _, _ -> }, + iconUrl = null, + ), + isScanCardNotificationVisible = true, + continueButtonEnabled = false, + onContinueButtonClick = {}, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt index ad733c39cf..6b708502ce 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -7,4 +7,6 @@ internal data class AddToPortfolioBSContentUM( val selectedWallet: UserWalletItemUM, val selectNetworkUM: SelectNetworkUM, val isScanCardNotificationVisible: Boolean, + val continueButtonEnabled: Boolean, + val onContinueButtonClick: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file From 1f8af50d6ad180e29eaa575ade699aac7482c96a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Aug 2024 22:22:48 +0400 Subject: [PATCH 19/41] Updated on 2026-08-14 --- core/ui/build.gradle.kts | 3 +- .../core/ui/components/token}/TokenItem.kt | 123 +++++++++++++++--- .../token/internal}/LockedRectangle.kt | 2 +- .../token/internal}/NonFiatContentBlock.kt | 6 +- .../token/internal}/TokenCryptoAmount.kt | 6 +- .../token/internal}/TokenFiatAmount.kt | 6 +- .../components/token/internal}/TokenPrice.kt | 6 +- .../components/token/internal}/TokenTitle.kt | 6 +- .../components/token}/state/TokenItemState.kt | 4 +- .../presentation/common/WalletPreviewData.kt | 121 ++--------------- .../common/preview/WalletScreenPreviewData.kt | 2 +- .../organizetokens/OrganizeTokensScreen.kt | 2 +- .../organizetokens/model/DraggableItem.kt | 2 +- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- .../state/model/WalletTokensListState.kt | 2 +- .../converter/TokenItemStateConverter.kt | 2 +- .../multicurrency/MultiCurrencyContentItem.kt | 2 +- 17 files changed, 143 insertions(+), 154 deletions(-) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component => core/ui/src/main/java/com/tangem/core/ui/components/token}/TokenItem.kt (76%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/LockedRectangle.kt (88%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/NonFiatContentBlock.kt (93%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenCryptoAmount.kt (89%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenFiatAmount.kt (92%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenPrice.kt (95%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token => core/ui/src/main/java/com/tangem/core/ui/components/token/internal}/TokenTitle.kt (92%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common => core/ui/src/main/java/com/tangem/core/ui/components/token}/state/TokenItemState.kt (98%) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 4758911a25..3781bde624 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,5 +1,3 @@ -import com.android.ide.common.resources.generateLocaleList - plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -42,6 +40,7 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.reorderable) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt similarity index 76% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 1917d4e4c8..8bdd28b235 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component +package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -16,15 +16,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints +import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.internal.* +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.token.* -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState +import java.util.UUID import kotlin.math.max private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 @@ -35,7 +38,7 @@ private enum class LayoutId { } @Composable -internal fun TokenItem( +fun TokenItem( state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier, @@ -397,8 +400,8 @@ private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider:: private class TokenItemStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.tokenItemVisibleState.copy( - iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true), + tokenItemVisibleState.copy( + iconState = coinIconState.copy(showCustomBadge = true), titleState = TokenItemState.TitleState.Content( text = "PolygonPolygonPolygonPolygonPolygonPolygon", hasPending = true, @@ -414,13 +417,101 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider Date: Wed, 21 Aug 2024 12:53:58 +0500 Subject: [PATCH 20/41] Updated on 2026-08-14 --- .../domain/feedback/FeedbackDataBuilder.kt | 6 +++ .../feedback/SaveBlockchainErrorUseCase.kt | 2 +- .../feedback/models/FeedbackEmailType.kt | 8 +++ .../utils/EmailMessageBodyResolver.kt | 36 +++++++++++++ .../utils/EmailMessageTitleResolver.kt | 4 +- .../feedback/utils/EmailSubjectResolver.kt | 1 + features/staking/impl/build.gradle.kts | 1 + .../viewmodel/StakingViewModel.kt | 52 ++++++++++++++++++- 8 files changed, 106 insertions(+), 4 deletions(-) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index a3cbd7d4f8..30d5dd83a8 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -86,6 +86,12 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") } + fun addStakingInfo(validatorName: String?, transactionType: String?, unsignedTransaction: String?) { + builder.appendKeyValue("Validator", validatorName ?: "unknown") + builder.appendKeyValue("Action", transactionType ?: "unknown") + builder.appendKeyValue("Unsigned transaction", unsignedTransaction ?: "unknown") + } + fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun build(): String = builder.trimEnd().toString() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt index 596699a073..687a9859d1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt @@ -14,7 +14,7 @@ class SaveBlockchainErrorUseCase( private val feedbackRepository: FeedbackRepository, ) { - fun invoke(error: BlockchainErrorInfo) { + operator fun invoke(error: BlockchainErrorInfo) { feedbackRepository.saveBlockchainErrorInfo(error = error) } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 1e4971e4cf..1be0afb1ff 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -22,4 +22,12 @@ sealed interface FeedbackEmailType { /** User has problem with sending transaction */ data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType + + /** User has problem with staking */ + data class StakingProblem( + override val cardInfo: CardInfo, + val validatorName: String?, + val transactionType: String?, + val unsignedTransaction: String?, + ) : FeedbackEmailType } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index a9bb1a64dd..c314b7a4bc 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -23,6 +23,12 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) is FeedbackEmailType.ScanningProblem -> addScanningProblemBody() is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) + is FeedbackEmailType.StakingProblem -> addStakingProblemBody( + type.cardInfo, + type.validatorName, + type.transactionType, + type.unsignedTransaction, + ) } return build() @@ -72,6 +78,36 @@ internal class EmailMessageBodyResolver( addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } + private suspend fun FeedbackDataBuilder.addStakingProblemBody( + cardInfo: CardInfo, + validatorName: String?, + transactionType: String?, + unsignedTransaction: String?, + ) { + addCardInfo(cardInfo) + addDelimiter() + + val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) + val blockchainInfo = blockchainError?.let { + feedbackRepository.getBlockchainInfo( + userWalletId = userWalletId, + blockchainId = blockchainError.blockchainId, + derivationPath = blockchainError.derivationPath, + ) + } + + if (blockchainInfo != null) { + addBlockchainError(blockchainInfo, blockchainError) + addDelimiter() + } + + addStakingInfo(validatorName, transactionType, unsignedTransaction) + addDelimiter() + + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) { addCardInfo(cardInfo) addDelimiter() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 6ab001b775..bfa9cfdede 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -19,7 +19,9 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed - is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + is FeedbackEmailType.TransactionSendingProblem, + is FeedbackEmailType.StakingProblem, + -> R.string.feedback_preface_tx_failed } .let(resources::getString) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index beb89739bd..503f1f514b 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -26,6 +26,7 @@ internal class EmailSubjectResolver(private val resources: Resources) { is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + is FeedbackEmailType.StakingProblem -> R.string.feedback_subject_tx_failed } .let(resources::getString) } diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 9d23634914..808810f2d7 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory) + implementation(projects.domain.feedback) /** Common */ implementation(projects.common.ui) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 209c6c9304..391e3e0c02 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -17,6 +17,11 @@ import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.staking.* import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction @@ -24,6 +29,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase @@ -94,6 +100,9 @@ internal class StakingViewModel @Inject constructor( private val isApproveNeededUseCase: IsApproveNeededUseCase, private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, + private val feedbackManager: FeedbackManager, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -133,6 +142,8 @@ internal class StakingViewModel @Inject constructor( private var stakingApproval: StakingApproval = StakingApproval.Empty private val allowanceTaskScheduler = SingleTaskScheduler() + private var transactionInProgress: StakingTransaction? = null + private var approvalJobHolder: JobHolder = JobHolder() init { @@ -206,9 +217,13 @@ internal class StakingViewModel @Inject constructor( stakingEventFactory.createStakingErrorAlert(it) return@launch } + val gasEstimate = constructedTransaction.gasEstimate + ?: return@launch stakingEventFactory.createGenericErrorAlert("Gas estimate is null") + + transactionInProgress = constructedTransaction sendStakingTransaction( transactionId = constructedTransaction.id, - gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"), + gasEstimate = gasEstimate, txData = transactionData, pendingActionList = confirmationState.pendingActions, ) @@ -503,7 +518,39 @@ internal class StakingViewModel @Inject constructor( } override fun onFailedTxEmailClick(errorMessage: String) { - // TODO staking sending feedback email [REDACTED_TASK_KEY] + viewModelScope.launch { + val network = cryptoCurrencyStatus.currency.network + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } + val amountState = uiState.value.amountState as? AmountState.Data + val confirmationState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val feeState = confirmationState?.feeState as? FeeState.Content + + val validator = validatorState?.chosenValidator + val feeAmount = feeState?.fee?.amount + val amount = amountState?.amountTextField?.cryptoAmount + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage, + blockchainId = network.id.value, + derivationPath = network.derivationPath.value, + destinationAddress = validator?.address.orEmpty(), + tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol, + amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), + fee = feeAmount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), + ), + ) + + val email = FeedbackEmailType.StakingProblem( + cardInfo = cardInfo, + validatorName = validator?.name, + transactionType = transactionInProgress?.type?.name, + unsignedTransaction = transactionInProgress?.unsignedTransaction, + ) + + feedbackManager.sendEmail(email) + } } fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { @@ -601,6 +648,7 @@ internal class StakingViewModel @Inject constructor( stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { txHash -> + transactionInProgress = null submitHash(transactionId, txHash) scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( From f2585c4134d92d573b2a19c88fdc476254efd23d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 18:25:51 +0500 Subject: [PATCH 21/41] Updated on 2026-08-14 --- .../field/AmountFieldChangeTransformer.kt | 5 ++++ .../data/staking/DefaultStakingRepository.kt | 25 +++++++++++-------- .../model/stakekit/YieldBalanceList.kt | 7 ++---- .../staking/repositories/StakingRepository.kt | 2 ++ .../CurrenciesStatusesLceOperations.kt | 10 +++++--- .../CurrenciesStatusesOperations.kt | 10 +++++--- .../repository/MockStakingRepository.kt | 3 +++ .../AmountRequirementStateTransformer.kt | 5 ++++ gradle/dependencies.toml | 2 +- 9 files changed, 46 insertions(+), 23 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index d45e7c07bd..528d288023 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.common.ui.amountScreen.converters.field import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState @@ -81,6 +82,10 @@ class AmountFieldChangeTransformer( cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), isError = false, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), ), ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index c4b9b8fad1..05dfc8c0b3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -70,7 +70,7 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggle: StakingFeatureToggles, private val walletManagersFacade: WalletManagersFacade, - private val moshi: Moshi, + moshi: Moshi, ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() @@ -111,6 +111,10 @@ internal class DefaultStakingRepository( private val tronStakeKitTransactionAdapter: JsonAdapter = moshi.adapter(TronStakeKitTransaction::class.java) + override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { + rawNetworkId.plus(rawCurrencyId) + } + override fun isStakingSupported(integrationKey: String): Boolean { return integrationIdMap.containsKey(integrationKey) } @@ -165,7 +169,7 @@ internal class DefaultStakingRepository( val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) - val isSupported = isStakingSupported(cryptoCurrencyId.getIntegrationKey()) + val isSupported = isStakingSupported(getIntegrationKey(cryptoCurrencyId)) when { prefetchedYield != null && isSupported -> { @@ -273,7 +277,7 @@ internal class DefaultStakingRepository( ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: return@withContext val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() @@ -307,7 +311,7 @@ internal class DefaultStakingRepository( send(YieldBalance.Empty) } else { launch(dispatchers.io) { - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") stakingBalanceStore.get(userWalletId, integrationId) .collectLatest { @@ -340,7 +344,7 @@ internal class DefaultStakingRepository( } else { fetchSingleYieldBalance(userWalletId, cryptoCurrency) - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) ?: return@withContext YieldBalance.Error @@ -371,7 +375,7 @@ internal class DefaultStakingRepository( val availableCurrencies = cryptoCurrencies .mapNotNull { currency -> val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network) - val integrationId = integrationIdMap[currency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(currency.id)] if (integrationId != null && address != null) { address to integrationId @@ -561,7 +565,7 @@ internal class DefaultStakingRepository( } override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { - return when (cryptoCurrency.id.getIntegrationKey()) { + return when (getIntegrationKey(cryptoCurrency.id)) { Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> { StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) } @@ -575,6 +579,7 @@ internal class DefaultStakingRepository( Blockchain.Solana, Blockchain.Cosmos, -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) + Blockchain.BSC, Blockchain.Ethereum, -> TransactionData.Compiled.Data.RawString(unsignedTransaction) Blockchain.Tron -> { @@ -611,8 +616,6 @@ internal class DefaultStakingRepository( private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" - private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId) - private fun getTronResource(network: Network): TronResource? { val blockchain = Blockchain.fromNetworkId(network.backendId) @@ -629,11 +632,11 @@ internal class DefaultStakingRepository( const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" + const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" const val TRON_INTEGRATION_ID = "tron-trx-native-staking" const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" const val NEAR_INTEGRATION_ID = "near-near-native-staking" const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" @@ -645,11 +648,11 @@ internal class DefaultStakingRepository( Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID, Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, + Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, - // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, // Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID, // Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt index 01cc23bdc9..042aa9acbf 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt @@ -5,13 +5,10 @@ sealed class YieldBalanceList { data class Data( val balances: List, ) : YieldBalanceList() { - fun getBalance(rawCurrencyId: String?, networkName: String): YieldBalance { + fun getBalance(rawCurrencyId: String?): YieldBalance { return balances.firstOrNull { yield -> (yield as? YieldBalance.Data)?.balance?.items - ?.any { - rawCurrencyId == it.rawCurrencyId && - networkName.equals(it.rawNetworkId, ignoreCase = true) - } == true + ?.any { rawCurrencyId == it.rawCurrencyId } == true } ?: YieldBalance.Error } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 8a48c154a9..528f40dac0 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -22,6 +22,8 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface StakingRepository { + fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String + fun isStakingSupported(integrationKey: String): Boolean suspend fun fetchEnabledYields(refresh: Boolean) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 59461356b9..8d97dc1a83 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -144,10 +144,14 @@ internal class CurrenciesStatusesLceOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( - rawCurrencyId = currency.id.rawCurrencyId, - networkName = currency.network.name, + val isStakingSupported = stakingRepository.isStakingSupported( + stakingRepository.getIntegrationKey(currency.id), ) + val yieldBalance = if (isStakingSupported) { + (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + } else { + null + } createCurrencyStatus( currency = currency, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 55ba05a3d3..9954751464 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -260,10 +260,14 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( - rawCurrencyId = currency.id.rawCurrencyId, - networkName = currency.network.name, + val isStakingSupported = stakingRepository.isStakingSupported( + stakingRepository.getIntegrationKey(currency.id), ) + val yieldBalance = if (isStakingSupported) { + (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + } else { + null + } createCurrencyStatus( currency = currency, quote = quote, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index a19fea75b9..34ebfe1a1c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -24,6 +24,9 @@ import org.joda.time.DateTime import java.math.BigDecimal class MockStakingRepository : StakingRepository { + + override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = "" + override fun isStakingSupported(currencyId: String): Boolean = true override suspend fun fetchEnabledYields(refresh: Boolean) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index f16597ae54..59d4adfaf7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount +import androidx.compose.ui.text.input.ImeAction import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.extensions.resourceReference @@ -31,6 +32,7 @@ internal class AmountRequirementStateTransformer( val isRequirementError = isRequirementError(prevState, amountRequirements) return if (isRequirementError) { prevState.copy( + isPrimaryButtonEnabled = false, amountTextField = prevState.amountTextField.copy( isError = true, error = resourceReference( @@ -43,6 +45,9 @@ internal class AmountRequirementStateTransformer( ), ), ), + keyboardOptions = prevState.amountTextField.keyboardOptions.copy( + imeAction = ImeAction.None, + ), ), ) } else { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index dbe4ee3f6d..da78cb472c 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-740" +tangemBlockchainSdk = "develop-745" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From e356f020f383723802953e0a2aa32d8d011440a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Aug 2024 18:21:13 +0400 Subject: [PATCH 22/41] Updated on 2026-08-14 --- .../core/ui/components/token/TokenItem.kt | 68 +++++++++- .../components/token/internal/TokenPrice.kt | 55 ++++++-- .../components/token/state/TokenItemState.kt | 126 +++++++++++------- .../common/preview/WalletScreenPreviewData.kt | 6 +- .../converter/TokenItemStateConverter.kt | 8 +- 5 files changed, 191 insertions(+), 72 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 8bdd28b235..027db6c7d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -37,12 +37,43 @@ private enum class LayoutId { ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT } +/** + * Token item for non reorderable list + * + * @param state token item state + * @param isBalanceHidden flag that shows/hides balance + * @param modifier modifier + * + * @see Figma Component + */ +@Composable +fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + TokenItem( + state = state, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + reorderableTokenListState = null, + ) +} + +/** + * Token item for reorderable list + * + * @param state token item state + * @param isBalanceHidden flag that shows/hides balance + * @param reorderableTokenListState reorderable token list state + * @param modifier modifier + * + * @see Figma Component + */ @Composable fun TokenItem( state: TokenItemState, isBalanceHidden: Boolean, + reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier, - reorderableTokenListState: ReorderableLazyListState? = null, ) { val betweenRowsMargin = TangemTheme.dimens.spacing2 @@ -76,7 +107,7 @@ fun TokenItem( ) TokenPrice( - state = state.cryptoPriceState, + state = state.subtitleState, modifier = Modifier .layoutId(layoutId = LayoutId.CRYPTO_PRICE) .padding(end = TangemTheme.dimens.spacing8), @@ -195,6 +226,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier is TokenItemState.Unreachable, -> { firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + + if (state.subtitleState != null) { + secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + } } } @@ -232,7 +267,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier y = when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, - -> (layoutHeight - title.height).div(other = 2) + -> { + if (state.subtitleState == null) { + (layoutHeight - title.height).div(other = 2) + } else { + verticalPadding + } + } else -> verticalPadding }, ) @@ -411,7 +452,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider { + is TokenPriceState.CryptoPriceContent -> { PriceBlock( modifier = modifier, price = state.price, @@ -32,13 +37,12 @@ internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modi priceChangePercent = state.priceChangePercent, ) } - is TokenPriceChangeState.Unknown -> { - PriceText(text = DASH_SIGN, modifier = modifier) - } - is TokenPriceChangeState.Loading -> { + is TokenPriceState.TextContent -> PriceText(text = state.value, modifier = modifier) + is TokenPriceState.Unknown -> PriceText(text = DASH_SIGN, modifier = modifier) + is TokenPriceState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) } - is TokenPriceChangeState.Locked -> { + is TokenPriceState.Locked -> { LockedRectangle(modifier = modifier.placeholderSize()) } null -> Unit @@ -122,4 +126,37 @@ private fun Modifier.placeholderSize(): Modifier = composed { return@composed this .padding(vertical = TangemTheme.dimens.spacing2) .size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12) -} \ No newline at end of file +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview(@PreviewParameter(TokenPriceChangeStateProvider::class) state: TokenPriceState) { + TangemThemePreview { + TokenPrice(state = state) + } +} + +private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.UP, + ), + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.DOWN, + ), + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.NEUTRAL, + ), + TokenPriceState.TextContent(value = "Subtitle"), + TokenPriceState.Unknown, + TokenPriceState.Loading, + TokenPriceState.Locked, + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 832b849235..9c014e8067 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -4,58 +4,78 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType -/** Token item state */ +/** TokenItem component state */ @Immutable sealed class TokenItemState { + /** Unique id */ abstract val id: String + /** Token icon state */ abstract val iconState: CurrencyIconState + /** Token title state (in one row with [fiatAmountState]) */ abstract val titleState: TitleState + /** Token subtitle state (under [titleState] and in one row with [cryptoAmountState]) */ + abstract val subtitleState: SubtitleState? + + /** Token fiat amount state (in one row with [titleState]) */ abstract val fiatAmountState: FiatAmountState? + /** Token crypto amount state (under [fiatAmountState] and in one row with [subtitleState]) */ abstract val cryptoAmountState: CryptoAmountState? - abstract val cryptoPriceState: CryptoPriceState? - - /** Loading token state */ + /** + * Loading token state + * + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + */ data class Loading( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState.Content, + override val subtitleState: SubtitleState = SubtitleState.Loading, ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading - override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Loading } - /** Locked token state */ + /** + * Locked token state + * + * @property id unique id + */ data class Locked(override val id: String) : TokenItemState() { override val iconState: CurrencyIconState = CurrencyIconState.Locked override val titleState: TitleState = TitleState.Locked + override val subtitleState: SubtitleState = SubtitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked - override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Locked } /** * Content token state * - * @property id unique id - * @property iconState token icon state - * @property titleState token name - * @property onItemClick callback which will be called when an item is clicked - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property fiatAmountState token fiat amount + * @property cryptoAmountState token crypto amount + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState, override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, - override val cryptoPriceState: CryptoPriceState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() @@ -63,9 +83,10 @@ sealed class TokenItemState { /** * Draggable token state * - * @property id unique id - * @property iconState token icon state - * @property titleState token name + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property cryptoAmountState token crypto amount */ data class Draggable( override val id: String, @@ -73,48 +94,50 @@ sealed class TokenItemState { override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, ) : TokenItemState() { + override val subtitleState: SubtitleState? = null override val fiatAmountState: FiatAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } /** * Unreachable token state * - * @property id token id - * @property iconState token icon state - * @property titleState token name - * @property onItemClick callback which will be called when an item is clicked - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Unreachable( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState? = null, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } /** * No derivation address state * - * @property id token id - * @property iconState token icon state - * @property titleState token name - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property onItemLongClick callback which will be called when an item is long clicked */ data class NoAddress( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState? = null, val onItemLongClick: () -> Unit, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } @Immutable @@ -122,9 +145,27 @@ sealed class TokenItemState { data class Content(val text: String, val hasPending: Boolean = false) : TitleState() - object Loading : TitleState() + data object Loading : TitleState() - object Locked : TitleState() + data object Locked : TitleState() + } + + @Immutable + sealed class SubtitleState { + + data class CryptoPriceContent( + val price: String, + val priceChangePercent: String, + val type: PriceChangeType, + ) : SubtitleState() + + data class TextContent(val value: String) : SubtitleState() + + data object Unknown : SubtitleState() + + data object Loading : SubtitleState() + + data object Locked : SubtitleState() } @Immutable @@ -134,34 +175,19 @@ sealed class TokenItemState { val hasStaked: Boolean = false, ) : FiatAmountState() - object Loading : FiatAmountState() + data object Loading : FiatAmountState() - object Locked : FiatAmountState() + data object Locked : FiatAmountState() } @Immutable sealed class CryptoAmountState { data class Content(val text: String) : CryptoAmountState() - object Unreachable : CryptoAmountState() + data object Unreachable : CryptoAmountState() - object Loading : CryptoAmountState() + data object Loading : CryptoAmountState() - object Locked : CryptoAmountState() - } - - sealed class CryptoPriceState { - - data class Content( - val price: String, - val priceChangePercent: String?, - val type: PriceChangeType?, - ) : CryptoPriceState() - - object Unknown : CryptoPriceState() - - object Loading : CryptoPriceState() - - object Locked : CryptoPriceState() + data object Locked : CryptoAmountState() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 12dcfa641f..63585b4bbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -20,7 +20,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "0,35853044 BTC"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "34 496,75 \$", priceChangePercent = "0,43 %", type = PriceChangeType.DOWN, @@ -46,7 +46,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Ethereum"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "1,856660295 ETH"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "1 799,41 \$", priceChangePercent = "5,16 %", type = PriceChangeType.UP, @@ -68,7 +68,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Shiba Inu"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "6 200 220,00 SHIB"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "0.01 \$", priceChangePercent = "1,34 %", type = PriceChangeType.DOWN, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index e47f61c804..e9752df81b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -59,7 +59,7 @@ internal class TokenItemStateConverter( hasStaked = !getStakedBalance().isZero(), ), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), - cryptoPriceState = getCryptoPriceState(), + subtitleState = getCryptoPriceState(), onItemClick = { clickIntents.onTokenItemClick(this) }, onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) @@ -97,12 +97,12 @@ internal class TokenItemStateConverter( onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) - private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState { + private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.SubtitleState { val fiatRate = value.fiatRate val priceChange = value.priceChange return if (fiatRate != null && priceChange != null) { - TokenItemState.CryptoPriceState.Content( + TokenItemState.SubtitleState.CryptoPriceContent( price = fiatRate.getFormattedCryptoPrice(), priceChangePercent = BigDecimalFormatter.formatPercent( percent = priceChange, @@ -111,7 +111,7 @@ internal class TokenItemStateConverter( type = priceChange.getPriceChangeType(), ) } else { - TokenItemState.CryptoPriceState.Unknown + TokenItemState.SubtitleState.Unknown } } From 71dfa1c99f14a593fa1a92f38965d8ddc69d27bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Aug 2024 15:06:14 +0300 Subject: [PATCH 23/41] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 8 ++++-- core/res/src/main/res/values-ja/strings.xml | 7 +++-- core/res/src/main/res/values-ru/strings.xml | 3 ++- .../src/main/res/values-uk-rUA/strings.xml | 3 ++- .../src/main/res/values-zh-rTW/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 8 +++--- .../data/staking/DefaultStakingRepository.kt | 4 +-- .../domain/staking/model/StakingEntryInfo.kt | 5 ++-- .../repository/MockStakingRepository.kt | 4 +-- .../impl/presentation/state/FeeState.kt | 2 +- .../state/InnerConfirmationStakingState.kt | 2 +- .../presentation/state/StakingNotification.kt | 21 +++++++-------- .../impl/presentation/state/StakingUiState.kt | 2 +- .../state/bottomsheet/InfoType.kt | 9 +++++++ .../StakingInfoBottomSheetConfig.kt | 2 +- .../ConfirmationStatePreviewData.kt | 8 ++++-- .../state/stub/StakingClickIntentsStub.kt | 4 +-- .../SetConfirmationStateLoadingTransformer.kt | 8 ++++-- .../SetInitialDataStateTransformer.kt | 1 + .../ShowInfoBottomSheetStateTransformer.kt | 9 +------ .../amount/AmountChangeStateTransformer.kt | 2 +- .../amount/AmountMaxValueStateTransformer.kt | 2 +- .../viewmodel/StakingClickIntents.kt | 2 +- .../viewmodel/StakingViewModel.kt | 9 ++++--- .../tokendetails/TokenDetailsPreviewData.kt | 11 +++++--- .../tokendetails/state/StakingBlockUM.kt | 5 ++-- .../TokenDetailsLoadedBalanceConverter.kt | 17 ++++++++---- .../factory/TokenStakingStateConverter.kt | 22 +++++++++++----- .../tokendetails/state/utils/BalanceUtils.kt | 2 +- .../state/utils/RewardScheduleUtils.kt | 26 +++++++++++++++++++ .../components/staking/TokenStakingBlock.kt | 18 +++++-------- 31 files changed, 146 insertions(+), 81 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 21258cdee1..cc74abea34 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -835,8 +835,8 @@ Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. - Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. - Genehmigung in Arbeit + Das Genehmigungsverfahren ist derzeit im Gange und wird in Kürze abgeschlossen sein + Genehmigung läuft Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. @@ -854,12 +854,16 @@ Netzwerk erfordert eine Mindesteinzahlung Der Swap wird nach Abschluss der Transaktion %s verfügbar sein. Du hast aktive Transaktion + Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. + Genehmigung in Arbeit Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt. Du hast keine %s Coins in deiner Liste Keine Token zum Tauschen verfügbar Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s Die Gebühr %s kann nicht gedeckt werden Der zu erhaltende Betrag muss mindestens %s betragen + Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %@) + Ausgewähltes Paar vorübergehend nicht verfügbar Service vorübergehend nicht verfügbar Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s Der zu tauschende Betrag muss mindestens %s betragen diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 046d0156bc..70892f297f 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -98,6 +98,7 @@ 有効にする 有効 エラー + 交換 移動する 取引履歴を調べる エクスプローラー @@ -823,8 +824,8 @@ ウォレット設定 Tangem %sを使用するか、カードをスキャンしてウォレットにアクセスしてください - スワップの承認は現在進行中で、まもなく完了する予定です。 - 承認が進行中 + 許可付与のプロセスは現在進行中であり、まもなく完了する予定です。 + 承認中 カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 @@ -842,6 +843,8 @@ ネットワークには最低残高が必要です スワップは、%s の取引完了後に利用可能となります。 アクティブな取引があります + スワップの承認は現在進行中で、まもなく完了する予定です。 + 承認が進行中 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 あなたのリストには、交換可能な %s トークンがありません。 スワップ可能なトークンがありません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index fe433aaed9..723c6c4ffb 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -684,7 +684,7 @@ Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. - Повторный стейкинг + Сменить валидатора Застейкать вознаграждения Отозвать Переголосовать @@ -702,6 +702,7 @@ Стейкинг закрыт Застейкать еще Застейкать %s + Вывести %s Разблокировать Выведено из стейкинга Проверьте процесс завершения стейкинга, чтобы вывести свои средства. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 62d3baa6e6..10a8cd574b 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -704,7 +704,7 @@ Місяць Тиждень Винагороди - Застейкати + Стейкінг закрито Застейкати більше Застейкати %s Зняти зі стейкінгу %s @@ -869,6 +869,7 @@ Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s Неможливо покрити комісію %s Сума отримання не може бути меншою за %s + Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %@) Сервіс тимчасово недоступний Сума до обміну не повинна перевищувати %s Сума для обміну має бути не менше %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index eb5bc2de81..3c2329daab 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -63,6 +63,7 @@ OK 主卡片 + %1$s-%2$s 拒絕 重新命名 保存設置 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 79135b1d0d..08e26457c7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -669,10 +669,10 @@ The allocated time for activating participation in staking. Migrate Native staking - Staking allow you to earn %1$s. Your staking rewards arrive every day. - Staking allow you to earn %1$s. Your staking rewards arrive every hour. - Staking allow you to earn %1$s. Your staking rewards arrive every month. - Staking allow you to earn %1$s. Your staking rewards arrive every week. + Staking allows you to earn %1$s. Your staking rewards arrive every day. + Staking allows you to earn %1$s. Your staking rewards arrive every hour. + Staking allows you to earn %1$s. Your staking rewards arrive every month. + Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards Rewards stop accruing immediately after you unstake. The unstaking process takes %s. Rebond diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index c4b9b8fad1..07357b9c71 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -148,8 +148,8 @@ internal class DefaultStakingRepository( val yield = getYield(cryptoCurrencyId, symbol) StakingEntryInfo( - interestRate = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), - periodInDays = yield.metadata.cooldownPeriod.days, + apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), + rewardSchedule = yield.metadata.rewardSchedule, tokenSymbol = yield.token.symbol, ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt index 691aece740..c400337e61 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt @@ -1,9 +1,10 @@ package com.tangem.domain.staking.model +import com.tangem.domain.staking.model.stakekit.Yield import java.math.BigDecimal data class StakingEntryInfo( - val interestRate: BigDecimal, - val periodInDays: Int, + val apr: BigDecimal, + val rewardSchedule: Yield.Metadata.RewardSchedule, val tokenSymbol: String, ) \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index a19fea75b9..59137dec80 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -32,9 +32,9 @@ class MockStakingRepository : StakingRepository { override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo = StakingEntryInfo( - interestRate = 1.toBigDecimal(), - periodInDays = 2, + apr = 1.toBigDecimal(), tokenSymbol = "SOL", + rewardSchedule = Yield.Metadata.RewardSchedule.DAY, ) override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = Yield( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt index ad93ecd3a3..d74bc8fe32 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt @@ -6,7 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal @Immutable -sealed class FeeState { +internal sealed class FeeState { data class Content( val fee: Fee?, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt index babc93a2dd..29ce1f54f5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state -enum class InnerConfirmationStakingState { +internal enum class InnerConfirmationStakingState { ASSENT, IN_PROGRESS, COMPLETED, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index c338f2c047..f9da881678 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -1,10 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.features.staking.impl.R internal sealed class StakingNotification(val config: NotificationConfig) { @@ -50,14 +47,10 @@ internal sealed class StakingNotification(val config: NotificationConfig) { ), ) { data class EarnRewards( - val subtitleResourceId: Int, - val currencyName: String, + val subtitleText: TextReference, ) : Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), - subtitle = resourceReference( - subtitleResourceId, - wrappedList(currencyName), - ), + subtitle = subtitleText, ) data class Unstake( @@ -66,7 +59,13 @@ internal sealed class StakingNotification(val config: NotificationConfig) { title = resourceReference(R.string.common_unstake), subtitle = resourceReference( R.string.staking_notification_unstake_text, - wrappedList(cooldownPeriodDays, cooldownPeriodDays), + wrappedList( + pluralReference( + id = R.plurals.common_days, + count = cooldownPeriodDays, + formatArgs = wrappedList(cooldownPeriodDays), + ), + ), ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 4b000bc130..0d7e1081b8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -9,8 +9,8 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt new file mode 100644 index 0000000000..b923734491 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt @@ -0,0 +1,9 @@ +package com.tangem.features.staking.impl.presentation.state.bottomsheet + +internal enum class InfoType { + ANNUAL_PERCENTAGE_RATE, + UNBONDING_PERIOD, + REWARD_CLAIMING, + WARMUP_PERIOD, + REWARD_SCHEDULE, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt index f8cc06e495..12b327a0d0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.bottomsheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference -data class StakingInfoBottomSheetConfig( +internal data class StakingInfoBottomSheetConfig( val title: TextReference, val text: TextReference, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 807e584ca3..f8a64a3695 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -3,6 +3,8 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType.Coin import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R @@ -81,8 +83,10 @@ internal object ConfirmationStatePreviewData { footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", notifications = persistentListOf( StakingNotification.Warning.EarnRewards( - currencyName = "Solana", - subtitleResourceId = R.string.staking_notification_earn_rewards_text_period_day, + subtitleText = resourceReference( + id = R.string.staking_notification_earn_rewards_text_period_day, + formatArgs = wrappedList("Solana"), + ), ), ), transactionDoneState = TransactionDoneState.Empty, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 55d130824f..7220af1f41 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -4,12 +4,12 @@ import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList @Suppress("TooManyFunctions") -object StakingClickIntentsStub : StakingClickIntents { +internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 1e77078e6e..07ba1f9fce 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,5 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R @@ -45,8 +47,10 @@ internal class SetConfirmationStateLoadingTransformer( ) } else { StakingNotification.Warning.EarnRewards( - currencyName = yield.token.name, - subtitleResourceId = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + subtitleText = resourceReference( + id = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + formatArgs = wrappedList(yield.token.name), + ), ) }, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 8d8a69e019..2c0bcbef78 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -18,6 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index 06f89bdd94..f0e0ade2cc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig import com.tangem.utils.transformer.Transformer @@ -42,12 +43,4 @@ internal class ShowInfoBottomSheetStateTransformer( ), ) } -} - -enum class InfoType { - ANNUAL_PERCENTAGE_RATE, - UNBONDING_PERIOD, - REWARD_CLAIMING, - WARMUP_PERIOD, - REWARD_SCHEDULE, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index b87f8a0251..098e959f7c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -8,8 +8,8 @@ import com.tangem.utils.transformer.Transformer internal class AmountChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val yield: Yield, private val value: String, + yield: Yield, ) : Transformer { private val amountRequirementStateTransformer = AmountRequirementStateTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index ae53eab63e..6314597d0d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.utils.transformer.Transformer internal class AmountMaxValueStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val yield: Yield, + yield: Yield, ) : Transformer { private val amountRequirementStateTransformer = AmountRequirementStateTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index f8fb30cfa9..fee1e2115d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 209c6c9304..3dbc94500f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -43,6 +43,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer @@ -327,7 +328,7 @@ internal class StakingViewModel @Inject constructor( } override fun onInitialInfoBannerClick() { - // innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) + innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } override fun onInfoClick(infoType: InfoType) { @@ -339,7 +340,7 @@ internal class StakingViewModel @Inject constructor( } override fun onAmountValueChange(value: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, value)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value, yield)) } override fun onAmountPasteTriggerDismiss() { @@ -369,7 +370,7 @@ internal class StakingViewModel @Inject constructor( StakingActionCommonType.PENDING_OTHER } stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) onNextClick(actionType, activeStake.pendingActions) } @@ -696,7 +697,7 @@ internal class StakingViewModel @Inject constructor( } private companion object { - const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking" + const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L const val BALANCE_UPDATE_DELAY = 11_000L } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index e7625b4013..9c69cd0b55 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -143,9 +143,14 @@ internal object TokenDetailsPreviewData { val stakingErrorBlock = StakingBlockUM.Error(iconState) val stakingAvailableBlock = StakingBlockUM.StakeAvailable( - interestRate = "7.38", - periodInDays = 4, - tokenSymbol = "XLM", + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList("3.27%"), + ), + subtitleText = resourceReference( + id = R.string.staking_notification_earn_rewards_text_period_day, + formatArgs = wrappedList("Solana"), + ), iconState = iconState, onStakeClicked = {}, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt index 91d76db70d..b29d476a32 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt @@ -21,9 +21,8 @@ internal sealed interface StakingBlockUM { data class StakeAvailable( val iconState: IconState, - val interestRate: String, - val periodInDays: Int, - val tokenSymbol: String, + val titleText: TextReference, + val subtitleText: TextReference, val onStakeClicked: () -> Unit, ) : StakingBlockUM } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 6aa3923cd9..0555333c41 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -22,6 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R @@ -191,13 +192,19 @@ internal class TokenDetailsLoadedBalanceConverter( stakingEntryInfo: StakingEntryInfo, iconState: IconState, ): StakingBlockUM.StakeAvailable { + val apr = BigDecimalFormatter.formatPercent( + percent = stakingEntryInfo.apr, + useAbsoluteValue = true, + ) return StakingBlockUM.StakeAvailable( - interestRate = BigDecimalFormatter.formatPercent( - percent = stakingEntryInfo.interestRate, - useAbsoluteValue = true, + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList(apr), + ), + subtitleText = resourceReference( + id = stakingEntryInfo.rewardSchedule.getStringResourceId(), + formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), ), - periodInDays = stakingEntryInfo.periodInDays, - tokenSymbol = stakingEntryInfo.tokenSymbol, iconState = iconState, onStakeClicked = clickIntents::onStakeBannerClick, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 3bc3e36023..b12e12eb53 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -1,12 +1,16 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -24,14 +28,20 @@ internal class TokenStakingStateConverter( ifLeft = { StakingBlockUM.Error(iconState = iconState) }, - ifRight = { + ifRight = { stakingEntryInfo -> + val apr = BigDecimalFormatter.formatPercent( + percent = stakingEntryInfo.apr, + useAbsoluteValue = true, + ) StakingBlockUM.StakeAvailable( - interestRate = BigDecimalFormatter.formatPercent( - percent = it.interestRate, - useAbsoluteValue = true, + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList(apr), + ), + subtitleText = resourceReference( + id = stakingEntryInfo.rewardSchedule.getStringResourceId(), + formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), ), - periodInDays = it.periodInDays, - tokenSymbol = it.tokenSymbol, iconState = iconState, onStakeClicked = clickIntents::onStakeBannerClick, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt index faa0144943..fe714ff2ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt @@ -3,7 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType import java.math.BigDecimal -fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { +internal fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) { this.plus(stakingAmount) } else { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt new file mode 100644 index 0000000000..8bb6550f79 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils + +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.features.tokendetails.impl.R + +internal fun Yield.Metadata.RewardSchedule.getStringResourceId(): Int { + return when (this) { + Yield.Metadata.RewardSchedule.BLOCK, + Yield.Metadata.RewardSchedule.DAY, + Yield.Metadata.RewardSchedule.ERA, + Yield.Metadata.RewardSchedule.EPOCH, + -> R.string.staking_notification_earn_rewards_text_period_day + + Yield.Metadata.RewardSchedule.HOUR, + -> R.string.staking_notification_earn_rewards_text_period_hour + + Yield.Metadata.RewardSchedule.WEEK, + -> R.string.staking_notification_earn_rewards_text_period_week + + Yield.Metadata.RewardSchedule.MONTH, + -> R.string.staking_notification_earn_rewards_text_period_month + + else + -> R.string.staking_notification_earn_rewards_text_period_day + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index c2d7940cbc..9d0087292d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.getGreyScaleColorFilter @@ -86,10 +88,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi SpacerW8() Column { Text( - text = stringResource( - R.string.token_details_staking_block_title, - state.interestRate, - ), + text = state.titleText.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, ) @@ -97,11 +96,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) Text( - text = stringResource( - R.string.token_details_staking_block_subtitle, - state.tokenSymbol, - state.periodInDays, - ), + text = state.subtitleText.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) @@ -161,8 +156,9 @@ private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) } SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = "Loading", // TODO staking - onClick = { /* [REDACTED_TODO_COMMENT] */ }, + showProgress = true, + text = TextReference.EMPTY.resolveReference(), + onClick = { /* no-op */ }, ) } } From cb203637f5d285fc1ef7299eefbc807cbef0b07a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Aug 2024 21:03:42 +0500 Subject: [PATCH 24/41] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 3 + core/res/src/main/res/values/strings.xml | 14 ++++- .../converters/YieldBalanceConverter.kt | 1 + .../staking/model/stakekit/YieldBalance.kt | 2 + .../converters/YieldBalancesConverter.kt | 43 ++++++++++---- .../ui/StakingInitialInfoContent.kt | 56 ++++++++++--------- 6 files changed, 81 insertions(+), 38 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index cc74abea34..d7bd63adac 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -100,6 +100,7 @@ Aktivieren Aktiviert Fehler + Umtausch Erkunden Transaktionsverlauf einsehen Explorer @@ -878,6 +879,8 @@ Auf dieser Karte sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben. Geringe Anzahl von Unterschriften Token in verschiedenen Netzwerken können unterschiedliche Adressen haben. Überprüfe bei der Überweisung noch einmal, ob deine Adresse mit der des Netzwerks übereinstimmt. + MATIC wird auf POL migriert. Es gibt jedoch keine Frist, und MATIC wird noch nicht abgeschafft. Du kannst MATIC-Token weiterhin verwenden oder sie über eien Exchange gegen POL tauschen. + Migration von MATIC zu POL Verwende deine Karte, um eine Adresse für das %d-Netz zu erhalten Verwende deine Karte, um mehrere Adressen für die %d-Netzwerke zu erhalten diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 08e26457c7..86185e2839 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -114,6 +114,10 @@ Go to provider Go to token Import + + in %d day + in %d days + Later Locked Main network @@ -656,12 +660,12 @@ Market rating Metrics Minimum Requirement - No rewards to claim + No rewards Reward claiming Method of receiving staking rewards.\nIt can be either automatic, where the reward is credited to your address, or manual, where you need to withdraw the reward by creating a transaction to receive it. Reward schedule This is a schedule that determines when participants in staking receive their rewards. - Rewards to claim: %s + Rewards: %s Staking %s Unbonding Period The period you must wait after requesting to withdraw funds from staking before the tokens become available. @@ -674,7 +678,8 @@ Staking allows you to earn %1$s. Your staking rewards arrive every month. Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards - Rewards stop accruing immediately after you unstake. The unstaking process takes %s. + Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. + Ready to withdraw Rebond Restake Restake rewards @@ -695,6 +700,7 @@ Stake more Stake %s Unstake %s + Unbonding Unlock locked Unstaked Check unstaked to claim your assets @@ -876,6 +882,8 @@ Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. + MATIC is being migrated to POL. However there is no deadline set and MATIC isn\'t being deprecated yet. You can safely continue using MATIC token or use exchanges to swap it for POL. + MATIC to POL Migration Use your card to get an address for %d network Use your card to get an addresses for %d networks diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index b1c9d7d47c..ec86b52e73 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -26,6 +26,7 @@ internal class YieldBalanceConverter : Converter, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 85b64c0645..9be3086028 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -1,10 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.common.extensions.isZero -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -18,6 +15,8 @@ import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.toPersistentList +import org.joda.time.DateTime +import java.util.Calendar internal class YieldBalancesConverter( private val cryptoCurrencyStatusProvider: Provider, @@ -88,7 +87,7 @@ internal class YieldBalancesConverter( } val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - val unbondingPeriod = yield.metadata.cooldownPeriod.days + val unbonding = getUnbondingDate(balance.date) validator?.let { BalanceState( validator = validator, @@ -108,11 +107,7 @@ internal class YieldBalancesConverter( ), ), rawCurrencyId = balance.rawCurrencyId, - unbondingPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(unbondingPeriod), - ), + unbondingPeriod = unbonding, pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -153,4 +148,32 @@ internal class YieldBalancesConverter( BalanceType.UNKNOWN, -> false } + + private fun getUnbondingDate(date: DateTime?): TextReference { + val now = DateTime.now().millis + val nowCalendar = Calendar.getInstance() + nowCalendar.resetHours() + + val endDate = Calendar.getInstance() + endDate.timeInMillis = date?.millis ?: now + endDate.resetHours() + + val days = ((endDate.timeInMillis - nowCalendar.timeInMillis) / DAY_IN_MILLIS).toInt() + return if (days > 0) { + pluralReference(R.plurals.common_in_days, days, wrappedList(days)) + } else { + resourceReference(R.string.common_today) + } + } + + private fun Calendar.resetHours() { + this[Calendar.HOUR_OF_DAY] = 0 + this[Calendar.MINUTE] = 0 + this[Calendar.SECOND] = 0 + this[Calendar.MILLISECOND] = 0 + } + + private companion object { + const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000 + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 6174dcc79c..8bd4543778 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -232,31 +232,37 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli @Composable private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextReference { - return if (balanceType == BalanceType.UNSTAKING) { - combinedReference( - resourceReference(R.string.staking_details_unbonding_period), - annotatedReference { - appendSpace() - appendColored( - text = balance.unbondingPeriod.resolveReference(), - color = TangemTheme.colors.text.accent, - ) - }, - ) - } else { - combinedReference( - resourceReference(R.string.app_name), - annotatedReference { - appendSpace() - appendColored( - text = BigDecimalFormatter.formatPercent( - percent = balance.validator.apr.orZero(), - useAbsoluteValue = true, - ), - color = TangemTheme.colors.text.accent, - ) - }, - ) + return when (balanceType) { + BalanceType.UNSTAKING -> { + combinedReference( + resourceReference(R.string.staking_unbonding), + annotatedReference { + appendSpace() + appendColored( + text = balance.unbondingPeriod.resolveReference(), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } + BalanceType.UNSTAKED -> { + resourceReference(R.string.staking_ready_to_withdraw) + } + else -> { + combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent( + percent = balance.validator.apr.orZero(), + useAbsoluteValue = true, + ), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } } } From 56992e44313f223a39030f8558d545813ce0750d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Aug 2024 15:00:24 +0400 Subject: [PATCH 25/41] Updated on 2026-08-14 --- .../portfolio/impl/ui/PortfolioItem.kt | 330 ++++-------------- .../preview/PreviewMyPortfolioUMProvider.kt | 23 +- .../impl/ui/state/PortfolioTokenUM.kt | 32 +- 3 files changed, 91 insertions(+), 294 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt index 3a98293707..035d1dbb91 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -1,58 +1,26 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.StringsSigns +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState -// TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement) -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - Column(modifier) { - Row( - modifier = Modifier - .combinedClickable( - onClick = { - hapticManager.perform(TangemHapticEffect.View.ContextClick) - state.onClick() - }, - onLongClick = { - hapticManager.perform(TangemHapticEffect.View.LongPress) - state.onLongTap() - }, - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .padding( - vertical = TangemTheme.dimens.spacing15, - horizontal = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Content(state) - } + TokenItem(state = state.tokenItemState, isBalanceHidden = state.isBalanceHidden) PortfolioQuickActions( modifier = Modifier.padding( @@ -68,225 +36,77 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie } } -@Composable -private fun RowScope.Content(state: PortfolioTokenUM) { - // TODO add custom token - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, // TODO add disabled state - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - - Column( - modifier = Modifier.align(Alignment.CenterVertically), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - when (state.balanceContent) { - is PortfolioTokenUM.BalanceContent.Disabled -> { - Disabled( - state = state, - disabledText = state.balanceContent.text.resolveReference(), - ) - } - PortfolioTokenUM.BalanceContent.Loading -> { - Loading(state = state) - } - is PortfolioTokenUM.BalanceContent.TokenBalance -> { - TokenBalance( - state = state, - content = state.balanceContent, - ) - } - } - } -} - -@Composable -private fun ColumnScope.TokenBalance(state: PortfolioTokenUM, content: PortfolioTokenUM.BalanceContent.TokenBalance) { - val balance = if (content.hidden) { - StringsSigns.STARS - } else { - content.balance - } - val tokenAmount = if (content.hidden) { - StringsSigns.STARS - } else { - content.tokenAmount - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.alignByBaseline(), - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.alignByBaseline(), - text = balance, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Text( - text = tokenAmount, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -private fun ColumnScope.Loading(state: PortfolioTokenUM) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.alignByBaseline(), - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - TextShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size40) - .alignByBaseline(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - TextShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size40) - .alignByBaseline(), - style = TangemTheme.typography.caption2, - textSizeHeight = true, - ) - } -} - -@Composable -private fun Disabled(state: PortfolioTokenUM, disabledText: String, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - Modifier.weight(1f), - ) { - Text( - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - - Text( - text = disabledText, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview() { +private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) { TangemThemePreview { - var quickActionsShown by remember { mutableStateOf(false) } - var quickActionsShown2 by remember { mutableStateOf(false) } - val sampleToken = PreviewMyPortfolioUMProvider().sampleToken + var quickActionsShown by remember { mutableStateOf(value = false) } - Box( - Modifier - .fillMaxSize() - .background(TangemTheme.colors.background.primary), - ) { - Column { - PortfolioItem( - state = sampleToken - .copy( - onClick = { - if (quickActionsShown2) { - quickActionsShown2 = false - } - quickActionsShown = quickActionsShown.not() - }, - isQuickActionsShown = quickActionsShown, - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - onClick = { - if (quickActionsShown) { - quickActionsShown = false - } - quickActionsShown2 = quickActionsShown2.not() - }, - isQuickActionsShown = quickActionsShown2, - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - balanceContent = ( - sampleToken.balanceContent - as PortfolioTokenUM.BalanceContent.TokenBalance - ) - .copy(hidden = true), - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - balanceContent = PortfolioTokenUM.BalanceContent.Disabled( - stringReference("No Address"), - ), - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy(balanceContent = PortfolioTokenUM.BalanceContent.Loading), - lastInList = true, - ) - } + val onItemClick = { + quickActionsShown = quickActionsShown.not() } + + PortfolioItem( + state = tokenUM.copy( + tokenItemState = when (tokenUM.tokenItemState) { + is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + else -> tokenUM.tokenItemState + }, + isQuickActionsShown = quickActionsShown, + ), + lastInList = true, + ) + } +} + +private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider( + collection = listOf( + tokenUM.copy( + tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy( + fiatAmountState = contentFiatAmount.copy(hasStaked = true), + ), + ), + tokenUM.copy( + tokenItemState = tokenUM.tokenItemState.copy( + fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN), + cryptoAmountState = tokenUM.tokenItemState.cryptoAmountState.copy(text = DASH_SIGN), + ), + ), + tokenUM.copy(isBalanceHidden = true), + tokenUM.copy( + tokenItemState = TokenItemState.Unreachable( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemClick = {}, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.NoAddress( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.Loading( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content, + subtitleState = tokenUM.tokenItemState.subtitleState, + ), + ), + ), +) { + + companion object { + val tokenUM = PreviewMyPortfolioUMProvider().sampleToken + val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as TokenFiatAmountState.Content } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index b607386001..1f21b46f5b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -1,6 +1,8 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import kotlinx.collections.immutable.persistentListOf @@ -32,19 +34,18 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider