From 20eb597613325c26ff00d7e35729ee92fb585d4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 20:29:25 +0500 Subject: [PATCH 001/197] 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 002/197] 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 003/197] 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 004/197] 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 005/197] 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 006/197] 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 007/197] 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 008/197] 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 009/197] 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 010/197] 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 011/197] 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 012/197] 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 013/197] 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 014/197] 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 015/197] 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 016/197] 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 017/197] 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 018/197] 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 019/197] 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 020/197] 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 021/197] 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 022/197] 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 023/197] 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 024/197] 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 025/197] Updated on 2026-08-14 --- .../portfolio/impl/ui/PortfolioItem.kt | 330 ++++-------------- .../preview/PreviewMyPortfolioUMProvider.kt | 23 +- .../impl/ui/state/PortfolioTokenUM.kt | 32 +- 3 files changed, 91 insertions(+), 294 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt index 3a98293707..035d1dbb91 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -1,58 +1,26 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.StringsSigns +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState -// TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement) -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - Column(modifier) { - Row( - modifier = Modifier - .combinedClickable( - onClick = { - hapticManager.perform(TangemHapticEffect.View.ContextClick) - state.onClick() - }, - onLongClick = { - hapticManager.perform(TangemHapticEffect.View.LongPress) - state.onLongTap() - }, - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .padding( - vertical = TangemTheme.dimens.spacing15, - horizontal = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Content(state) - } + TokenItem(state = state.tokenItemState, isBalanceHidden = state.isBalanceHidden) PortfolioQuickActions( modifier = Modifier.padding( @@ -68,225 +36,77 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie } } -@Composable -private fun RowScope.Content(state: PortfolioTokenUM) { - // TODO add custom token - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, // TODO add disabled state - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - - Column( - modifier = Modifier.align(Alignment.CenterVertically), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - when (state.balanceContent) { - is PortfolioTokenUM.BalanceContent.Disabled -> { - Disabled( - state = state, - disabledText = state.balanceContent.text.resolveReference(), - ) - } - PortfolioTokenUM.BalanceContent.Loading -> { - Loading(state = state) - } - is PortfolioTokenUM.BalanceContent.TokenBalance -> { - TokenBalance( - state = state, - content = state.balanceContent, - ) - } - } - } -} - -@Composable -private fun ColumnScope.TokenBalance(state: PortfolioTokenUM, content: PortfolioTokenUM.BalanceContent.TokenBalance) { - val balance = if (content.hidden) { - StringsSigns.STARS - } else { - content.balance - } - val tokenAmount = if (content.hidden) { - StringsSigns.STARS - } else { - content.tokenAmount - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.alignByBaseline(), - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.alignByBaseline(), - text = balance, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Text( - text = tokenAmount, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -private fun ColumnScope.Loading(state: PortfolioTokenUM) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.alignByBaseline(), - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - TextShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size40) - .alignByBaseline(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - TextShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size40) - .alignByBaseline(), - style = TangemTheme.typography.caption2, - textSizeHeight = true, - ) - } -} - -@Composable -private fun Disabled(state: PortfolioTokenUM, disabledText: String, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - Modifier.weight(1f), - ) { - Text( - text = state.title, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - - Text( - text = disabledText, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview() { +private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) { TangemThemePreview { - var quickActionsShown by remember { mutableStateOf(false) } - var quickActionsShown2 by remember { mutableStateOf(false) } - val sampleToken = PreviewMyPortfolioUMProvider().sampleToken + var quickActionsShown by remember { mutableStateOf(value = false) } - Box( - Modifier - .fillMaxSize() - .background(TangemTheme.colors.background.primary), - ) { - Column { - PortfolioItem( - state = sampleToken - .copy( - onClick = { - if (quickActionsShown2) { - quickActionsShown2 = false - } - quickActionsShown = quickActionsShown.not() - }, - isQuickActionsShown = quickActionsShown, - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - onClick = { - if (quickActionsShown) { - quickActionsShown = false - } - quickActionsShown2 = quickActionsShown2.not() - }, - isQuickActionsShown = quickActionsShown2, - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - balanceContent = ( - sampleToken.balanceContent - as PortfolioTokenUM.BalanceContent.TokenBalance - ) - .copy(hidden = true), - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy( - balanceContent = PortfolioTokenUM.BalanceContent.Disabled( - stringReference("No Address"), - ), - ), - lastInList = true, - ) - PortfolioItem( - state = sampleToken - .copy(balanceContent = PortfolioTokenUM.BalanceContent.Loading), - lastInList = true, - ) - } + val onItemClick = { + quickActionsShown = quickActionsShown.not() } + + PortfolioItem( + state = tokenUM.copy( + tokenItemState = when (tokenUM.tokenItemState) { + is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + else -> tokenUM.tokenItemState + }, + isQuickActionsShown = quickActionsShown, + ), + lastInList = true, + ) + } +} + +private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider( + collection = listOf( + tokenUM.copy( + tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy( + fiatAmountState = contentFiatAmount.copy(hasStaked = true), + ), + ), + tokenUM.copy( + tokenItemState = tokenUM.tokenItemState.copy( + fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN), + cryptoAmountState = tokenUM.tokenItemState.cryptoAmountState.copy(text = DASH_SIGN), + ), + ), + tokenUM.copy(isBalanceHidden = true), + tokenUM.copy( + tokenItemState = TokenItemState.Unreachable( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemClick = {}, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.NoAddress( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.Loading( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content, + subtitleState = tokenUM.tokenItemState.subtitleState, + ), + ), + ), +) { + + companion object { + val tokenUM = PreviewMyPortfolioUMProvider().sampleToken + val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as TokenFiatAmountState.Content } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index b607386001..1f21b46f5b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -1,6 +1,8 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import kotlinx.collections.immutable.persistentListOf @@ -32,19 +34,18 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider - Сайт блокчейна + Веб-сайт Покупательское предпочтение Разница между объемом покупателей и продавцов Циркулирующее предложение @@ -577,18 +583,11 @@ Комиссия, которую нужно заплатить за использование каждого неиспользованного выхода транзакции (UTXO) в сети Kaspa. Чем больше UTXO вы используете в транзакции, тем выше будет комиссия. KAS за UTXO %1$s, %2$s - Адрес Код назначения Введите адрес Адрес совпадает с адресом кошелька - Недопустимый Tag. Он не будет добавлен в транзакцию. Недопустимый Memo. Он не будет добавлен в транзакцию. - Tag Memo - Включая комиссию - Низкая - Нормальная - Приоритетная Проверьте своё интернет соединение Информация о комиссии сети недоступна Из @@ -603,7 +602,7 @@ Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе. + Для сохранения вашего аккаунта в блокчейне и защиты от возможных рисков необходим баланс не менее %s. Эта сумма останется на вашем счете и не может быть снята. Экзистенциальный депозит Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Установлена высокая комиссия @@ -637,21 +636,15 @@ Отправка %s Вы отправляете **%1$s**, включая комиссию сети %2$s Вы отправляете **%1$s** и %2$s - Отправка %s - Всего - %1$s и %2$s будет отправлено - ≈ %1$s (вкл. комиссию: %2$s) - %s будет отправлено Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. - Неверный адрес Транзакция отправлена Подготовьтесь к сканированию карты, которую вы хотите настроить. Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя Активно - Для завершения стейкинга нажмите сюда + Для завершения стейкинга нажмите на блок выше Сумма для стейкинга должна быть не менее %s Забрать средства Годовая процентная ставка @@ -682,12 +675,12 @@ Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый месяц. Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг - Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. + Награда перестанет начисляться сразу после того, как вы начнете процесс завершения стейкинга. Процесс завершения длится %s. Сменить валидатора Застейкать вознаграждения Отозвать Переголосовать - Автоматически + Авто Вручную Блок День @@ -859,6 +852,8 @@ Для работы с сетью необходим депозит Обмен будет доступен после завершения %s транзакции У вас есть активная транзакция + Разрешение обмена в процессе и будет скоро завершено + Разрешение в процессе Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. У вас в списке нет монет доступных для обмена с %s Нет доступных для обмена токенов @@ -881,6 +876,8 @@ На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. Малое количество подписей Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. + Токен MATIC мигрирует на POL. Однако крайний срок не установлен, и MATIC пока не устарел. Вы можете спокойно продолжать использовать токен MATIC или воспользоваться биржами, чтобы обменять его на POL. + Миграция MATIC в POL Используйте вашу карту, чтобы получить адрес для %d сети Используйте вашу карту, чтобы получить адреса для %d сетей diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 329dcfb63e..fce9968300 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -104,7 +104,6 @@ Explore Explore transaction history Explorer - Fee Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s Fast Market @@ -173,6 +172,8 @@ Contract address Contract address is invalid Please select the network + This token has already been added to the list + Token already exists Decimal must be a valid integer, up to %li Custom derivation E. g. m/00\'/0000\'/0\'/0/0 @@ -348,6 +349,7 @@ My portfolio Market To generate addresses for selected networks, you must scan your Tangem Wallet card + This section’s data is sourced from the following networks: %s Unable to load the data… Quick actions Result @@ -374,7 +376,7 @@ Based on %d rating Based on %d ratings - Blockchain site + Website Buy pressure The difference between buyers volume and sellers volume Circulating supply @@ -572,18 +574,11 @@ The fee required for using each unspent transaction output (UTXO) in the Kaspa network. The more UTXOs you use in a transaction, the higher the fee will be. KAS per UTXO %1$s, %2$s - Address Destination Tag Enter address Address is the same as wallet address - Invalid Tag. It won\'t be added to the transaction. Invalid Memo. It won\'t be added to the transaction. - Tag Memo - Include fee - Low - Normal - Priority Check your network connection Network fee info unreachable From @@ -598,7 +593,7 @@ Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - The account will be wiped from the blockchain if a balance goes below the existential deposit. Please leave %s on your balance. + A balance of at least %s is required to keep your account on the blockchain to prevent security risks. This amount will remain in your balance and cannot be withdrawn. Existential deposit The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. Custom fee is high @@ -632,21 +627,15 @@ Send %s You are sending **%1$s** including a network fee of %2$s You are sending **%1$s** and %2$s - Sending %s - Total - %1$s and %2$s will be sent - ≈ %1$s (inc. fee: %2$s) - %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. - Invalid address Transaction sent Prepare to scan card you want to setup. Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name Active - To unstake your assets, click here. + To unstake your assets, tap the block above The amount to stake must be at least %s Claim unstaked Annual percentage rate diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 8a455b1b25..187e4090fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -101,7 +101,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { title = resourceReference(R.string.send_notification_existential_deposit_title), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), + text = resourceReference(R.string.common_ok), onClick = onConfirmClick, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 5f05279157..f31136c8dd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -475,10 +475,7 @@ internal class StateBuilder( ), iconResId = R.drawable.ic_alert_circle_24, buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference( - R.string.send_notification_leave_button, - wrappedList(deposit), - ), + text = resourceReference(R.string.common_ok), onClick = { actions.onLeaveExistentialDeposit( SwapAmount( From 6f80bdb6367fc60a5047cc9e2a5bbcff677e6c42 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 18:17:33 +0300 Subject: [PATCH 046/197] Updated on 2026-08-14 --- .../sdk/mocks/content/WalletMockContent.kt | 11 +++++++ .../presentation/state/fee/FeeStateFactory.kt | 29 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt index 8e059f5450..6168bdda1f 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -111,6 +111,10 @@ object WalletMockContent : MockContent { publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), ), extendedPublicKey = ExtendedPublicKey( publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), @@ -189,6 +193,13 @@ object WalletMockContent : MockContent { parentFingerprint = byteArrayOf(0, 0, 0, 0), childNumber = 0, ), + DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), ), ), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 3a30e424b9..3ff511170b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -1,10 +1,12 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.extensions.isZero +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.features.send.impl.presentation.state.SendNotification @@ -15,6 +17,7 @@ import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import com.tangem.blockchain.common.AmountType as SdkAmountType /** * Factory to produce fee state for [SendUiState] @@ -94,7 +97,7 @@ internal class FeeStateFactory( feeState = feeState.copy( feeSelectorState = updatedFeeSelectorState, fee = fee, - isFeeApproximate = isFeeApproximate(fee), + isFeeApproximate = isFeeApproximate(state.amountState), ), ) } @@ -180,11 +183,29 @@ internal class FeeStateFactory( return noErrors && (isNotEmptyCustom || isNotCustom) } - private fun isFeeApproximate(fee: Fee): Boolean { + private fun isFeeApproximate(state: AmountState): Boolean { val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false + val amount = (state as? AmountState.Data)?.amountTextField?.cryptoAmount ?: return false return isFeeApproximateUseCase( networkId = cryptoCurrencyStatus.currency.network.id, - amountType = fee.amount.type, + amountType = amount.type.toSdkAmountType(), ) } + + private fun AmountType.toSdkAmountType(): SdkAmountType { + return when (this) { + AmountType.CoinType -> SdkAmountType.Coin + is AmountType.FiatType -> error("unsupported type FiatType") + AmountType.ReserveType -> SdkAmountType.Reserve + is AmountType.TokenType -> SdkAmountType.Token( + Token( + name = this.token.name, + symbol = this.token.symbol, + contractAddress = this.token.contractAddress, + decimals = this.token.decimals, + id = this.token.id.rawCurrencyId, + ), + ) + } + } } \ No newline at end of file From 5f338d293bb24459f78682b165e233313a6dac4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 22:26:45 +0300 Subject: [PATCH 047/197] Updated on 2026-08-14 --- .../impl/ui/AddToPortfolioBottomSheet.kt | 4 +- .../markets/portfolio/impl/ui/MyPortfolio.kt | 4 +- .../impl/ui/TokenActionsBottomSheet.kt | 10 +- .../impl/ui/WalletSelectorBottomSheet.kt | 142 ++++++++++++++++++ .../PreviewAddToPortfolioBSContentProvider.kt | 44 +++--- .../ui/state/AddToPortfolioBSContentUM.kt | 3 + .../portfolio/impl/ui/state/MyPortfolioUM.kt | 8 + ...SContent.kt => TokenActionsBSContentUM.kt} | 2 +- .../ui/state/WalletSelectorBSContentUM.kt | 11 ++ 9 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/{TokenActionsBSContent.kt => TokenActionsBSContentUM.kt} (95%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index e9b760f529..422bcdb678 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -55,8 +55,10 @@ internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { ) { Content( modifier = Modifier.fillMaxWidth(), - state = config.content as AddToPortfolioBSContentUM, + state = it, ) + + WalletSelectorBottomSheet(it.walletSelectorConfig) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 2acf18d893..88436fa864 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -69,6 +69,8 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { MyPortfolioUM.Unavailable -> UnavailableContent() } } + + AddToPortfolioBottomSheet(state.addToPortfolioBS) } @Composable @@ -112,7 +114,7 @@ private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: M verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { Text( - text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network", + text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network", // FIXME style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt index cfc6c814d1..bc725d98f3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -18,12 +18,12 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.rows.CornersToRound import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContent +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import kotlinx.collections.immutable.toImmutableList @Composable fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( + TangemBottomSheet( config = config, title = { content -> TangemBottomSheetTitle(content.title) @@ -34,7 +34,7 @@ fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { } @Composable -private fun Content(content: TokenActionsBSContent) { +private fun Content(content: TokenActionsBSContentUM) { Column( modifier = Modifier .padding( @@ -78,9 +78,9 @@ private fun Preview() { TangemBottomSheetConfig( isShow = true, onDismissRequest = {}, - content = TokenActionsBSContent( + content = TokenActionsBSContentUM( title = "Wallet 1", - actions = TokenActionsBSContent.Action.entries.toImmutableList(), + actions = TokenActionsBSContentUM.Action.entries.toImmutableList(), onActionClick = {}, ), ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt new file mode 100644 index 0000000000..a9e190928f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt @@ -0,0 +1,142 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider +import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + addBottomInsets = false, + title = { content -> + TangemTopAppBar( + title = resourceReference(R.string.manage_tokens_wallet_selector_title), + titleAlignment = Alignment.CenterHorizontally, + startButton = TopAppBarButtonUM.Back(content.onBack), + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) + }, + ) { content -> + Content( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing8, + ), + state = content, + ) + } +} + +@Composable +private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Column( + modifier = modifier + .verticalScroll(rememberScrollState()), + ) { + BlockCard( + modifier = Modifier.fillMaxSize(), + colors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) { + state.userWallets.forEach { state -> + key(state.id) { + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + state = state, + ) + } + } + } + SpacerH(bottomBarHeight) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + WalletSelectorBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = WalletSelectorBSContentUM( + userWallets = persistentListOf( + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.Checkmark, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + ), + onBack = {}, + onWalletSelected = {}, + ), + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewContent() { + TangemThemePreview { + Content( + state = WalletSelectorBSContentUM( + userWallets = persistentListOf( + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.Checkmark, + ), + PreviewAddToPortfolioBSContentProvider().userWallet.copy( + endIcon = UserWalletItemUM.EndIcon.None, + ), + ), + onBack = {}, + onWalletSelected = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index d184d3aac6..d218f4f8db 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -2,6 +2,8 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId @@ -20,18 +22,20 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider isSelected = false, ) + val userWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ) + override val values: Sequence get() = sequenceOf( AddToPortfolioBSContentUM( - selectedWallet = UserWalletItemUM( - id = UserWalletId("1"), - name = stringReference("Wallet 1"), - information = stringReference("3 cards, 10,123$"), - imageUrl = "", - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ), + selectedWallet = userWallet, selectNetworkUM = SelectNetworkUM( tokenId = "etherium", tokenName = "Etherium", @@ -51,17 +55,15 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider isScanCardNotificationVisible = true, continueButtonEnabled = true, onContinueButtonClick = {}, + walletSelectorConfig = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), + onWalletClick = {}, ), AddToPortfolioBSContentUM( - selectedWallet = UserWalletItemUM( - id = UserWalletId("1"), - name = stringReference("Wallet 1"), - information = stringReference("3 cards, 10,123$"), - imageUrl = "", - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ), + selectedWallet = userWallet, selectNetworkUM = SelectNetworkUM( tokenId = "etherium", tokenName = "Etherium", @@ -81,6 +83,12 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider isScanCardNotificationVisible = true, continueButtonEnabled = false, onContinueButtonClick = {}, + walletSelectorConfig = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), + onWalletClick = {}, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt index 6b708502ce..7369b5cf69 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.markets.portfolio.impl.ui.state import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent internal data class AddToPortfolioBSContentUM( @@ -9,4 +10,6 @@ internal data class AddToPortfolioBSContentUM( val isScanCardNotificationVisible: Boolean, val continueButtonEnabled: Boolean, val onContinueButtonClick: () -> Unit, + val onWalletClick: () -> Unit, + val walletSelectorConfig: TangemBottomSheetConfig, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt index 7fa4c11a4a..c4f8d45a46 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -1,11 +1,19 @@ package com.tangem.features.markets.portfolio.impl.ui.state import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class MyPortfolioUM { + val addToPortfolioBS: TangemBottomSheetConfig = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ) + data class Tokens( val tokens: ImmutableList, val buttonState: AddButtonState, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt similarity index 95% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt index 2252b38f70..7b2a115f54 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.ImmutableList -internal data class TokenActionsBSContent( +internal data class TokenActionsBSContentUM( val title: String, val actions: ImmutableList, val onActionClick: (Action) -> Unit, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt new file mode 100644 index 0000000000..ee727b3608 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +internal data class WalletSelectorBSContentUM( + val userWallets: ImmutableList, + val onWalletSelected: (UserWalletItemUM) -> Unit, + val onBack: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file From 99176f84ecf32b4d5cf6f2f1468861f61bcc9a5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 16:13:39 +0400 Subject: [PATCH 048/197] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 16 +++++ .../repository/DefaultCurrenciesRepository.kt | 37 +++++++++++ ...AllWalletsCryptoCurrencyStatusesUseCase.kt | 66 +++++++++++++++++++ .../CurrenciesStatusesOperations.kt | 2 +- .../tokens/repository/CurrenciesRepository.kt | 8 ++- .../repository/MockCurrenciesRepository.kt | 5 ++ 6 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 6e5da90b47..078e47c07e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -104,6 +104,22 @@ internal object TokensDomainModule { ) } + @Provides + @Singleton + fun provideGetAllWalletsCryptoCurrencyStatusesUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + stakingRepository: StakingRepository, + ): GetAllWalletsCryptoCurrencyStatusesUseCase { + return GetAllWalletsCryptoCurrencyStatusesUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + } + @Provides @Singleton fun provideGetCurrencyWarningsUseCase( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 5e4c6b350e..83d87b583b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -40,6 +40,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -505,6 +506,42 @@ internal class DefaultCurrenciesRepository( } } + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { + return userWalletsStore.userWallets.flatMapLatest { userWallets -> + val userWalletsWithCurrencies = userWallets + .filterNot(UserWallet::isLocked) + .map { userWallet -> + if (userWallet.isMultiCurrency) { + getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> + val filterResponse = storedTokens.tokens.filter { it.id == currencyRawId } + + responseCurrenciesFactory.createCurrencies( + response = storedTokens.copy(tokens = filterResponse), + scanResponse = userWallet.scanResponse, + ) + } + } else { + flow { + val currency = getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId) + + val currencies = if (currency.id.rawCurrencyId == currencyRawId) { + listOf(currency) + } else { + emptyList() + } + + emit(currencies) + } + } + .map { userWallet to it } + } + + combine(userWalletsWithCurrencies) { it.toMap() } + .onEmpty { emit(value = emptyMap()) } + } + } + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt new file mode 100644 index 0000000000..816433f7be --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +/** + * Get crypto currency statuses by raw ID for all wallets + * + * @property currenciesRepository currencies repository + * @property quotesRepository quotes repository + * @property networksRepository networks repository + * @property stakingRepository staking repository + * +[REDACTED_AUTHOR] + */ +class GetAllWalletsCryptoCurrencyStatusesUseCase( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, +) { + + /** + * Get crypto currency statuses by [currencyRawId] for all wallets + * + * @param currencyRawId currency raw ID + */ + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke( + currencyRawId: String, + ): Flow>>> { + return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) + .flatMapLatest { userWalletsWithCurrencies: Map> -> + val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) -> + val operations = CurrenciesStatusesOperations( + userWalletId = userWallet.walletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + + val currencyStatusFlows = cryptoCurrencies.map { cryptoCurrency -> + operations.getCurrencyStatusFlow(cryptoCurrency) + .map { it.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } + } + + combine(currencyStatusFlows) { statuses -> userWallet to statuses.toList() } + .onEmpty { emit(userWallet to emptyList()) } + } + + combine(walletStatusFlows) { it.toMap() } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 74e207b2e7..1a9304db56 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -207,7 +207,7 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } - private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { + fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val quoteFlow = getQuotes(currenciesIds) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 1bd10511b8..dcf638ef97 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -226,9 +227,7 @@ interface CurrenciesRepository { */ fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token - /** - * Creates token [cryptoCurrency] based on [contractAddress] and [networkId] it`s will be added - */ + /** Creates token [CryptoCurrency.Token] based on [contractAddress] and [networkId] for specified [userWalletId] */ suspend fun createTokenCurrency( userWalletId: UserWalletId, contractAddress: String, @@ -236,4 +235,7 @@ interface CurrenciesRepository { ): CryptoCurrency.Token suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean + + /** Get crypto currencies by [currencyRawId] from all user wallets */ + fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 17f718c351..317a1a3dbe 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -152,4 +153,8 @@ internal class MockCurrenciesRepository( override suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean { return false } + + override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { + return emptyFlow() + } } \ No newline at end of file From 27347a13e8aecb93aa1a5f1452e6999197ddfd65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 12:59:18 +0300 Subject: [PATCH 049/197] Updated on 2026-08-14 --- .../java/com/tangem/datasource/api/stakekit/StakeKitApi.kt | 1 + .../java/com/tangem/data/staking/DefaultStakingRepository.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 1c9b1ab3e4..b1f2170fea 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -17,6 +17,7 @@ interface StakeKitApi { @GET("yields/enabled") suspend fun getMultipleYields( + @Query("preferredValidatorsOnly") preferredValidatorsOnly: Boolean? = null, @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null, @Query("type") type: YieldType? = null, @Query("revenueOption") revenueOption: RevenueOption? = null, diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 343c8f3116..24039a6bb3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -125,7 +125,9 @@ internal class DefaultStakingRepository( key = YIELDS_STORE_KEY, skipCache = refresh, block = { - val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + val stakingTokensWithYields = stakeKitApi.getMultipleYields(preferredValidatorsOnly = true) + .getOrThrow() + stakingYieldsStore.store(stakingTokensWithYields.data) }, ) From 6d3eb6de79eec302a33b833632c947cf7a1b43ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 10:06:13 +0400 Subject: [PATCH 050/197] Updated on 2026-08-14 --- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + features/markets/impl/build.gradle.kts | 12 +- .../impl/model/MarketsTokenDetailsModel.kt | 12 +- .../impl/DefaultMarketsPortfolioComponent.kt | 10 +- .../impl/model/MarketsPortfolioModel.kt | 70 +++++++++++- .../impl/model/MyPortfolioUMMFactory.kt | 80 ++++++++++++++ .../impl/model/PortfolioTokenUMConverter.kt | 44 ++++++++ .../impl/model/TokensPortfolioUMConverter.kt | 58 ++++++++++ .../markets/portfolio/impl/ui/MyPortfolio.kt | 103 +++++++++--------- libs/crypto/build.gradle.kts | 3 + .../com/tangem/lib/crypto/BlockchainUtils.kt | 6 + 11 files changed, 332 insertions(+), 67 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 9b00a821e5..9617ee7825 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -74,6 +74,7 @@ data class TangemDimens internal constructor( val size56: Dp = 56.dp, val size60: Dp = 60.dp, val size62: Dp = 62.dp, + val size63: Dp = 63.dp, val size64: Dp = 64.dp, val size68: Dp = 68.dp, val size70: Dp = 70.dp, diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 320f41fbc3..410e6dbd11 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -17,11 +17,16 @@ dependencies { implementation(projects.core.navigation) /* Domain */ - implementation(projects.domain.markets) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) - implementation(projects.domain.wallets.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.markets) + implementation(projects.domain.staking.models) + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) /* Compose */ implementation(deps.compose.coil) @@ -52,4 +57,7 @@ dependencies { implementation(projects.common.ui) implementation(projects.common.uiCharts) implementation(projects.common.routing) + + /* Libs */ + implementation(projects.libs.crypto) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index d2f3aa34f5..41a616a52f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -2,7 +2,9 @@ package com.tangem.features.markets.details.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse -import com.tangem.common.ui.charts.state.* +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartDataProducer +import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -22,13 +24,11 @@ import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice -import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween -import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -296,7 +296,9 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } - val networks = result.networks + val networks = result.networks?.filter { + BlockchainUtils.isSupportedNetworkId(it.networkId) + } networksState.value = if (networks.isNullOrEmpty()) { TokenNetworksState.NoNetworksAvailable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt index be951a14a5..73841ec05d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -2,14 +2,15 @@ package com.tangem.features.markets.portfolio.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -27,10 +28,9 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - MyPortfolio( - modifier = modifier, - state = MyPortfolioUM.Loading, - ) + val state by model.state.collectAsStateWithLifecycle() + + MyPortfolio(modifier = modifier, state = state) } @AssistedFactory diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index ba1f20d5c9..83fd674f85 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -1,30 +1,92 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import timber.log.Timber import javax.inject.Inject @Stable @ComponentScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, + getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - @Suppress("UnusedPrivateMember") + val state: StateFlow get() = _state + private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) + private val params = paramsContainer.require() - @Suppress("UnusedPrivateMember") + private val availableNetworks = MutableStateFlow?>(value = null) + + private val factory = MyPortfolioUMMFactory(::onAddClick, ::onTokenItemClick) + + init { + combine( + flow = getAllWalletsCryptoCurrencyStatusesUseCase(params.tokenId), + flow2 = availableNetworks, + flow3 = getSelectedAppCurrencyUseCase(), + flow4 = getBalanceHidingSettingsUseCase(), + ) { walletsWithCurrencyStatuses, networks, maybeAppCurrency, isBalanceHidden -> + val appCurrency = maybeAppCurrency.getOrElse { e -> + Timber.e("Failed to load app currency: $e") + AppCurrency.Default + } + + factory.create( + walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, + availableNetworks = networks, + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden.isBalanceHidden, + ) + } + .onEach { _state.value = it } + .launchIn(modelScope) + } + fun setTokenNetworks(networks: List) { - // TODO [REDACTED_TASK_KEY] + availableNetworks.value = networks } fun setNoNetworksAvailable() { - // TODO [REDACTED_TASK_KEY] + availableNetworks.value = emptyList() + } + + private fun onAddClick() { + // TODO [REDACTED_JIRA] + } + + private fun onTokenItemClick(token: CryptoCurrencyStatus) { + _state.update { + (_state.value as? MyPortfolioUM.Tokens)?.let { state -> + state.copy( + tokens = state.tokens.map { tokenUM -> + if (tokenUM.tokenItemState.id == token.currency.id.value) { + tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown) + } else { + tokenUM + } + } + .toImmutableList(), + ) + } ?: it + } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt new file mode 100644 index 0000000000..bd9042cac0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt @@ -0,0 +1,80 @@ +package com.tangem.features.markets.portfolio.impl.model + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM + +/** + * Factory for creating [MyPortfolioUM] + * + * @property onAddClick callback when user wants to add new token + * @property onTokenItemClick callback when user wants to see actions with token + * +[REDACTED_AUTHOR] + */ +internal class MyPortfolioUMMFactory( + private val onAddClick: () -> Unit, + private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, +) { + + /** + * Create [MyPortfolioUM] from data set + * + * @param walletsWithCurrencyStatuses wallets with currency statuses + * @param availableNetworks available networks + * @param appCurrency app currency + * @param isBalanceHidden flag that indicates if balance should be hidden + */ + fun create( + walletsWithCurrencyStatuses: Map>>, + availableNetworks: List?, + appCurrency: AppCurrency, + isBalanceHidden: Boolean, + ): MyPortfolioUM { + if (availableNetworks == null) return MyPortfolioUM.Loading + + if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable + + val availableWalletsWithStatuses = walletsWithCurrencyStatuses + .filterAvailable(networks = availableNetworks) + .flatMapStatuses() + + if (availableWalletsWithStatuses.isEmpty()) { + return MyPortfolioUM.AddFirstToken(onAddClick = onAddClick) + } + + return TokensPortfolioUMConverter( + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden, + availableNetworks = availableNetworks, + onAddClick = onAddClick, + onTokenItemClick = onTokenItemClick, + ) + .convert(availableWalletsWithStatuses) + } + + private fun Map>>.filterAvailable( + networks: List, + ): Map> { + val networkIds = networks.map { it.networkId } + + return mapValues { entry -> + entry.value.mapNotNull { maybeStatus -> + maybeStatus.getOrNull() + ?.takeIf { networkIds.contains(it.currency.network.backendId) } + } + } + } + + private fun Map>.flatMapStatuses(): Map { + return this + .flatMap { entry -> + entry.value.map { entry.key to it } + } + .toMap() + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt new file mode 100644 index 0000000000..d299172177 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.utils.converter.Converter + +/** + * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] + * +[REDACTED_AUTHOR] + */ +internal class PortfolioTokenUMConverter( + private val appCurrency: AppCurrency, + private val isBalanceHidden: Boolean, + private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, +) : Converter, PortfolioTokenUM> { + + override fun convert(value: Map.Entry): PortfolioTokenUM { + val (userWallet, cryptoCurrencyStatus) = value + + val tokenItemStateConverter = TokenItemStateConverter( + appCurrency = appCurrency, + titleStateProvider = { TokenItemState.TitleState.Content(text = userWallet.name) }, + subtitleStateProvider = { + TokenItemState.SubtitleState.TextContent(value = cryptoCurrencyStatus.currency.name) + }, + onItemClick = onTokenItemClick, + onItemLongClick = null, + ) + + return PortfolioTokenUM( + tokenItemState = tokenItemStateConverter.convert(value = cryptoCurrencyStatus), + isBalanceHidden = isBalanceHidden, + isQuickActionsShown = false, + onQuickActionClick = { + // TODO: [REDACTED_JIRA] + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt new file mode 100644 index 0000000000..aa3b90ea8f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -0,0 +1,58 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens] + * +[REDACTED_AUTHOR] + */ +internal class TokensPortfolioUMConverter( + private val appCurrency: AppCurrency, + private val isBalanceHidden: Boolean, + private val availableNetworks: List, + private val onAddClick: () -> Unit, + private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, +) : Converter, MyPortfolioUM.Tokens> { + + override fun convert(value: Map): MyPortfolioUM.Tokens { + return MyPortfolioUM.Tokens( + tokens = PortfolioTokenUMConverter(appCurrency, isBalanceHidden, onTokenItemClick) + .convertList(value.entries) + .toImmutableList(), + buttonState = getAddButtonState( + availableWalletsWithStatuses = value, + availableNetworks = availableNetworks, + ), + onAddClick = onAddClick, + ) + } + + private fun getAddButtonState( + availableWalletsWithStatuses: Map, + availableNetworks: List, + ): AddButtonState { + val networkIds = availableNetworks.map { it.networkId } + + val allNetworksAdded = availableWalletsWithStatuses + // User can add currencies only in multi-currency wallets + .filterKeys(UserWallet::isMultiCurrency) + .entries + .groupBy( + keySelector = { it.key }, + valueTransform = { it.value.currency.network.backendId }, + ) + .mapValues { it.value.toSet() } + // Each wallets contains all available networks? + .all { it.value.containsAll(networkIds) } + + return if (allNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 88436fa864..6f5c6f01db 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SmallButtonShimmer @@ -26,51 +25,68 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState @Composable internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { InformationBlock( modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - Text( - text = stringResource(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, + contentHorizontalPadding = TangemTheme.dimens.spacing0, + title = { Title() }, action = { if (state !is MyPortfolioUM.Tokens) return@InformationBlock - when (state.buttonState) { - MyPortfolioUM.Tokens.AddButtonState.Loading -> { - SmallButtonShimmer( - modifier = Modifier.size(width = 63.dp, height = TangemTheme.dimens.size18), - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - ) - } - else -> { - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.markets_add_token), - icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), - onClick = state.onAddClick, - enabled = state.buttonState == MyPortfolioUM.Tokens.AddButtonState.Available, - ), - ) - } - } + AddButton(state = state.buttonState, onClick = state.onAddClick) }, ) { + val contentModifier = Modifier.padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ) + when (state) { is MyPortfolioUM.Tokens -> TokenList(state = state) - is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state) - MyPortfolioUM.Loading -> LoadingPlaceholder() - MyPortfolioUM.Unavailable -> UnavailableContent() + is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier) + MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier) + MyPortfolioUM.Unavailable -> UnavailableContent(modifier = contentModifier) } } - AddToPortfolioBottomSheet(state.addToPortfolioBS) + AddToPortfolioBottomSheet(config = state.addToPortfolioBS) +} + +@Composable +private fun Title() { + Text( + text = stringResource(R.string.markets_common_my_portfolio), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun AddButton(state: AddButtonState, onClick: () -> Unit) { + when (state) { + AddButtonState.Loading -> { + SmallButtonShimmer( + modifier = Modifier.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), + shape = RoundedCornerShape(TangemTheme.dimens.radius3), + ) + } + AddButtonState.Available, + AddButtonState.Unavailable, + -> { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.markets_add_token), + icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), + onClick = onClick, + enabled = state == AddButtonState.Available, + ), + ) + } + } } @Composable @@ -90,12 +106,7 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier @Composable private fun UnavailableContent(modifier: Modifier = Modifier) { Text( - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), + modifier = modifier, text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, @@ -105,12 +116,7 @@ private fun UnavailableContent(modifier: Modifier = Modifier) { @Composable private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { Column( - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), + modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { Text( @@ -118,6 +124,7 @@ private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: M style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) + PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.markets_add_to_portfolio_button), @@ -128,19 +135,13 @@ private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: M @Composable private fun LoadingPlaceholder(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), - ) { + Column(modifier = modifier) { TextShimmer( modifier = Modifier.fillMaxWidth(), style = TangemTheme.typography.body2, textSizeHeight = true, ) + TextShimmer( modifier = Modifier.fillMaxWidth(fraction = 0.7f), style = TangemTheme.typography.body2, diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 9a8ccaea31..1e443985c3 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -20,4 +20,7 @@ dependencies { /** Core */ implementation(projects.core.utils) + + /** Libs */ + implementation(projects.libs.blockchainSdk) } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 35b8386cd9..dd253860fd 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -2,6 +2,8 @@ package com.tangem.lib.crypto import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter import com.tangem.lib.crypto.models.XrpTaggedAddress @@ -55,4 +57,8 @@ object BlockchainUtils { val blockchain = Blockchain.fromId(networkId) return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet } + + fun isSupportedNetworkId(networkId: String): Boolean { + return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false + } } \ No newline at end of file From a816b14c6946776a5d4b96082d89444a9cea6706 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 14:58:46 +0400 Subject: [PATCH 051/197] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 7 +- .../converter/UserWalletItemUMConverter.kt | 102 ++++++++++++++++++ .../domain/tokens/model/TotalFiatBalance.kt | 0 .../details/utils/UserWalletMappers.kt | 102 ------------------ .../details/utils/UserWalletsFetcher.kt | 33 ++++-- 5 files changed, 130 insertions(+), 114 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt rename domain/tokens/{src/main/kotlin => models/src/main/java}/com/tangem/domain/tokens/model/TotalFiatBalance.kt (100%) delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index d7b55fb311..9380922c04 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -28,11 +28,12 @@ dependencies { implementation(projects.core.utils) /** Project - Domain */ - implementation(projects.domain.tokens.models) - implementation(projects.domain.wallets.models) - implementation(projects.domain.staking.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.legacy) + implementation(projects.domain.staking.models) + implementation(projects.domain.tokens.models) implementation(projects.domain.transaction.models) + implementation(projects.domain.wallets.models) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt new file mode 100644 index 0000000000..704315fc82 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -0,0 +1,102 @@ +package com.tangem.common.ui.userwallet.converter + +import com.tangem.common.ui.R +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.StringsSigns.DOT +import com.tangem.utils.StringsSigns.STARS +import com.tangem.utils.converter.Converter + +/** + * Converter from [UserWallet] to [UserWalletItemUM] + * + * @property onClick lambda be invoked when item is clicked + * @property appCurrency selected app currency + * @property balance wallet balance + * @property isLoading wallet loading state + * @property isBalanceHidden wallet balance is hidden + * +[REDACTED_AUTHOR] + */ +class UserWalletItemUMConverter( + private val onClick: (UserWalletId) -> Unit, + private val appCurrency: AppCurrency? = null, + private val balance: TotalFiatBalance? = null, + private val isLoading: Boolean = true, + private val isBalanceHidden: Boolean = false, +) : Converter { + + override fun convert(value: UserWallet): UserWalletItemUM { + return with(value) { + UserWalletItemUM( + id = walletId, + name = stringReference(name), + information = getInfo( + appCurrency = appCurrency, + balance = balance, + isBalanceHidden = isBalanceHidden, + isLoading = isLoading, + ), + imageUrl = artworkUrl, + isEnabled = !isLocked, + onClick = { onClick(value.walletId) }, + ) + } + } + + private fun UserWallet.getInfo( + appCurrency: AppCurrency?, + balance: TotalFiatBalance?, + isBalanceHidden: Boolean, + isLoading: Boolean, + ): TextReference { + val dividerRef = stringReference(value = " $DOT ") + + val cardCount = getCardsCount() ?: 1 + val cardCountRef = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + + return when { + isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) + isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) + isLoading -> cardCountRef + else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) + } + } + + private fun getBalanceInfo( + balance: TotalFiatBalance?, + appCurrency: AppCurrency?, + cardCountRef: TextReference, + dividerRef: TextReference, + ): TextReference { + val amount = when (balance) { + is TotalFiatBalance.Loaded -> balance.amount + is TotalFiatBalance.Failed, + is TotalFiatBalance.Loading, + null, + -> null + } + + return if (amount != null && appCurrency != null) { + val formattedAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + val amountRef = stringReference(formattedAmount) + combinedReference(cardCountRef, dividerRef, amountRef) + } else { + combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt similarity index 100% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt deleted file mode 100644 index f51b383fbd..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.details.utils - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.util.getCardsCount -import com.tangem.domain.tokens.model.TotalFiatBalance -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.details.impl.R -import com.tangem.utils.StringsSigns.STARS -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -internal fun List.toUiModels( - onClick: (UserWalletId) -> Unit, - appCurrency: AppCurrency? = null, - balances: Map = emptyMap(), - isLoading: Boolean = true, - isBalancesHidden: Boolean = false, -): ImmutableList = this.map { model -> - val balance = balances[model.walletId] - - model.toUiModel( - balance = balance, - appCurrency = appCurrency, - isLoading = isLoading, - isBalanceHidden = isBalancesHidden, - onClick = { onClick(model.walletId) }, - ) -}.toImmutableList() - -private fun UserWallet.toUiModel( - balance: TotalFiatBalance?, - appCurrency: AppCurrency?, - isLoading: Boolean, - isBalanceHidden: Boolean, - onClick: () -> Unit, -): UserWalletItemUM = UserWalletItemUM( - id = walletId, - name = stringReference(name), - information = getInfo( - appCurrency = appCurrency, - balance = balance, - isBalanceHidden = isBalanceHidden, - isLoading = isLoading, - ), - imageUrl = artworkUrl, - isEnabled = !isLocked, - onClick = onClick, -) - -private fun UserWallet.getInfo( - appCurrency: AppCurrency?, - balance: TotalFiatBalance?, - isBalanceHidden: Boolean, - isLoading: Boolean, -): TextReference { - val dividerRef = stringReference(value = " • ") - - val cardCount = getCardsCount() ?: 1 - val cardCountRef = TextReference.PluralRes( - id = R.plurals.card_label_card_count, - count = cardCount, - formatArgs = wrappedList(cardCount), - ) - - return when { - isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) - isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) - isLoading -> cardCountRef - else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) - } -} - -private fun getBalanceInfo( - balance: TotalFiatBalance?, - appCurrency: AppCurrency?, - cardCountRef: TextReference, - dividerRef: TextReference, -): TextReference { - val amount = when (balance) { - is TotalFiatBalance.Loaded -> balance.amount - is TotalFiatBalance.Failed, - is TotalFiatBalance.Loading, - null, - -> null - } - - return if (amount != null && appCurrency != null) { - val formattedAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - val amountRef = stringReference(formattedAmount) - combinedReference(cardCountRef, dividerRef, amountRef) - } else { - combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index cd5013c005..c9115930ec 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.utils import arrow.core.Either import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router @@ -25,6 +26,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -41,7 +43,10 @@ internal class UserWalletsFetcher @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> - emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) + val uiModels = UserWalletItemUMConverter(onClick = ::navigateToWalletSettings).convertList(wallets) + .toImmutableList() + + emit(uiModels) combine( getSelectedAppCurrencyUseCase().distinctUntilChanged(), @@ -75,20 +80,30 @@ internal class UserWalletsFetcher @Inject constructor( ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, - block = { maybeBalances.bindOrNull().orEmpty() }, + block = { + maybeBalances.bindOrNull().orEmpty() + .filterKeys { userWalletId -> wallets.any { it.walletId == userWalletId } } + .mapKeys { entry -> wallets.first { it.walletId == entry.key } } + }, ) + val appCurrency = withError( transform = { Error.UnableToGetAppCurrency }, block = { maybeAppCurrency.toLce().bind() }, ) - wallets.toUiModels( - appCurrency = appCurrency, - balances = balances, - onClick = ::navigateToWalletSettings, - isBalancesHidden = balanceHidingSettings.isBalanceHidden, - isLoading = maybeBalances.isLoading(), - ) + balances + .map { (userWallet, balance) -> + UserWalletItemUMConverter( + onClick = ::navigateToWalletSettings, + appCurrency = appCurrency, + balance = balance, + isBalanceHidden = balanceHidingSettings.isBalanceHidden, + isLoading = maybeBalances.isLoading(), + ) + .convert(userWallet) + } + .toImmutableList() } private fun navigateToWalletSettings(userWalletId: UserWalletId) { From f2bf27ee5faeee5f3fb407109148f1e7301b0eab Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 18:32:19 +0300 Subject: [PATCH 052/197] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index f31136c8dd..157e1ee276 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -363,8 +363,8 @@ internal class StateBuilder( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( - title = stringReference("//TODO"), - description = stringReference("//TODO"), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { @@ -993,8 +993,8 @@ internal class StateBuilder( warnings.add( 0, SwapWarning.TransactionInProgressWarning( - title = stringReference("//TODO"), - description = stringReference("//TODO"), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) return uiState.copy( From 3e7941113858c52af4497191fb63f814cd6a2f17 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Aug 2024 17:36:47 +0300 Subject: [PATCH 053/197] Updated on 2026-08-14 --- .../firebase/FirebaseAppInstanceIdProvider.kt | 24 +++++++++++++++++++ .../tap/di/analytics/AnalyticsModule.kt | 6 +++++ .../core/analytics/AppInstanceIdProvider.kt | 8 +++++++ .../analytics/DummyAppInstanceIdProvider.kt | 12 ++++++++++ features/details/impl/build.gradle.kts | 1 + .../preview/PreviewDetailsComponent.kt | 2 ++ .../features/details/utils/ItemsBuilder.kt | 10 +++++++- 7 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt new file mode 100644 index 0000000000..8d33e1522e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.common.analytics.handlers.firebase + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import com.tangem.core.analytics.AppInstanceIdProvider +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import kotlin.coroutines.resume + +internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider { + + override suspend fun getAppInstanceId(): String? = suspendCancellableCoroutine { continuation -> + Firebase.analytics.appInstanceId + .addOnSuccessListener { continuation.resume(it) } + .addOnFailureListener { + Timber.w("Fail to get appInstanceId") + continuation.resume(null) + } + } + + override fun getAppInstanceIdSync(): String? { + return Firebase.analytics.appInstanceId.result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt index c687d63c10..60fea2b718 100644 --- a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt @@ -1,9 +1,11 @@ package com.tangem.tap.di.analytics +import com.tangem.core.analytics.AppInstanceIdProvider import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase +import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,4 +25,8 @@ internal object AnalyticsModule { @Provides @Singleton fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy() + + @Provides + @Singleton + fun provideAppInstanceIdProvider(): AppInstanceIdProvider = FirebaseAppInstanceIdProvider() } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt b/core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt new file mode 100644 index 0000000000..0d1e973283 --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/AppInstanceIdProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.core.analytics + +interface AppInstanceIdProvider { + + suspend fun getAppInstanceId(): String? + + fun getAppInstanceIdSync(): String? +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt b/core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt new file mode 100644 index 0000000000..94f92f0b3c --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/DummyAppInstanceIdProvider.kt @@ -0,0 +1,12 @@ +package com.tangem.core.analytics + +class DummyAppInstanceIdProvider : AppInstanceIdProvider { + + override suspend fun getAppInstanceId(): String? { + return null + } + + override fun getAppInstanceIdSync(): String? { + return null + } +} \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index b8d9e6465d..9a16bc4892 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) implementation(projects.core.navigation) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common.routing) implementation(projects.common.ui) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 209cad1288..1a15b85fc5 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.analytics.DummyAppInstanceIdProvider import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.features.details.component.DetailsComponent @@ -18,6 +19,7 @@ internal class PreviewDetailsComponent : DetailsComponent { ItemsBuilder( router = DummyRouter(), urlOpener = DummyUrlOpener(), + appInstanceIdProvider = DummyAppInstanceIdProvider(), ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index f14c204327..2f1093804f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -1,6 +1,7 @@ package com.tangem.features.details.utils import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.AppInstanceIdProvider import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.url.UrlOpener @@ -19,6 +20,7 @@ import javax.inject.Inject internal class ItemsBuilder @Inject constructor( private val router: Router, private val urlOpener: UrlOpener, + private val appInstanceIdProvider: AppInstanceIdProvider, ) { fun buildAll(isWalletConnectAvailable: Boolean, onSupportClick: () -> Unit): ImmutableList = @@ -50,7 +52,7 @@ internal class ItemsBuilder @Inject constructor( block = BlockUM( text = resourceReference(R.string.details_buy_wallet), iconRes = R.drawable.ic_tangem_24, - onClick = { urlOpener.openUrl(BUY_TANGEM_URL) }, + onClick = { urlOpener.openUrl(buildBuyLink()) }, ), ), ), @@ -103,6 +105,12 @@ internal class ItemsBuilder @Inject constructor( ), ) + private fun buildBuyLink(): String { + return appInstanceIdProvider.getAppInstanceIdSync()?.let { + "$BUY_TANGEM_URL&app_instance_id=$it" + } ?: BUY_TANGEM_URL + } + private companion object { const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" } From 297a006e3d16a75768bbf2fc4a440d70e2a20d73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 15:25:08 +0400 Subject: [PATCH 054/197] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 29 ++++++- .../DefaultCustomTokensRepository.kt | 47 +++++++++++ .../managetokens/di/ManageTokensDataModule.kt | 8 ++ .../CheckIsCurrencyNotAddedUseCase.kt | 20 +++++ .../managetokens/CreateCurrencyUseCase.kt | 24 ++++++ .../domain/managetokens/FindTokenUseCase.kt | 54 +++++++++++++ .../managetokens/ValidateTokenFormUseCase.kt | 80 +++++++++++++++++++ .../managetokens/model/AddCustomTokenForm.kt | 25 ++++++ .../CustomTokenFormValidationException.kt | 24 ++++++ .../model/exceptoin/FindTokenException.kt | 8 ++ .../repository/CustomTokensRepository.kt | 33 ++++++++ 11 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index dd78da6dfd..0dd7b11804 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain -import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.* +import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import dagger.Module import dagger.Provides @@ -17,4 +18,30 @@ internal object ManageTokensDomainModule { fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase { return GetManagedTokensUseCase(manageTokensRepository) } + + @Provides + @Singleton + fun provideValidateTokenFormatUseCase(customTokensRepository: CustomTokensRepository): ValidateTokenFormUseCase { + return ValidateTokenFormUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideCreateCurrencyUseCase(customTokensRepository: CustomTokensRepository): CreateCurrencyUseCase { + return CreateCurrencyUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideFindTokenUseCase(customTokensRepository: CustomTokensRepository): FindTokenUseCase { + return FindTokenUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideCheckIsCurrencyNotAddedUseCase( + customTokensRepository: CustomTokensRepository, + ): CheckIsCurrencyNotAddedUseCase { + return CheckIsCurrencyNotAddedUseCase(customTokensRepository) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt new file mode 100644 index 0000000000..50f20df32c --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -0,0 +1,47 @@ +package com.tangem.data.managetokens + +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +internal class DefaultCustomTokensRepository : CustomTokensRepository { + + override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun isCurrencyNotAdded( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + contractAddress: String?, + ): Boolean { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun findToken( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Token? { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun createCoin( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin { + TODO("Should be implemented in [REDACTED_JIRA]") + } + + override suspend fun createCustomToken( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All, + ): CryptoCurrency.Token { + TODO("Should be implemented in [REDACTED_JIRA]") + } +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 5cb10e4936..f61ff6a0b3 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -1,11 +1,13 @@ package com.tangem.data.managetokens.di +import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -37,4 +39,10 @@ internal object ManageTokensDataModule { dispatchers, ) } + + @Provides + @Singleton + fun provideCustomTokensRepository(): CustomTokensRepository { + return DefaultCustomTokensRepository() + } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt new file mode 100644 index 0000000000..04aa0761eb --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class CheckIsCurrencyNotAddedUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + contractAddress: String?, + ): Either = Either.catch { + repository.isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress) + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt new file mode 100644 index 0000000000..dc935f08af --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network + +class CreateCurrencyUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All?, + ): Either = Either.catch { + if (formValues == null) { + repository.createCoin(networkId, derivationPath) + } else { + repository.createCustomToken(networkId, derivationPath, formValues) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt new file mode 100644 index 0000000000..3542154d4e --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/FindTokenUseCase.kt @@ -0,0 +1,54 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.tangem.domain.managetokens.model.exceptoin.FindTokenException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class FindTokenUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): Either = either { + val currency = findCurrency( + userWalletId = userWalletId, + contractAddress = contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ) + + ensureNotNull(currency) { + FindTokenException.NotFound + } + } + + private suspend fun Raise.findCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Token? = catch( + block = { + repository.findToken( + userWalletId = userWalletId, + contractAddress = contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ) + }, + catch = { + raise(FindTokenException.DataError(it)) + }, + ) +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt new file mode 100644 index 0000000000..ffbfa795bf --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt @@ -0,0 +1,80 @@ +package com.tangem.domain.managetokens + +import arrow.core.EitherNel +import arrow.core.nonEmptyListOf +import arrow.core.raise.* +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network + +class ValidateTokenFormUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke( + networkId: Network.ID, + formValues: AddCustomTokenForm.Raw, + ): EitherNel = either { + if (formValues.name.isBlank() && formValues.symbol.isBlank() && formValues.decimals.isBlank()) { + val contractAddress = withError({ nonEmptyListOf(it) }) { + ensureIsContractAddressValid(formValues.contractAddress, networkId) + } + + AddCustomTokenForm.Validated.ContractAddress(contractAddress) + } else { + zipOrAccumulate( + { ensureIsContractAddressValid(formValues.contractAddress, networkId) }, + { ensureIsDecimalsValid(formValues.decimals) }, + { ensure(formValues.name.isNotBlank()) { CustomTokenFormValidationException.EmptyName } }, + { ensure(formValues.symbol.isNotBlank()) { CustomTokenFormValidationException.EmptySymbol } }, + ) { contractAddress, decimals, _, _ -> + AddCustomTokenForm.Validated.All( + contractAddress = contractAddress, + symbol = formValues.symbol, + name = formValues.name, + decimals = decimals, + ) + } + } + } + + private suspend fun Raise.ensureIsContractAddressValid( + contractAddress: String, + networkId: Network.ID, + ): String { + ensure(contractAddress.isNotBlank()) { + CustomTokenFormValidationException.ContractAddress.Empty + } + + val isValid = catch({ repository.validateContractAddress(contractAddress, networkId) }) { + raise(CustomTokenFormValidationException.DataError(it)) + } + + ensure(isValid) { + CustomTokenFormValidationException.ContractAddress.Invalid + } + + return contractAddress + } + + private fun Raise.ensureIsDecimalsValid(decimals: String): Int { + ensure(condition = decimals.isNotBlank()) { + CustomTokenFormValidationException.Decimals.Empty + } + + val decimalsInt = ensureNotNull(decimals.toIntOrNull()) { + CustomTokenFormValidationException.Decimals.Invalid + } + ensure(condition = decimalsInt in MIN_DECIMALS..MAX_DECIMALS) { + CustomTokenFormValidationException.Decimals.Invalid + } + + return decimalsInt + } + + companion object { + const val MIN_DECIMALS = 1 + const val MAX_DECIMALS = 30 + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt new file mode 100644 index 0000000000..b70cd54ab9 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.managetokens.model + +sealed class AddCustomTokenForm { + + data class Raw( + val contractAddress: String, + val name: String, + val symbol: String, + val decimals: String, + ) : AddCustomTokenForm() + + sealed class Validated { + + data class ContractAddress( + val contractAddress: String, + ) : Validated() + + data class All( + val contractAddress: String, + val name: String, + val symbol: String, + val decimals: Int, + ) : Validated() + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt new file mode 100644 index 0000000000..a197980215 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/CustomTokenFormValidationException.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class CustomTokenFormValidationException { + + sealed class ContractAddress : CustomTokenFormValidationException() { + + data object Empty : ContractAddress() + + data object Invalid : ContractAddress() + } + + sealed class Decimals : CustomTokenFormValidationException() { + + data object Empty : Decimals() + + data object Invalid : Decimals() + } + + data object EmptyName : CustomTokenFormValidationException() + + data object EmptySymbol : CustomTokenFormValidationException() + + data class DataError(val cause: Throwable) : CustomTokenFormValidationException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt new file mode 100644 index 0000000000..cc8b180305 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/FindTokenException.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class FindTokenException { + + data object NotFound : FindTokenException() + + data class DataError(val cause: Throwable) : FindTokenException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt new file mode 100644 index 0000000000..b578627da0 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.managetokens.repository + +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +interface CustomTokensRepository { + + suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean + + suspend fun isCurrencyNotAdded( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + contractAddress: String?, + ): Boolean + + suspend fun findToken( + userWalletId: UserWalletId, + contractAddress: String, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Token? + + suspend fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin + + suspend fun createCustomToken( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All, + ): CryptoCurrency.Token +} \ No newline at end of file From ce9335e3bda3dba1ceab417b3a88db8d9da54d73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Aug 2024 19:12:07 +0400 Subject: [PATCH 055/197] Updated on 2026-08-14 --- .../common/currency/CryptoCurrencyFactory.kt | 47 ++++++-- .../data/common/currency/NetworkOperations.kt | 15 +++ .../data/common/currency/TokensOperations.kt | 2 +- .../DefaultCustomTokensRepository.kt | 103 ++++++++++++++++-- .../managetokens/di/ManageTokensDataModule.kt | 14 ++- 5 files changed, 160 insertions(+), 21 deletions(-) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index 9af9e67943..38d8376ff2 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -12,6 +12,29 @@ import com.tangem.blockchain.common.Token as SdkToken // FIXME: Make internal class CryptoCurrencyFactory { + @Suppress("LongParameterList") // Yep, it's long + fun createToken( + network: Network, + rawId: String?, + name: String, + symbol: String, + decimals: Int, + contractAddress: String, + ): CryptoCurrency.Token { + val id = getTokenId(network, rawId, contractAddress) + + return CryptoCurrency.Token( + id = id, + network = network, + name = name, + symbol = symbol, + decimals = decimals, + iconUrl = rawId?.let(::getTokenIconUrlFromDefaultHost), + isCustom = isCustomToken(id, network), + contractAddress = contractAddress, + ) + } + fun createToken( sdkToken: SdkToken, blockchain: Blockchain, @@ -49,15 +72,7 @@ class CryptoCurrencyFactory { } val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null - return CryptoCurrency.Coin( - id = getCoinId(network, blockchain.toCoinId()), - network = network, - name = blockchain.fullName, - symbol = blockchain.currency, - iconUrl = getCoinIconUrl(blockchain), - decimals = blockchain.decimals(), - isCustom = isCustomCoin(network), - ) + return createCoin(network) } fun createCoin( @@ -69,6 +84,20 @@ class CryptoCurrencyFactory { return createCoin(blockchain, extraDerivationPath, derivationStyleProvider) } + fun createCoin(network: Network): CryptoCurrency.Coin { + val blockchain = Blockchain.fromId(network.id.value) + + return CryptoCurrency.Coin( + id = getCoinId(network, blockchain.toCoinId()), + network = network, + name = blockchain.fullName, + symbol = blockchain.currency, + iconUrl = getCoinIconUrl(blockchain), + decimals = blockchain.decimals(), + isCustom = isCustomCoin(network), + ) + } + fun createToken( token: Token, networkId: String, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index 8f2e5b60ac..6febfc5fb7 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -11,6 +11,21 @@ fun getBlockchain(networkId: Network.ID): Blockchain { return Blockchain.fromId(networkId.value) } +fun getNetwork(networkId: Network.ID, derivationPath: Network.DerivationPath): Network { + val blockchain = getBlockchain(networkId) + + return Network( + id = networkId, + backendId = blockchain.toNetworkId(), + name = blockchain.getNetworkName(), + isTestnet = blockchain.isTestnet(), + derivationPath = derivationPath, + currencySymbol = blockchain.currency, + standardType = getNetworkStandardType(blockchain), + hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + ) +} + fun getNetwork( blockchain: Blockchain, extraDerivationPath: String?, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt index 72018fc4ed..2c2c8d95ad 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt @@ -85,7 +85,7 @@ private fun getCurrencyIdBody(network: Network): CurrencyIdBody { } } -private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { +fun getTokenIconUrlFromDefaultHost(tokenId: String): String { return buildString { append(DEFAULT_TOKENS_ICONS_HOST) append('/') diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 50f20df32c..8488ff5b54 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -1,16 +1,46 @@ package com.tangem.data.managetokens +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetwork +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.supportedBlockchains +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext -internal class DefaultCustomTokensRepository : CustomTokensRepository { +internal class DefaultCustomTokensRepository( + private val tangemTechApi: TangemTechApi, + private val userWalletsStore: UserWalletsStore, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : CustomTokensRepository { - override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean { - TODO("Should be implemented in [REDACTED_JIRA]") - } + private val cryptoCurrencyFactory = CryptoCurrencyFactory() + + override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean = + withContext(dispatchers.io) { + when (val blockchain = Blockchain.fromId(networkId.value)) { + Blockchain.Unknown, + Blockchain.Binance, + Blockchain.BinanceTestnet, + -> true + Blockchain.Cardano -> blockchain.validateContractAddress(contractAddress.lowercase()) + else -> blockchain.validateAddress(contractAddress.lowercase()) + } + } override suspend fun isCurrencyNotAdded( userWalletId: UserWalletId, @@ -18,7 +48,17 @@ internal class DefaultCustomTokensRepository : CustomTokensRepository { derivationPath: Network.DerivationPath, contractAddress: String?, ): Boolean { - TODO("Should be implemented in [REDACTED_JIRA]") + return withContext(dispatchers.io) { + val storedCurrencies: UserTokensResponse = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?: error("User tokens not found") + + storedCurrencies.tokens.none { token -> + Blockchain.fromId(networkId.value).toNetworkId() == token.networkId && + derivationPath.value == token.derivationPath && + contractAddress.equals(token.contractAddress, ignoreCase = true) + } + } } override suspend fun findToken( @@ -26,15 +66,51 @@ internal class DefaultCustomTokensRepository : CustomTokensRepository { contractAddress: String, networkId: Network.ID, derivationPath: Network.DerivationPath, - ): CryptoCurrency.Token? { - TODO("Should be implemented in [REDACTED_JIRA]") + ): CryptoCurrency.Token? = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + ?: error("User wallet not found") + val network = getNetwork(networkId, derivationPath) + + val supportedTokenNetworkIds = userWallet.scanResponse.card + .supportedBlockchains(userWallet.scanResponse.cardTypesResolver) + .filter(Blockchain::canHandleTokens) + .map(Blockchain::toNetworkId) + + val response = tangemTechApi.getCoins( + contractAddress = contractAddress, + networkIds = network.backendId, + active = true, + ).getOrThrow() + + response.coins.firstNotNullOfOrNull { coin -> + val coinNetwork = coin.networks.firstOrNull { network -> + (network.contractAddress != null || network.decimalCount != null) && + network.contractAddress.equals(contractAddress, ignoreCase = true) && + network.networkId in supportedTokenNetworkIds + } + + if (coinNetwork != null) { + cryptoCurrencyFactory.createToken( + network = network, + rawId = coin.id, + name = coin.name, + symbol = coin.symbol, + decimals = coinNetwork.decimalCount!!.toInt(), + contractAddress = contractAddress, + ) + } else { + null + } + } } override suspend fun createCoin( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { - TODO("Should be implemented in [REDACTED_JIRA]") + val network = getNetwork(networkId, derivationPath) + + return cryptoCurrencyFactory.createCoin(network) } override suspend fun createCustomToken( @@ -42,6 +118,15 @@ internal class DefaultCustomTokensRepository : CustomTokensRepository { derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { - TODO("Should be implemented in [REDACTED_JIRA]") + val network = getNetwork(networkId, derivationPath) + + return cryptoCurrencyFactory.createToken( + network = network, + rawId = null, + name = formValues.name, + symbol = formValues.symbol, + decimals = formValues.decimals, + contractAddress = formValues.contractAddress, + ) } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index f61ff6a0b3..64d77526c1 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -42,7 +42,17 @@ internal object ManageTokensDataModule { @Provides @Singleton - fun provideCustomTokensRepository(): CustomTokensRepository { - return DefaultCustomTokensRepository() + fun provideCustomTokensRepository( + tangemTechApi: TangemTechApi, + userWalletsStore: UserWalletsStore, + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): CustomTokensRepository { + return DefaultCustomTokensRepository( + tangemTechApi, + userWalletsStore, + appPreferencesStore, + dispatchers, + ) } } \ No newline at end of file From e59c7e5c5ab22166a5b2819a762c2bc76d22a42f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 13:14:28 +0400 Subject: [PATCH 056/197] Updated on 2026-08-14 --- .../DefaultCustomTokensRepository.kt | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 8488ff5b54..eabde921a1 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -1,8 +1,11 @@ package com.tangem.data.managetokens +import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter +import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getBlockchain import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -70,6 +73,11 @@ internal class DefaultCustomTokensRepository( val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") val network = getNetwork(networkId, derivationPath) + val tokenAddress = convertTokenAddress( + networkId, + contractAddress, + symbol = null, + ) val supportedTokenNetworkIds = userWallet.scanResponse.card .supportedBlockchains(userWallet.scanResponse.cardTypesResolver) @@ -85,7 +93,7 @@ internal class DefaultCustomTokensRepository( response.coins.firstNotNullOfOrNull { coin -> val coinNetwork = coin.networks.firstOrNull { network -> (network.contractAddress != null || network.decimalCount != null) && - network.contractAddress.equals(contractAddress, ignoreCase = true) && + network.contractAddress.equals(tokenAddress, ignoreCase = true) && network.networkId in supportedTokenNetworkIds } @@ -96,7 +104,7 @@ internal class DefaultCustomTokensRepository( name = coin.name, symbol = coin.symbol, decimals = coinNetwork.decimalCount!!.toInt(), - contractAddress = contractAddress, + contractAddress = tokenAddress, ) } else { null @@ -119,6 +127,11 @@ internal class DefaultCustomTokensRepository( formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { val network = getNetwork(networkId, derivationPath) + val tokenAddress = convertTokenAddress( + networkId, + formValues.contractAddress, + formValues.symbol, + ) return cryptoCurrencyFactory.createToken( network = network, @@ -126,7 +139,24 @@ internal class DefaultCustomTokensRepository( name = formValues.name, symbol = formValues.symbol, decimals = formValues.decimals, - contractAddress = formValues.contractAddress, + contractAddress = tokenAddress, ) } + + private fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { + val convertedAddress = when (getBlockchain(networkId)) { + Blockchain.Hedera, + Blockchain.HederaTestnet, + -> HederaTokenAddressConverter().convertToTokenId(contractAddress) + Blockchain.Cardano -> { + // TODO: [REDACTED_JIRA] + CardanoTokenAddressConverter().convertToFingerprint(contractAddress, symbol) + } + else -> contractAddress + } + + return requireNotNull(convertedAddress) { + "Token contract address is invalid" + } + } } \ No newline at end of file From ed3b894c36367530dc5ca4bc1e3701727019d40e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 18:13:17 +0400 Subject: [PATCH 057/197] Updated on 2026-08-14 --- .../tangem/tap/domain/card/CryptoCurrenciesMocks.kt | 1 + .../data/common/currency/NetworkOperations.kt | 2 ++ .../java/com/tangem/domain/tokens/model/Network.kt | 1 + .../com/tangem/domain/tokens/mock/MockNetworks.kt | 3 +++ .../impl/DefaultAddCustomTokenComponent.kt | 3 ++- .../impl/DefaultCustomTokenFormComponent.kt | 2 +- .../preview/PreviewCustomTokenSelectorComponent.kt | 13 ++++++++----- .../entity/customtoken/AddCustomTokenConfig.kt | 8 +++++--- .../managetokens/entity/item/DerivationPathUM.kt | 8 +++----- .../managetokens/ui/AddCustomTokenBottomSheet.kt | 11 +++++++---- .../managetokens/ui/CustomTokenSelectorContent.kt | 13 ++++++++----- 11 files changed, 41 insertions(+), 24 deletions(-) diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt index 5ecd65378b..01faeadf30 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt @@ -68,6 +68,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { isTestnet = false, standardType = Network.StandardType.ERC20, hasFiatFeeRate = true, + canHandleTokens = true, ), name = "NEVER-MIND", symbol = "NEVER-MIND", diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index 6febfc5fb7..4cb4fb8d6e 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -23,6 +23,7 @@ fun getNetwork(networkId: Network.ID, derivationPath: Network.DerivationPath): N currencySymbol = blockchain.currency, standardType = getNetworkStandardType(blockchain), hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + canHandleTokens = blockchain.canHandleTokens(), ) } @@ -45,6 +46,7 @@ fun getNetwork( currencySymbol = blockchain.currency, standardType = getNetworkStandardType(blockchain), hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + canHandleTokens = blockchain.canHandleTokens(), ) } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 59d8d3a517..3543b7094b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -29,6 +29,7 @@ data class Network( val isTestnet: Boolean, val standardType: StandardType, val hasFiatFeeRate: Boolean, + val canHandleTokens: Boolean, ) { init { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 091154dda9..cd410b3487 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -22,6 +22,7 @@ internal object MockNetworks { currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, + canHandleTokens = true, ) val network2 = Network( @@ -33,6 +34,7 @@ internal object MockNetworks { currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, + canHandleTokens = true, ) val network3 = Network( @@ -44,6 +46,7 @@ internal object MockNetworks { currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, + canHandleTokens = true, ) val networkStatus1 = NetworkStatus( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index dd0faa0556..48e7a1b01f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -124,8 +124,9 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( userWalletId = config.userWalletId, network = config.selectedNetwork ?: error("Network is not selected"), derivationPath = config.selectedDerivationPath ?: SelectedDerivationPath( + id = config.selectedNetwork.id, value = config.selectedNetwork.derivationPath, - name = resourceReference(R.string.custom_token_derivation_path_default), + networkName = resourceReference(R.string.custom_token_derivation_path_default), ), formValues = config.formValues, onSelectNetworkClick = ::showNetworkSelector, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt index 982e4979e1..2ca0b7e1a1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt @@ -49,7 +49,7 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor( tokenForm = getInitialTokenForm(), derivationPath = ClickableFieldUM( label = resourceReference(R.string.custom_token_derivation_path), - value = params.derivationPath.name, + value = params.derivationPath.networkName, onClick = ::selectDerivationPath, ), saveToken = { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 1e773cc319..7d596d3f8d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -29,13 +29,15 @@ internal class PreviewCustomTokenSelectorComponent( when (params) { is Params.DerivationPathSelector -> { val d = SelectedDerivationPath( - value = "m/44'/0'/0'/0/$index", - name = stringReference(value = "Network $index"), + id = Network.ID(index.toString()), + value = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"), + networkName = stringReference(value = "Network $index"), ) DerivationPathUM( - value = "m/44'/0'/0'/0/$index", - blockchainName = d.name, + id = d.id.value, + value = d.value.value.orEmpty(), + networkName = d.networkName, isSelected = d.value == params.selectedDerivationPath?.value, onSelectedStateChange = { params.onDerivationPathSelected(d) }, ) @@ -44,7 +46,8 @@ internal class PreviewCustomTokenSelectorComponent( val n = SelectedNetwork( id = Network.ID(index.toString()), name = stringReference(value = "Network $index"), - derivationPath = "m/44'/0'/0'/0/$index", + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"), + canHandleTokens = false, ) CurrencyNetworkUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt index 417cd15585..6a43cdc40d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt @@ -28,11 +28,13 @@ internal data class AddCustomTokenConfig( internal data class SelectedNetwork( val id: Network.ID, val name: TextReference, - val derivationPath: String, + val derivationPath: Network.DerivationPath, + val canHandleTokens: Boolean, ) @Serializable internal data class SelectedDerivationPath( - val value: String, - val name: TextReference, + val id: Network.ID, + val value: Network.DerivationPath, + val networkName: TextReference, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt index 2190c50e4f..bf6387ff0d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt @@ -3,11 +3,9 @@ package com.tangem.features.managetokens.entity.item import com.tangem.core.ui.extensions.TextReference internal data class DerivationPathUM( + override val id: String, val value: String, - val blockchainName: TextReference, + val networkName: TextReference, override val isSelected: Boolean, override val onSelectedStateChange: (Boolean) -> Unit, -) : SelectableItemUM { - - override val id: String = value -} \ No newline at end of file +) : SelectableItemUM \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index b1b3cc0f0a..69ea2418d6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -103,7 +103,8 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< selectedNetwork = SelectedNetwork( id = Network.ID(value = "1"), name = stringReference("Ethereum"), - derivationPath = "m/44'/0'/0'/0/0", + derivationPath = Network.DerivationPath.None, + canHandleTokens = false, ), ), ), @@ -115,7 +116,8 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< selectedNetwork = SelectedNetwork( id = Network.ID(value = "0"), name = stringReference("Ethereum"), - derivationPath = "m/44'/0'/0'/0/0", + derivationPath = Network.DerivationPath.None, + canHandleTokens = false, ), ), ), @@ -125,8 +127,9 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR, popBack = {}, selectedDerivationPath = SelectedDerivationPath( - value = "m/44'/0'/0'/0/0", - name = stringReference("Ethereum"), + id = Network.ID(value = "0"), + value = Network.DerivationPath.None, + networkName = stringReference("Ethereum"), ), ), ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index f5545db44b..67c77ff303 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -154,7 +155,7 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) icon = CurrencyIconState.CoinIcon( url = null, fallbackResId = model.iconResId, - isGrayscale = !model.isSelected, + isGrayscale = false, showCustomBadge = false, ), showCustom = false, @@ -205,7 +206,7 @@ private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Mod contentAlignment = Alignment.CenterStart, ) { Text( - text = "${model.value} ${if (model.isSelected) "- selected" else ""}", + text = "${model.networkName.resolveReference()}: ${model.value} ${if (model.isSelected) "<" else ""}", color = TangemTheme.colors.text.primary1, ) } @@ -235,7 +236,8 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : selectedNetwork = SelectedNetwork( id = Network.ID(value = "0"), name = stringReference(""), - derivationPath = "m/44'/0'/0'/0/0", + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + canHandleTokens = false, ), onNetworkSelected = {}, ), @@ -244,8 +246,9 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = UserWalletId(stringValue = "321"), selectedDerivationPath = SelectedDerivationPath( - value = "m/44'/0'/0'/0/0", - name = stringReference(""), + id = Network.ID(value = "0"), + value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + networkName = stringReference(""), ), onDerivationPathSelected = {}, ), From 01d839b4c5d807e5ea42c1797b223e0e03f4e071 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 15:35:08 +0400 Subject: [PATCH 058/197] Updated on 2026-08-14 --- .../tap/domain/card/CryptoCurrenciesMocks.kt | 26 +++++++++++++++---- .../data/common/currency/NetworkOperations.kt | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt index 01faeadf30..a9e93345cc 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt @@ -1,8 +1,12 @@ package com.tangem.tap.domain.card import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetworkDerivationPath +import com.tangem.data.common.currency.getNetworkStandardType import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency @@ -40,11 +44,23 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { } private fun createCoin(blockchain: Blockchain): CryptoCurrency { - return factory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - derivationStyleProvider = scanResponse.derivationStyleProvider, - )!! + val network = Network( + id = Network.ID(blockchain.id), + backendId = blockchain.toNetworkId(), + name = blockchain.getNetworkName(), + isTestnet = blockchain.isTestnet(), + derivationPath = getNetworkDerivationPath( + blockchain, + extraDerivationPath = null, + scanResponse.derivationStyleProvider, + ), + currencySymbol = blockchain.currency, + standardType = getNetworkStandardType(blockchain), + hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, + canHandleTokens = false, + ) + + return factory.createCoin(network = network) } // Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index 4cb4fb8d6e..3666f5f012 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -50,7 +50,7 @@ fun getNetwork( ) } -private fun getNetworkDerivationPath( +fun getNetworkDerivationPath( blockchain: Blockchain, extraDerivationPath: String?, cardDerivationStyleProvider: DerivationStyleProvider?, From 7b7cece46f2d4f96935012757d4252b2590b7b3e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 15:46:27 +0300 Subject: [PATCH 059/197] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 4 +- .../tangem/common/ui/tokens/TokenPriceText.kt | 55 +++++ .../response/TokenMarketInfoResponse.kt | 11 +- .../core/ui/components/fields/SearchBar.kt | 2 + .../core/ui/utils/BigDecimalFormatter.kt | 9 + .../java/com/tangem/utils/StringsSigns.kt | 1 + .../tangem/data/common/utils/RequestUtils.kt | 13 +- .../converters/TokenMarketInfoConverter.kt | 1 + .../repository/DefaultQuotesRepository.kt | 4 +- .../tangem/domain/markets/TokenMarketInfo.kt | 8 +- .../markets/GetCurrencyQuotesUseCase.kt | 24 +++ .../domain/markets/GetTokenQuotesUseCase.kt | 26 --- .../tokens/repository/QuotesRepository.kt | 4 +- .../tokens/repository/MockQuotesRepository.kt | 2 +- .../token/block/TokenMarketBlockComponent.kt | 2 + .../impl/model/MarketsTokenDetailsModel.kt | 133 ++++++------ .../model/converters/InsightsConverter.kt | 138 +++++++------ .../impl/model/converters/MetricsConverter.kt | 17 +- .../converters/PricePerformanceConverter.kt | 30 +-- .../converters/TokenMarketInfoConverter.kt | 12 +- .../formatter/MarketsDateTimeFormatters.kt | 19 +- .../impl/model/state/QuotesStateUpdater.kt | 83 ++++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 9 +- .../details/impl/ui/components/Description.kt | 3 +- .../details/impl/ui/components/LinksBlock.kt | 1 + .../ui/components/PricePerformanceBlock.kt | 5 +- .../ui/components/TokenMarketDetailsBody.kt | 7 +- .../details/impl/ui/state/MetricsUM.kt | 4 +- .../entry/impl/ui/EntryBottomSheetContent.kt | 11 +- .../block/impl/model/TokenMarketBlockModel.kt | 13 +- .../token/block/impl/ui/TokenMarketBlock.kt | 11 +- .../statemanager/MarketsListUMStateManager.kt | 2 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 2 +- .../impl/ui/components/MarketsListItem.kt | 191 +----------------- .../presentation/TokenDetailsFragment.kt | 1 + 35 files changed, 436 insertions(+), 422 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt delete mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 031784429c..f32af0c735 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -41,7 +41,7 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetTokenQuotesUseCase { - return GetTokenQuotesUseCase(quotesRepository = quotesRepository) + fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase { + return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt new file mode 100644 index 0000000000..6a337dae15 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.tokens + +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.res.TangemTheme + +/** + * Text view for token price. + * + * @param price Price of the token. + * @param priceChangeType Type of the price change. + */ +@Composable +fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { + val growColor = TangemTheme.colors.text.accent + val fallColor = TangemTheme.colors.text.warning + val generalColor = TangemTheme.colors.text.primary1 + + val color = remember(generalColor) { Animatable(generalColor) } + var animationSkipped by remember { mutableStateOf(false) } + + LaunchedEffect(price) { + if (animationSkipped.not()) { + animationSkipped = true + return@LaunchedEffect + } + + if (priceChangeType != null) { + val nextColor = when (priceChangeType) { + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + PriceChangeType.NEUTRAL -> return@LaunchedEffect + } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } + } + + Text( + modifier = modifier, + text = price, + color = color.value, + maxLines = 1, + style = TangemTheme.typography.body2, + overflow = TextOverflow.Visible, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index ef485e1829..21b38fdfb0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -67,7 +67,16 @@ data class TokenMarketInfoResponse( val buyPressureChange: Change?, @Json(name = "experienced_buyer_change") val experiencedBuyerChange: Change?, - ) + @Json(name = "networks") + val sourceNetworks: List?, + ) { + data class SourceNetwork( + @Json(name = "network_id") + val id: String, + @Json(name = "network_name") + val name: String, + ) + } data class Change( @Json(name = "24h") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 3de373a0e3..9863c30ba0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController @@ -69,6 +70,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie textStyle = TangemTheme.typography.body2.copy( color = TangemTheme.colors.text.primary1, ), + cursorBrush = SolidColor(TangemTheme.colors.icon.primary1), decorationBox = @Composable { innerTextField -> DecorationBox( state = state, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 91ef672575..fc916ea18a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -336,6 +336,15 @@ object BigDecimalFormatter { ): String { if (amount == null) return EMPTY_BALANCE_SIGN + if (amount < BigDecimal.ONE) { + return formatFiatPriceUncapped( + fiatAmount = amount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) + } + val rawAmount = formatCompactAmount( amount = amount, locale = locale, diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 15d69db683..b2d8906fb0 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -9,4 +9,5 @@ object StringsSigns { const val DASH_SIGN = "—" const val LOWER_SIGN = "<" const val TILDE_SIGN = "~" + const val NON_BREAKING_SPACE = '\u00A0' } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt index e7f3fbfe45..710a982835 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt @@ -7,8 +7,10 @@ import kotlinx.coroutines.yield import timber.log.Timber import kotlin.coroutines.cancellation.CancellationException -@Suppress("UnconditionalJumpStatementInLoop") -suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): T { +@Suppress("UnconditionalJumpStatementInLoop", "MagicNumber") +suspend fun retryOnError(priority: Boolean = false, startRetryDelay: Int = 500, call: suspend () -> T): T { + var currentDelay = startRetryDelay + var priorityCounter = 5 while (true) { return try { call() @@ -19,9 +21,12 @@ suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): Timber.e(e, "Error occurred during retryOnError block") - if (priority.not()) { + if (priority && priorityCounter > 0) { + --priorityCounter + } else { yield() - delay(timeMillis = 500) + delay(timeMillis = currentDelay.toLong()) + currentDelay *= 2 } continue diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index e9314b0d7e..d77d7618fe 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -57,6 +57,7 @@ internal object TokenMarketInfoConverter : Converter): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return appPreferencesStore.getObject( key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, ) .distinctUntilChanged() .filterNotNull() .flatMapLatest { appCurrency -> - fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false) + fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh) quotesStore.get(currenciesIds).map(quotesConverter::convertSet) } .cancellable() diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt index 7fe496e5f1..6f02eb0639 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -27,7 +27,13 @@ data class TokenMarketInfo( val liquidityChange: Change?, val buyPressureChange: Change?, val experiencedBuyerChange: Change?, - ) + val sourceNetworks: List, + ) { + data class SourceNetwork( + val id: String, + val name: String, + ) + } data class Change( val day: BigDecimal?, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt new file mode 100644 index 0000000000..1ec184fbdc --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.markets + +import arrow.core.* +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.repository.QuotesRepository +import kotlinx.coroutines.flow.* + +@Suppress("UnusedPrivateMember") +class GetCurrencyQuotesUseCase( + private val quotesRepository: QuotesRepository, +) { + // TODO apply interval parameter [REDACTED_TASK_KEY] + operator fun invoke( + currencyID: CryptoCurrency.ID, + interval: PriceChangeInterval, + refresh: Boolean, + ): Flow> { + return quotesRepository.getQuotesUpdates( + currenciesIds = setOf(currencyID), + refresh = refresh, + ).map { it.firstOrNull().toOption() }.catch { emit(None) } + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt deleted file mode 100644 index 059c80ac82..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain.markets - -import arrow.core.Either -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.repository.QuotesRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -@Suppress("UnusedPrivateMember") -class GetTokenQuotesUseCase( - private val quotesRepository: QuotesRepository, -) { - operator fun invoke(tokenId: String, interval: PriceChangeInterval): Flow> { - return flowOf( - Either.catch { - Quote( - rawCurrencyId = "USD", - fiatRate = 100.toBigDecimal(), // mock - priceChange = 10.toBigDecimal(), // mock - ) - }.mapLeft {}, - ) - // TODO implement quotes fetching from repository [REDACTED_TASK_KEY] - // quotesRepository.getQuotesUpdates() - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 4ebd284421..57a8429697 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -12,12 +12,12 @@ interface QuotesRepository { /** * Retrieves updates of quotes for a set of specified cryptocurrencies, identified by their unique IDs. * - * Loads remote quotes if they have expired. + * Loads remote quotes if they have expired or if [refresh] is `true`. * * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ - fun getQuotesUpdates(currenciesIds: Set): Flow> + fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean = false): Flow> /** * Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index 3d45f98879..393432cf16 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -13,7 +13,7 @@ internal class MockQuotesRepository( private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotesUpdates(currenciesIds: Set): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return quotes.map { it.getOrElse { e -> throw e } } } diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt index dbd1b4f877..9cc202bdb4 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.token.block import androidx.compose.runtime.Stable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.serialization.Serializable @Stable @@ -10,6 +11,7 @@ interface TokenMarketBlockComponent : ComposableContentComponent { @Serializable data class Params( + val cryptoCurrencyID: CryptoCurrency.ID, val tokenId: String, val tokenName: String, val tokenSymbol: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 41a616a52f..3f01b427d9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -13,7 +13,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter @@ -24,6 +23,10 @@ import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.formatter.* +import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice +import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween +import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval +import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM @@ -76,6 +79,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( urlOpener.openUrl(it.url) }, ) + private val descriptionConverter = DescriptionConverter( onReadModeClicked = { showInfoBottomSheet(it) @@ -95,7 +99,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( BigDecimalFormatter.formatFiatPriceUncapped( fiatAmount = value, fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = "", + fiatCurrencySymbol = currentAppCurrency.value.symbol, ) }, ) @@ -115,7 +119,8 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), ) - private var lastUpdatedTimestamp: Long = DateTime.now().millis + private val currentTokenInfo = MutableStateFlow(null) + private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis) val isVisibleOnScreen = MutableStateFlow(false) val networksState = MutableStateFlow(TokenNetworksState.Loading) @@ -155,6 +160,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), ) + private val quotesStateUpdater = QuotesStateUpdater( + currentAppCurrency = Provider { currentAppCurrency.value }, + state = state, + currentQuotes = currentQuotes, + lastUpdatedTimestamp = lastUpdatedTimestamp, + currentTokenInfo = currentTokenInfo, + ) + private val loadChartJobHolder = JobHolder() init { @@ -279,41 +292,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) tokenMarketInfo.fold( - ifRight = { result -> - currentQuotes.value = result.quotes - val percent = result.quotes.getPercentByInterval(interval = state.value.selectedInterval) - state.update { - it.copy( - priceText = result.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), - priceChangePercentText = result.quotes.getFormattedPercentByInterval( - interval = it.selectedInterval, - ), - priceChangeType = percent.percentChangeType(), - body = MarketsTokenDetailsUM.Body.Content( - description = descriptionConverter.convert(result), - infoBlocks = infoConverter.convert(result), - ), - ) - } - - val networks = result.networks?.filter { - BlockchainUtils.isSupportedNetworkId(it.networkId) - } - - networksState.value = if (networks.isNullOrEmpty()) { - TokenNetworksState.NoNetworksAvailable - } else { - TokenNetworksState.NetworksAvailable(networks) - } - - chartDataProducer.runTransaction { - updateLook { - it.copy( - type = getChartTypeByPercent(percent), - ) - } - } - }, + ifRight = { result -> updateInfo(result) }, ifLeft = { state.update { if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) { @@ -333,42 +312,50 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } - private suspend fun updateQuotes(newQuotes: TokenQuotes) { - val triggerPriceChangeType = getFormattedPriceChange( - currentPrice = currentQuotes.value.currentPrice, - updatedPrice = newQuotes.currentPrice, - ) - val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { - triggeredEvent( - data = triggerPriceChangeType, - onConsume = { - state.update { it.copy(triggerPriceChange = consumedEvent()) } - }, + private fun updateInfo(newInfo: TokenMarketInfo) { + lastUpdatedTimestamp.value = DateTime.now().millis + + currentTokenInfo.value = newInfo + currentQuotes.value = newInfo.quotes + + val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval) + state.update { + it.copy( + priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( + interval = it.selectedInterval, + ), + priceChangeType = percent.percentChangeType(), + body = MarketsTokenDetailsUM.Body.Content( + description = descriptionConverter.convert(newInfo), + infoBlocks = infoConverter.convert(newInfo), + ), ) - } else { - consumedEvent() } + val networks = newInfo.networks?.filter { + BlockchainUtils.isSupportedNetworkId(it.networkId) + } + + networksState.value = if (networks.isNullOrEmpty()) { + TokenNetworksState.NoNetworksAvailable + } else { + TokenNetworksState.NetworksAvailable(networks) + } + + chartDataProducer.runTransaction { + updateLook { + it.copy( + type = getChartTypeByPercent(percent), + ) + } + } + } + + private suspend fun updateQuotes(newQuotes: TokenQuotes) { + quotesStateUpdater.updateQuotes(newQuotes) + val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) - val priceChangeType = percent.percentChangeType() - - // wait until marker is removed - state.first { it.markerSet.not() } - - currentQuotes.value = newQuotes - lastUpdatedTimestamp = DateTime.now().millis - - state.update { stateToUpdate -> - stateToUpdate.copy( - priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency.value), - priceChangePercentText = newQuotes.getFormattedPercentByInterval( - interval = stateToUpdate.selectedInterval, - ), - priceChangeType = priceChangeType, - triggerPriceChange = trigger, - dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval), - ) - } chartDataProducer.runTransaction { updateLook { @@ -489,9 +476,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( launch { while (true) { delay(timeMillis) - // Update quotes only when the container bottom sheet is in the expanded state - - // and is visible on the screen + // Update quotes only when content visible on the screen isVisibleOnScreen.first { it } loadQuotes() @@ -504,7 +489,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( interval = interval, startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( interval = interval, - currentTimestamp = lastUpdatedTimestamp, + currentTimestamp = lastUpdatedTimestamp.value, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt index 9bbcff51d9..376bf704a0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -2,7 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo @@ -14,7 +14,7 @@ import com.tangem.utils.Provider import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @Stable @@ -48,7 +48,10 @@ internal class InsightsConverter( onInfoClick( InfoBottomSheetContent( title = resourceReference(R.string.markets_token_details_insights), - body = stringReference("//TODO"), + body = resourceReference( + R.string.markets_insights_info_description_message, + wrappedList(value.sourceNetworks.joinToString { it.name }), + ), ), ) }, @@ -62,74 +65,79 @@ internal class InsightsConverter( liquidityChange: BigDecimal?, buyPressureChange: BigDecimal?, ): ImmutableList { - return persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = experiencedBuyerChange.convertChange(), - change = experiencedBuyerChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - body = resourceReference(R.string.markets_token_details_experienced_buyers_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = buyPressureChange.convertChange(isFiatValue = true), - change = buyPressureChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_buy_pressure), - body = resourceReference(R.string.markets_token_details_buy_pressure_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = holdersChange.convertChange(), - change = holdersChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_holders), - body = resourceReference(R.string.markets_token_details_holders_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = liquidityChange.convertChange(), - change = liquidityChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_liquidity), - body = resourceReference(R.string.markets_token_details_liquidity_description), - ), - ) - }, - ), - ) + return listOfNotNull( + experiencedBuyerChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = experiencedBuyerChange.convertChange(), + change = experiencedBuyerChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + body = resourceReference(R.string.markets_token_details_experienced_buyers_description), + ), + ) + }, + ) + }, + buyPressureChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = buyPressureChange.convertChange(isFiatValue = true), + change = buyPressureChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_buy_pressure), + body = resourceReference(R.string.markets_token_details_buy_pressure_description), + ), + ) + }, + ) + }, + holdersChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = holdersChange.convertChange(), + change = holdersChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_holders), + body = resourceReference(R.string.markets_token_details_holders_description), + ), + ) + }, + ) + }, + liquidityChange?.let { + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = liquidityChange.convertChange(), + change = liquidityChange.changeType(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_liquidity), + body = resourceReference(R.string.markets_token_details_liquidity_description), + ), + ) + }, + ) + }, + ).toImmutableList() } - private fun BigDecimal?.changeType(): InfoPointUM.ChangeType? { + private fun BigDecimal.changeType(): InfoPointUM.ChangeType? { return when { - this == null -> null this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN else -> null } } - private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String { - if (this == null) return StringsSigns.DASH_SIGN - + private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { val value = if (isFiatValue) { val currency = appCurrency() BigDecimalFormatter.formatCompactFiatAmount( @@ -141,11 +149,9 @@ internal class InsightsConverter( BigDecimalFormatter.formatCompactAmount(amount = this.abs()) } - val spacing = if (isFiatValue) " " else "" - return when { - this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value - this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value + this > BigDecimal.ZERO -> StringsSigns.PLUS + value + this < BigDecimal.ZERO -> StringsSigns.MINUS + value this == BigDecimal.ZERO -> value else -> StringsSigns.DASH_SIGN } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt index 4bf9f1e111..c6c57682f7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -14,9 +14,6 @@ import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -import java.math.RoundingMode -import java.text.NumberFormat -import java.util.Locale @Stable internal class MetricsConverter( @@ -116,19 +113,15 @@ internal class MetricsConverter( private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { return if (crypto) { - val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply { - maximumFractionDigits = 0 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - formatter.format(this) + BigDecimalFormatter.formatCompactAmount( + amount = this ?: BigDecimal.ZERO, + ) } else { val currency = appCurrency() - BigDecimalFormatter.formatFiatAmount( - fiatAmount = this, + BigDecimalFormatter.formatCompactFiatAmount( + amount = this, fiatCurrencyCode = currency.code, fiatCurrencySymbol = currency.symbol, - decimals = 0, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt index 0ccf01cde7..38ee8682bf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt @@ -7,24 +7,23 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.utils.Provider import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter import java.math.BigDecimal import java.math.RoundingMode @Stable internal class PricePerformanceConverter( private val appCurrency: Provider, -) : Converter { +) { - override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM { + fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { return PricePerformanceUM( - h24 = value.day.convert(), - month = value.month.convert(), - all = value.allTime.convert(), + h24 = value.day.convert(currentPrice), + month = value.month.convert(currentPrice), + all = value.allTime.convert(currentPrice), ) } - private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value { + private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value { if (this == null) { return PricePerformanceUM.Value( low = StringsSigns.DASH_SIGN, @@ -36,7 +35,7 @@ internal class PricePerformanceConverter( return PricePerformanceUM.Value( low = low.convert(), high = high.convert(), - indicatorFraction = calculateFraction(), + indicatorFraction = calculateFraction(currentPrice), ) } @@ -50,10 +49,15 @@ internal class PricePerformanceConverter( ) } - private fun TokenMarketInfo.Range.calculateFraction(): Float { - if (low == null || high == null || low == BigDecimal.ZERO) return 0f - return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP) - .setScale(2, RoundingMode.HALF_UP) - .toFloat().coerceAtMost(1f) + private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float { + return when { + low == null || high == null || high == BigDecimal.ZERO || currentPrice < low -> 0f + currentPrice > high || low == high -> 1f + else -> { + (currentPrice - low!!).divide(high!! - low!!, RoundingMode.HALF_UP) + .setScale(2, RoundingMode.HALF_UP) + .toFloat().coerceAtMost(1f) + } + } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt index f03cbff9c4..e90ad2b0d3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -17,6 +17,9 @@ internal class TokenMarketInfoConverter( ) : Converter { private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) + + @Suppress("UnusedPrivateMember") + // TODO second markets iteration private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency) @@ -25,9 +28,14 @@ internal class TokenMarketInfoConverter( override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { return MarketsTokenDetailsUM.InformationBlocks( insights = value.insights?.let { insightsConverter.convert(it) }, - securityScore = securityScoreConverter.convert(Unit), + securityScore = null, metrics = value.metrics?.let { metricsConverter.convert(it) }, - pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) }, + pricePerformance = value.pricePerformance?.let { + pricePerformanceConverter.convert( + value = it, + currentPrice = value.quotes.currentPrice, + ) + }, links = value.links?.let { linksConverter.convert(it) }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt index acc0ec44fa..5abc296917 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -23,7 +23,7 @@ internal object MarketsDateTimeFormatters { private val dateFormatter = DateTimeFormatters.dateDDMMYYYY - internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { + fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { return when (interval) { PriceChangeInterval.H24 -> { value: BigDecimal -> value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter) @@ -44,7 +44,7 @@ internal object MarketsDateTimeFormatters { } } - internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { + fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { return when (interval) { PriceChangeInterval.H24 -> resourceReference(R.string.common_today) PriceChangeInterval.WEEK, @@ -78,10 +78,7 @@ internal object MarketsDateTimeFormatters { } } - internal fun formatDateByIntervalWithMarker( - interval: PriceChangeInterval, - markerTimestamp: BigDecimal, - ): TextReference { + fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference { return when (interval) { PriceChangeInterval.H24, PriceChangeInterval.WEEK, @@ -127,4 +124,14 @@ internal object MarketsDateTimeFormatters { PriceChangeInterval.ALL_TIME -> 0 } } + + fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference { + return formatDateByInterval( + interval = interval, + startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( + interval = interval, + currentTimestamp = currentTimestamp, + ), + ) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt new file mode 100644 index 0000000000..dd664b03ca --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -0,0 +1,83 @@ +package com.tangem.features.markets.details.impl.model.state + +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenQuotes +import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter +import com.tangem.features.markets.details.impl.model.formatter.* +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.utils.Provider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import org.joda.time.DateTime +import java.math.BigDecimal + +internal class QuotesStateUpdater( + private val currentAppCurrency: Provider, + private val state: MutableStateFlow, + private val currentQuotes: MutableStateFlow, + private val lastUpdatedTimestamp: MutableStateFlow, + private val currentTokenInfo: MutableStateFlow, +) { + private val pricePerformanceConverter = PricePerformanceConverter(currentAppCurrency) + + suspend fun updateQuotes(newQuotes: TokenQuotes) { + val triggerPriceChangeType = getFormattedPriceChange( + currentPrice = currentQuotes.value.currentPrice, + updatedPrice = newQuotes.currentPrice, + ) + val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { + triggeredEvent( + data = triggerPriceChangeType, + onConsume = { + state.update { it.copy(triggerPriceChange = consumedEvent()) } + }, + ) + } else { + consumedEvent() + } + + val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) + val priceChangeType = percent.percentChangeType() + + // wait until marker is removed + state.first { it.markerSet.not() } + + currentQuotes.value = newQuotes + lastUpdatedTimestamp.value = DateTime.now().millis + + state.update { stateToUpdate -> + stateToUpdate.copy( + priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()), + priceChangePercentText = newQuotes.getFormattedPercentByInterval( + interval = stateToUpdate.selectedInterval, + ), + priceChangeType = priceChangeType, + triggerPriceChange = trigger, + dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString( + stateToUpdate.selectedInterval, + currentTimestamp = lastUpdatedTimestamp.value, + ), + body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice), + ) + } + } + + private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body { + val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this + + return if (this is MarketsTokenDetailsUM.Body.Content) { + copy( + infoBlocks = infoBlocks.copy( + pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price), + ), + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index e801ed3cb0..23e21f4840 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.details.impl.ui +import android.content.res.Configuration import androidx.compose.animation.Animatable import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween @@ -193,7 +194,7 @@ private fun TokenPriceText( val fallColor = TangemTheme.colors.text.warning val generalColor = TangemTheme.colors.text.primary1 - val color = remember { Animatable(generalColor) } + val color = remember(generalColor) { Animatable(generalColor) } EventEffect(triggerPriceChange) { val nextColor = when (it) { @@ -268,12 +269,11 @@ fun PriceChangeInterval.getText(): TextReference { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview() { TangemThemePreview { - Content( - modifier = Modifier.background(TangemTheme.colors.background.tertiary), - addTopBarStatusBarInsets = false, + MarketsTokenDetailsContent( state = MarketsTokenDetailsUM( tokenName = "Token Name", priceText = "$0.00000000324", @@ -303,6 +303,7 @@ private fun Preview() { onBackClick = {}, backgroundColor = TangemTheme.colors.background.tertiary, portfolioBlock = {}, + addTopBarStatusBarPadding = false, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt index e895b421f5..aca7dd32ae 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer import com.tangem.features.markets.impl.R +import com.tangem.utils.StringsSigns @Composable internal fun Description( @@ -33,7 +34,7 @@ internal fun Description( append(description.resolveReference()) } withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { - append(" " + stringResource(R.string.common_read_more)) + append(" " + stringResource(R.string.common_read_more).replace(' ', StringsSigns.NON_BREAKING_SPACE)) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index ca7e4d4390..92d905ff2e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -60,6 +60,7 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { title = stringResource(id = R.string.markets_token_details_blockchain_site), links = state.blockchainSite, onLinkClick = state.onLinkClick, + lastBlock = true, ) } }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 66a09fb533..057d1bbed6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -124,15 +124,16 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi ) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( + modifier = Modifier.weight(1f), text = state.low, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, ) - SpacerW8() Text( + modifier = Modifier.weight(1f), text = state.high, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 620e182622..5d9e93fd4e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -126,9 +126,10 @@ private fun LazyListScope.loadingInfoBlocks() { InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) } - item("securityScore-loading") { - SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) - } + // TODO second markets iteration + // item("securityScore-loading") { + // SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) + // } item("metrics-loading") { MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt index b1982093d4..8b28533fb3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.markets.details.impl.ui.state -import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.ImmutableList internal data class MetricsUM( - val metrics: PersistentList, + val metrics: ImmutableList, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt index dccea6ad25..5f30e89585 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -30,7 +30,6 @@ internal fun EntryBottomSheetContent( modifier: Modifier = Modifier, ) { val primary = TangemTheme.colors.background.primary - val secondary = TangemTheme.colors.background.secondary val backgroundColor = remember { Animatable(primary) } LocalMainBottomSheetColor.current.value = backgroundColor.value @@ -73,7 +72,7 @@ private fun BackgroundColorEffects( bottomSheetState: State, ) { val primary = TangemTheme.colors.background.primary - val secondary = TangemTheme.colors.background.secondary + val tertiary = TangemTheme.colors.background.tertiary // Order of LaunchedEffects is important here @@ -81,7 +80,7 @@ private fun BackgroundColorEffects( when (activeChild) { is MarketsEntryChildFactory.Child.TokenDetails -> { backgroundColor.animateTo( - secondary, + tertiary, animationSpec = tween(durationMillis = 500), ) } @@ -99,7 +98,7 @@ private fun BackgroundColorEffects( when (bottomSheetState.value) { BottomSheetState.EXPANDED -> { backgroundColor.animateTo( - secondary, + tertiary, animationSpec = tween(durationMillis = 100), ) } @@ -113,12 +112,12 @@ private fun BackgroundColorEffects( } } - LaunchedEffect(primary, secondary) { + LaunchedEffect(primary, tertiary) { if (backgroundColor.isRunning) return@LaunchedEffect when (activeChild) { is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.snapTo(secondary) + backgroundColor.snapTo(tertiary) } MarketsEntryChildFactory.Child.TokenList -> { backgroundColor.snapTo(primary) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 42184de78e..5967eaa915 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -17,6 +17,8 @@ import com.tangem.domain.markets.* import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -29,7 +31,7 @@ internal class TokenMarketBlockModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, - private val getTokenQuotesUseCase: GetTokenQuotesUseCase, + private val getTokenQuotesUseCase: GetCurrencyQuotesUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { @@ -46,6 +48,7 @@ internal class TokenMarketBlockModel @Inject constructor( ) private var quotesState: QuotesState? = null + private val quotesUpdateJobHolder = JobHolder() val state = MutableStateFlow( TokenMarketBlockUM( @@ -65,10 +68,11 @@ internal class TokenMarketBlockModel @Inject constructor( private fun startFetching() { modelScope.launch { getTokenQuotesUseCase( - tokenId = params.tokenId, + currencyID = params.cryptoCurrencyID, interval = PriceChangeInterval.H24, + refresh = true, ).collect { - it.onRight { res -> + it.onSome { res -> quotesState = QuotesState( currentPrice = res.fiatRate, h24Percent = res.priceChange, @@ -90,7 +94,7 @@ internal class TokenMarketBlockModel @Inject constructor( ) } } - } + }.saveIn(quotesUpdateJobHolder) modelScope.launch(dispatchers.main) { val result = getTokenPriceChartUseCase( @@ -131,7 +135,6 @@ internal class TokenMarketBlockModel @Inject constructor( ), ) - // FIXME navigation crash [REDACTED_TASK_KEY] router.push( AppRoute.MarketsTokenDetails( token = tokenParam, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 67e5d7305e..8191f0addf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.TextShimmer @@ -83,19 +84,21 @@ private fun LeftSide( FlowRow( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { - Text( - text = priceText, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, + TokenPriceText( + modifier = Modifier.alignByBaseline(), + price = priceText, + priceChangeType = type, ) Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { PriceChangeInPercent( + modifier = Modifier.alignByBaseline(), valueInPercent = percentText, type = type, ) Text( + modifier = Modifier.alignByBaseline(), text = stringResource(id = R.string.wallet_marketprice_block_update_time), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 50170ce417..f22b26c622 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -192,7 +192,7 @@ internal class MarketsListUMStateManager( private fun state(): MarketsListUM = MarketsListUM( list = ListUM.Loading, searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + placeholderText = resourceReference(R.string.common_search), query = "", onQueryChange = { searchQuery = it }, isActive = false, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index e7164e73ab..fa2964129f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -256,7 +256,7 @@ private fun Preview() { onItemClick = {}, ), searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + placeholderText = resourceReference(R.string.common_search), query = "", onQueryChange = {}, isActive = false, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index 7658893fb7..c153024da0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -1,185 +1,46 @@ package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.IntOffset import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.features.markets.impl.R -import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.utils.StringsSigns.MINUS -import kotlinx.coroutines.launch -import kotlin.math.roundToInt import kotlin.random.Random -internal enum class DragValue { Start, End } - -const val SWIPE_THRESHOLD_PERCENT = 0.8f -const val SWIPE_VELOCITY_THRESHOLD = 20f - -@Suppress("LongMethod") -@OptIn(ExperimentalFoundationApi::class) @Composable -fun MarketsListItem( - model: MarketsListItemUM, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, - onSwipeToAction: () -> Unit = {}, -) { - val actionWidth = TangemTheme.dimens.size68 - val actionWidthPx = with(LocalDensity.current) { actionWidth.toPx() } - - val hapticManager = LocalHapticManager.current - - val anchors = DraggableAnchors { - DragValue.Start at 0f - DragValue.End at -actionWidthPx - } - val state = remember { - AnchoredDraggableState( - initialValue = DragValue.Start, - anchors = anchors, - positionalThreshold = { it * (1 - SWIPE_THRESHOLD_PERCENT) }, - velocityThreshold = { SWIPE_VELOCITY_THRESHOLD }, - animationSpec = tween(easing = FastOutSlowInEasing), - confirmValueChange = { it == DragValue.Start }, - ) - } - val dragInteractionSource = remember { MutableInteractionSource() } - val clickInteractionSource = remember { MutableInteractionSource() } - val isInDraggedState by dragInteractionSource.collectIsDraggedAsState() - - LaunchedEffect(Unit) { - var actionPerformed = false - var releasePerformed = true - launch { - snapshotFlow { state.offset } - .collect { - val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT - if (it < border && actionPerformed.not()) { - hapticManager.perform(TangemHapticEffect.View.GestureThresholdActivate) - actionPerformed = true - releasePerformed = false - } - - if (it > border) { - if (releasePerformed.not()) { - hapticManager.perform(TangemHapticEffect.View.GestureThresholdDeactivate) - releasePerformed = true - } - actionPerformed = false - } - } - } - launch { - snapshotFlow { isInDraggedState } - .collect { - if (it.not() && actionPerformed) { - releasePerformed = true - onSwipeToAction() - } - } - } - } - - Box( - modifier = Modifier - .height(intrinsicSize = IntrinsicSize.Min) - .fillMaxWidth(), - ) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .fillMaxHeight() - .offset { - IntOffset( - x = actionWidthPx.roundToInt() + - state - .requireOffset() - .toInt(), - y = 0, - ) - } - .width(actionWidth) - .background(TangemTheme.colors.control.checked), - contentAlignment = Alignment.Center, - ) { - Image( - modifier = Modifier.size(TangemTheme.dimens.size28), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_mini_28), - colorFilter = ColorFilter.tint(TangemTheme.colors.icon.primary2), - contentDescription = null, - ) - } - - Box( - modifier = modifier - .align(Alignment.CenterStart) - .clip(RectangleShape) - .offset { - IntOffset( - x = state - .requireOffset() - .toInt(), - y = 0, - ) - } - .anchoredDraggable( - state = state, - orientation = Orientation.Horizontal, - interactionSource = dragInteractionSource, - ) - .clickable( - enabled = true, - interactionSource = clickInteractionSource, - indication = rememberRipple(), - onClick = onClick, - ), - ) { - MarketsListItemContent(model = model) - } - } +internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { + MarketsListItemContent( + modifier = modifier + .fillMaxWidth() + .clip(RectangleShape) + .clickable(onClick = onClick), + model = model, + ) } @Composable @@ -325,38 +186,6 @@ private fun RowScope.TokenMarketCapText(text: String) { ) } -@Composable -private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { - val growColor = TangemTheme.colors.text.accent - val fallColor = TangemTheme.colors.text.warning - val generalColor = TangemTheme.colors.text.primary1 - - val color = remember { Animatable(generalColor) } - - LaunchedEffect(price) { - if (priceChangeType != null) { - val nextColor = when (priceChangeType) { - PriceChangeType.UP, - -> growColor - PriceChangeType.DOWN -> fallColor - PriceChangeType.NEUTRAL -> return@LaunchedEffect - } - - color.animateTo(nextColor, snap()) - color.animateTo(generalColor, tween(durationMillis = 500)) - } - } - - Text( - modifier = modifier, - text = price, - color = color.value, - maxLines = 1, - style = TangemTheme.typography.body2, - overflow = TextOverflow.Visible, - ) -} - @Composable private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) { val chartWidth = TangemTheme.dimens.size56 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index 9385875fb1..c942539d2f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -88,6 +88,7 @@ internal class TokenDetailsFragment : ComposeFragment() { val tokenId = id.rawCurrencyId ?: return null // token price is not available return TokenMarketBlockComponent.Params( + cryptoCurrencyID = id, tokenId = tokenId, tokenName = name, tokenSymbol = symbol, From 4f7e1822857f35826457b110af6fce8ef3679b0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 16:04:23 +0300 Subject: [PATCH 060/197] Updated on 2026-08-14 --- .../com/tangem/common/ui/charts/state/Utils.kt | 13 +++++++++++++ .../converter/PriceAndTimePointValuesConverter.kt | 11 ++--------- .../details/impl/model/MarketsTokenDetailsModel.kt | 5 +++-- .../token/block/impl/model/TokenMarketBlockModel.kt | 8 ++++++-- .../model/converters/MarketsTokenItemConverter.kt | 3 ++- 5 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt new file mode 100644 index 0000000000..558f63dbde --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/Utils.kt @@ -0,0 +1,13 @@ +package com.tangem.common.ui.charts.state + +import kotlinx.collections.immutable.toImmutableList + +fun MarketChartData.Data.sorted(): MarketChartData.Data { + val points = this.x.zip(this.y).sortedBy { it.first } + val (x, y) = points.unzip() + + return MarketChartData.Data( + x = x.toImmutableList(), + y = y.toImmutableList(), + ) +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt index 8eeec1a691..e51a8f72a6 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt @@ -22,11 +22,6 @@ class PriceAndTimePointValuesConverter( private val formatYValuesCache = mutableMapOf() private val formatXValuesCache = mutableMapOf() - private data class Point( - val x: BigDecimal, - val y: BigDecimal, - ) - override fun convert(data: MarketChartData.Data): MarketChartRawData { formatYValuesCache.clear() formatXValuesCache.clear() @@ -38,10 +33,8 @@ class PriceAndTimePointValuesConverter( ) minMaxCache = cache - val points = data.x.zip(data.y) { x, y -> Point(x, y) }.sortedBy { it.x } - - val normY = points.map { it.y }.normalizeToDouble(min = cache.minY, max = cache.maxY) - val normX = points.map { it.x }.normalizeTime(min = cache.minX, max = cache.maxX) + val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY) + val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX) return if (normX.size > MAX_POINTS) { LTThreeBuckets diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 3f01b427d9..e899cfeb23 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.sorted import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -237,9 +238,9 @@ internal class MarketsTokenDetailsModel @Inject constructor( chart.onRight { chartDataProducer.runTransactionSuspend { chartData = MarketChartData.Data( - x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(), y = it.priceY.toImmutableList(), - ) + x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted() updateLook { it.copy( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 5967eaa915..bb584f1731 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -105,13 +106,16 @@ internal class TokenMarketBlockModel @Inject constructor( ) result.onRight { res -> + // wait until quotes are loaded + state.first { it.currentPrice != null } + state.update { stateToUpdate -> stateToUpdate.copy( chartData = priceAndTimePointValuesConverter.convert( MarketChartData.Data( y = res.priceY.toImmutableList(), - x = res.timeStamps.sorted().map { it.toBigDecimal() }.toImmutableList(), - ), + x = res.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted(), ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index c43e29f406..295cebedcf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.tokenlist.impl.model.converters import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -115,7 +116,7 @@ internal class MarketsTokenItemConverter( MarketChartData.Data( y = ct.priceY.toImmutableList(), x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(), - ), + ).sorted(), ) } } From 4f3d1f10c7a264d40a2876c328973405d72e822f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 12:24:26 +0300 Subject: [PATCH 061/197] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 15 ++ .../DefaultTransactionRepository.kt | 15 ++ .../domain/feedback/FeedbackDataBuilder.kt | 9 +- .../feedback/models/FeedbackEmailType.kt | 4 +- .../utils/EmailMessageBodyResolver.kt | 10 +- .../transaction/TransactionRepository.kt | 8 + .../usecase/SendMultipleTransactionUseCase.kt | 147 ++++++++++++++++++ .../SetConfirmationStateAssentTransformer.kt | 18 +-- ...etConfirmationStateCompletedTransformer.kt | 18 +-- .../viewmodel/StakingViewModel.kt | 102 +++++++----- gradle/dependencies.toml | 2 +- 11 files changed, 268 insertions(+), 80 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 73c0236567..e50891f228 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -42,6 +42,21 @@ internal object TransactionDomainModule { ) } + @Provides + @Singleton + fun provideSendMultipleTransactionUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, + walletManagersFacade: WalletManagersFacade, + ): SendMultipleTransactionUseCase { + return SendMultipleTransactionUseCase( + demoConfig = DemoConfig(), + cardSdkConfigRepository = cardSdkConfigRepository, + transactionRepository = transactionRepository, + walletManagersFacade = walletManagersFacade, + ) + } + @Provides @Singleton fun provideAssociateAssetUseCase( diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 87f16d07be..0452da5a7c 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -158,6 +158,21 @@ internal class DefaultTransactionRepository( (walletManager as TransactionSender).send(txData, signer) } + override suspend fun sendMultipleTransactions( + txsData: List, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + (walletManager as TransactionSender).sendMultiple(txsData, signer) + } + override fun createTransactionDataExtras( data: String, network: Network, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 30d5dd83a8..91f1f6b990 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -86,10 +86,13 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") } - fun addStakingInfo(validatorName: String?, transactionType: String?, unsignedTransaction: String?) { + fun addStakingInfo(validatorName: String?, transactionTypes: List, unsignedTransactions: List) { builder.appendKeyValue("Validator", validatorName ?: "unknown") - builder.appendKeyValue("Action", transactionType ?: "unknown") - builder.appendKeyValue("Unsigned transaction", unsignedTransaction ?: "unknown") + builder.appendKeyValue("Action", transactionTypes.joinToString(separator = "\n").ifEmpty { "unknown" }) + builder.appendKeyValue( + key = "Unsigned transaction", + value = unsignedTransactions.joinToString(separator = "\n") { it ?: "unknown" }.ifEmpty { "unknown" }, + ) } fun addDelimiter(): StringBuilder = builder.appendDelimiter() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 1be0afb1ff..735365aeac 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -27,7 +27,7 @@ sealed interface FeedbackEmailType { data class StakingProblem( override val cardInfo: CardInfo, val validatorName: String?, - val transactionType: String?, - val unsignedTransaction: String?, + val transactionTypes: List, + val unsignedTransactions: List, ) : FeedbackEmailType } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index c314b7a4bc..ae17e1da30 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -26,8 +26,8 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.StakingProblem -> addStakingProblemBody( type.cardInfo, type.validatorName, - type.transactionType, - type.unsignedTransaction, + type.transactionTypes, + type.unsignedTransactions, ) } @@ -81,8 +81,8 @@ internal class EmailMessageBodyResolver( private suspend fun FeedbackDataBuilder.addStakingProblemBody( cardInfo: CardInfo, validatorName: String?, - transactionType: String?, - unsignedTransaction: String?, + transactionTypes: List, + unsignedTransactions: List, ) { addCardInfo(cardInfo) addDelimiter() @@ -102,7 +102,7 @@ internal class EmailMessageBodyResolver( addDelimiter() } - addStakingInfo(validatorName, transactionType, unsignedTransaction) + addStakingInfo(validatorName, transactionTypes, unsignedTransactions) addDelimiter() addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 6784c1726f..247f3c7605 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult +import com.tangem.blockchain.common.transaction.TransactionsSendResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.models.TransactionType @@ -58,6 +59,13 @@ interface TransactionRepository { network: Network, ): com.tangem.blockchain.extensions.Result + suspend fun sendMultipleTransactions( + txsData: List, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): com.tangem.blockchain.extensions.Result + fun createTransactionDataExtras( data: String, network: Network, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt new file mode 100644 index 0000000000..c7fce8b278 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendMultipleTransactionUseCase.kt @@ -0,0 +1,147 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.transaction.TransactionsSendResult +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.network.ResultChecker +import com.tangem.common.core.TangemSdkError +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.DemoTransactionSender +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.R +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.sdk.extensions.localizedDescriptionRes +import com.tangem.utils.toFormattedString + +// TODO [REDACTED_TASK_KEY] merge with SendTransactionUseCase +class SendMultipleTransactionUseCase( + private val demoConfig: DemoConfig, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val transactionRepository: TransactionRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + suspend operator fun invoke( + txsData: List, + userWallet: UserWallet, + network: Network, + ): Either> { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner(cardId = card.cardId.takeIf { isCardNotBackedUp }) + + val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() + if (userWallet.scanResponse.card.isStart2Coin) { + cardSdkConfigRepository.setLinkedTerminal(false) + } + val sendResult = try { + if (demoConfig.isDemoCardId(cardId = userWallet.cardId)) { + sendDemo( + userWallet = userWallet, + network = network, + transactionsData = txsData, + signer = signer, + ) + } else { + val sendResult = transactionRepository.sendMultipleTransactions( + txsData = txsData, + signer = signer, + userWalletId = userWallet.walletId, + network = network, + ) + when (sendResult) { + is Result.Failure -> handleError(sendResult).left() + is Result.Success -> sendResult.data.right() + } + } + } catch (ex: Exception) { + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + SendTransactionError.DataError(ex.message).left() + } + + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + return sendResult.fold( + ifRight = { result -> result.hashes.right() }, + ifLeft = { it.left() }, + ) + } + + private suspend fun sendDemo( + userWallet: UserWallet, + network: Network, + transactionsData: List, + signer: TransactionSigner, + ): Either { + val demoTransactionSender = DemoTransactionSender( + walletManagersFacade + .getOrCreateWalletManager(userWallet.walletId, network) + ?: error("WalletManager is null"), + ) + + val result = demoTransactionSender.sendMultiple(transactionDataList = transactionsData, signer = signer) + + return if (result is Result.Failure && result.error.customMessage.contains(DemoTransactionSender.ID)) { + SendTransactionError.DemoCardError.left() + } else { + TransactionsSendResult(listOf("hash")).right() + } + } + + private fun handleError(result: Result.Failure): SendTransactionError { + if (ResultChecker.isNetworkError(result)) { + return SendTransactionError.NetworkError( + code = result.error.message, + message = result.error.customMessage, + ) + } + val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() + return when (error) { + is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) + is BlockchainSdkError.CreateAccountUnderfunded -> { + val minAmount = error.minReserve + val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty() + SendTransactionError.CreateAccountUnderfunded(minValue) + } + else -> { + SendTransactionError.BlockchainSdkError( + code = error.code, + message = error.customMessage, + ) + } + } + } + + private fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { + return if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + when (val tangemError = error.tangemError) { + is TangemSdkError -> { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) + } + is BlockchainSdkError.WrappedTangemError -> { + parseWrappedError(tangemError) // todo remove when sdk errors are revised + } + else -> { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index ca470397b6..6a214fde9a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -1,10 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState @@ -17,31 +15,23 @@ import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - private val stakingGasEstimate: StakingGasEstimate, + private val fee: Fee, private val pendingActionList: ImmutableList, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate), + confirmationState = prevState.confirmationState.copyWrapped(fee), ) } - private fun StakingStates.ConfirmationState.copyWrapped( - gasEstimate: StakingGasEstimate, - ): StakingStates.ConfirmationState { + private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( - fee = Fee.Common( - Amount( - currencySymbol = gasEstimate.token.symbol, - value = gasEstimate.amount, - decimals = gasEstimate.token.decimals, - ), - ), + fee = fee, rate = feeCryptoCurrencyStatus?.value?.fiatRate, isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 6b11ad41ab..27e6593bd0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,9 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -15,32 +13,24 @@ import com.tangem.utils.transformer.Transformer internal class SetConfirmationStateCompletedTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - private val stakingGasEstimate: StakingGasEstimate, + private val fee: Fee, private val txUrl: String, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate), + confirmationState = prevState.confirmationState.copyWrapped(fee), ) } - private fun StakingStates.ConfirmationState.copyWrapped( - gasEstimate: StakingGasEstimate, - ): StakingStates.ConfirmationState { + private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( isPrimaryButtonEnabled = true, innerState = InnerConfirmationStakingState.COMPLETED, feeState = FeeState.Content( - fee = Fee.Common( - Amount( - currencySymbol = gasEstimate.token.symbol, - value = gasEstimate.amount, - decimals = gasEstimate.token.decimals, - ), - ), + fee = fee, rate = feeCryptoCurrencyStatus?.value?.fiatRate, isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 42a33c0bfd..8dea58c5cd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -6,7 +6,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle @@ -28,7 +30,6 @@ import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams -import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.FetchPendingTransactionsUseCase @@ -37,10 +38,7 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase -import com.tangem.domain.transaction.usecase.GetAllowanceUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase @@ -70,6 +68,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber import java.math.BigDecimal +import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import kotlin.properties.Delegates @@ -87,6 +86,7 @@ internal class StakingViewModel @Inject constructor( private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, private val estimateGasUseCase: EstimateGasUseCase, private val sendTransactionUseCase: SendTransactionUseCase, + private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, private val submitHashUseCase: SubmitHashUseCase, @@ -143,7 +143,7 @@ internal class StakingViewModel @Inject constructor( private var stakingApproval: StakingApproval = StakingApproval.Empty private val allowanceTaskScheduler = SingleTaskScheduler() - private var transactionInProgress: StakingTransaction? = null + private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() private var approvalJobHolder: JobHolder = JobHolder() @@ -187,7 +187,7 @@ internal class StakingViewModel @Inject constructor( val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address") - val stakingTransaction = getStakingTransactionUseCase( + val stakingTransactions = getStakingTransactionUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, params = ActionParams( @@ -206,9 +206,9 @@ internal class StakingViewModel @Inject constructor( return@launch } - stakingTransaction + val fullTransactionsData = stakingTransactions .filterNot { it.type == StakingTransactionType.APPROVAL } - .forEach { transaction -> + .map { transaction -> val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase( networkId = cryptoCurrencyStatus.currency.network.id.value, fee = fee, @@ -218,17 +218,20 @@ internal class StakingViewModel @Inject constructor( stakingEventFactory.createStakingErrorAlert(it) return@launch } - val gasEstimate = constructedTransaction.gasEstimate - ?: return@launch stakingEventFactory.createGenericErrorAlert("Gas estimate is null") - transactionInProgress = constructedTransaction - sendStakingTransaction( - transactionId = constructedTransaction.id, - gasEstimate = gasEstimate, - txData = transactionData, - pendingActionList = confirmationState.pendingActions, + FullTransactionData( + stakeKitTransaction = constructedTransaction, + tangemTransaction = transactionData, ) } + + transactionsInProgress.addAll(fullTransactionsData.map { it.stakeKitTransaction }) + + sendStakingTransaction( + fullTransactionsData = fullTransactionsData, + fee = fee, + pendingActionList = confirmationState.pendingActions, + ) } } } @@ -291,7 +294,7 @@ internal class StakingViewModel @Inject constructor( validatorAddress: String, ) { val pendingAction = pendingActions.firstOrNull() - val stakingGasEstimate = estimateGasUseCase( + val gasEstimate = estimateGasUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, params = ActionParams( @@ -313,7 +316,13 @@ internal class StakingViewModel @Inject constructor( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - stakingGasEstimate = stakingGasEstimate, + fee = Fee.Common( + Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ), + ), pendingActionList = pendingActions, ), ) @@ -555,8 +564,8 @@ internal class StakingViewModel @Inject constructor( val email = FeedbackEmailType.StakingProblem( cardInfo = cardInfo, validatorName = validator?.name, - transactionType = transactionInProgress?.type?.name, - unsignedTransaction = transactionInProgress?.unsignedTransaction, + transactionTypes = transactionsInProgress.map { it.type.name }, + unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction }, ) feedbackManager.sendEmail(email) @@ -632,13 +641,12 @@ internal class StakingViewModel @Inject constructor( } private suspend fun sendStakingTransaction( - transactionId: String, - gasEstimate: StakingGasEstimate, - txData: TransactionData, + fullTransactionsData: List, + fee: Fee, pendingActionList: ImmutableList, ) { - sendTransactionUseCase( - txData = txData, + sendMultipleTransactionUseCase( + txsData = fullTransactionsData.map { it.tangemTransaction }, userWallet = userWallet, network = cryptoCurrencyStatus.currency.network, ).fold( @@ -648,18 +656,21 @@ internal class StakingViewModel @Inject constructor( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - stakingGasEstimate = gasEstimate, + fee = fee, pendingActionList = pendingActionList, ), ) stakingEventFactory.createSendTransactionErrorAlert(error) }, - ifRight = { txHash -> - transactionInProgress = null - submitHash(transactionId, txHash) + ifRight = { transactionHashes -> + transactionsInProgress.clear() + submitHash( + transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, + transactionHashes = transactionHashes, + ) scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( - txHash = txHash, + txHash = transactionHashes.last(), networkId = cryptoCurrencyStatus.currency.network.id, ).getOrElse { "" } @@ -667,7 +678,7 @@ internal class StakingViewModel @Inject constructor( SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - stakingGasEstimate = gasEstimate, + fee = fee, txUrl = txUrl, ), ) @@ -675,18 +686,22 @@ internal class StakingViewModel @Inject constructor( ) } - private suspend fun submitHash(transactionId: String, transactionHash: String) { - submitHashUseCase.submitHash( - transactionId = transactionId, - transactionHash = transactionHash, - ) - .onLeft { - saveUnsubmittedHashUseCase.invoke( + private suspend fun submitHash(transactionIds: List, transactionHashes: List) { + transactionIds + .zip(transactionHashes) + .forEach { (transactionId, transactionHash) -> + submitHashUseCase.submitHash( transactionId = transactionId, transactionHash = transactionHash, ) - }.onRight { - Timber.d("Successful hash submission") + .onLeft { + saveUnsubmittedHashUseCase.invoke( + transactionId = transactionId, + transactionHash = transactionHash, + ) + }.onRight { + Timber.d("Successful hash submission") + } } } @@ -750,6 +765,11 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } + private data class FullTransactionData( + val stakeKitTransaction: StakingTransaction, + val tangemTransaction: TransactionData.Compiled, + ) + private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 45137306fa..fc08cd3ad3 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-748" +tangemBlockchainSdk = "develop-750" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 9b23c9a7a2473b10e49f29cf5bee6819f3ed1856 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 15:32:52 +0500 Subject: [PATCH 062/197] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 15 ++ .../card/DefaultDerivationsRepository.kt | 7 +- .../domain/card/MissedDerivationsFinder.kt | 37 ++-- .../data/common/currency/TokensOperations.kt | 7 +- data/manage-tokens/build.gradle.kts | 2 + .../DefaultManageTokensRepository.kt | 204 +++++++++++++++++- .../managetokens/di/ManageTokensDataModule.kt | 12 +- .../utils/ManageTokensUpdateFetcher.kt | 4 +- .../utils/ManagedCryptoCurrencyFactory.kt | 26 ++- .../repository/DefaultCurrenciesRepository.kt | 2 +- .../utils/UserTokensBackwardCompatibility.kt | 2 +- .../card/repository/DerivationsRepository.kt | 4 + .../DefaultWalletManagersFacade.kt | 38 +++- .../walletmanager/WalletManagersFacade.kt | 3 + .../domain/walletmanager/model/TokenInfo.kt | 12 ++ domain/manage-tokens/build.gradle.kts | 9 +- .../model/ManagedCryptoCurrency.kt | 5 +- .../managetokens/SaveManagedTokensUseCase.kt | 33 +++ .../model/ManageTokensUpdateAction.kt | 2 +- .../repository/ManageTokensRepository.kt | 9 + .../PreviewCustomTokenSelectorComponent.kt | 12 +- .../preview/PreviewManageTokensComponent.kt | 13 +- .../entity/item/CurrencyNetworkUM.kt | 4 +- .../entity/managetokens/ManageTokensUM.kt | 3 + .../managetokens/model/ManageTokensModel.kt | 39 +++- .../managetokens/ui/ManageTokensScreen.kt | 9 +- .../utils/list/ChangedCurrenciesManager.kt | 36 ++-- .../utils/list/ManageTokensListManager.kt | 26 +-- .../utils/list/ManageTokensUiActions.kt | 6 +- .../utils/list/ManageTokensUiManager.kt | 8 +- .../utils/mapper/CurrencyNetworksMapper.kt | 4 +- .../utils/ui/CurrencyItemOperations.kt | 4 +- .../utils/ui/CurrencyNetworkOperations.kt | 2 +- 33 files changed, 484 insertions(+), 115 deletions(-) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 0dd7b11804..1513f4d484 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,5 +1,8 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.managetokens.SaveManagedTokensUseCase +import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -44,4 +47,16 @@ internal object ManageTokensDomainModule { ): CheckIsCurrencyNotAddedUseCase { return CheckIsCurrencyNotAddedUseCase(customTokensRepository) } + + @Provides + @Singleton + fun provideSaveManagedTokensUseCase( + manageTokensRepository: ManageTokensRepository, + derivationsRepository: DerivationsRepository, + ): SaveManagedTokensUseCase { + return SaveManagedTokensUseCase( + manageTokensRepository = manageTokensRepository, + derivationsRepository = derivationsRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 5f4b695932..a5d6f883b7 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -12,6 +12,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -30,6 +31,10 @@ internal class DefaultDerivationsRepository( ) : DerivationsRepository { override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { + derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) + } + + override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { @@ -38,7 +43,7 @@ internal class DefaultDerivationsRepository( } val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse) - .find(currencies) + .findByNetworks(networks) .ifEmpty { Timber.d("Nothing to derive") return diff --git a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt index 97c0a7bf8f..af980fd382 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt @@ -11,6 +11,7 @@ import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.operations.derivation.ExtendedPublicKeysMap private typealias DerivationData = Pair> @@ -26,8 +27,12 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { + return currencies.map { it.network }.let(::findByNetworks) + } + + fun findByNetworks(networks: List): Derivations { return buildMap> { - currencies + networks .mapToNewDerivations() .forEach { data -> val current = this[data.first] @@ -41,25 +46,25 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } } - private fun List.mapToNewDerivations(): List { + private fun List.mapToNewDerivations(): List { val config = CardConfig.createConfig(scanResponse.card) - return mapNotNull { currency -> - val blockchain = Blockchain.fromId(id = currency.network.id.value) + return mapNotNull { network -> + val blockchain = Blockchain.fromId(id = network.id.value) val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null - findNewDerivations(curve = curve, scanResponse = scanResponse, currency = currency) + findNewDerivations(curve = curve, scanResponse = scanResponse, network = network) } } private fun findNewDerivations( curve: EllipticCurve, scanResponse: ScanResponse, - currency: CryptoCurrency, + network: Network, ): DerivationData? { val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null val publicKey = wallet.publicKey.toMapKey() - val derivationCandidates = currency + val derivationCandidates = network .getDerivationCandidates(curve) .ifEmpty { return null } .filterAlreadyDerivedKeys(publicKey) @@ -68,13 +73,13 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { return publicKey to derivationCandidates } - private fun CryptoCurrency.getDerivationCandidates(curve: EllipticCurve): List { - val blockchain = Blockchain.fromId(id = network.id.value) + private fun Network.getDerivationCandidates(curve: EllipticCurve): List { + val blockchain = Blockchain.fromId(id = this.id.value) return buildList { add(blockchain.getDerivationPath(curve = curve)) - add(blockchain.getCustomDerivationPath(curve = curve, currency = this@getDerivationCandidates)) - add(blockchain.getCardanoDerivationPathIfNeeded(currency = this@getDerivationCandidates)) + add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates)) + add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates)) } .filterNotNull() .distinct() @@ -88,17 +93,17 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } } - private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, currency: CryptoCurrency): DerivationPath? { + private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? { return if (getSupportedCurves().contains(curve)) { - currency.network.derivationPath.value?.let(::DerivationPath) + network.derivationPath.value?.let(::DerivationPath) } else { null } } - private fun Blockchain.getCardanoDerivationPathIfNeeded(currency: CryptoCurrency): DerivationPath? { - return if (currency is CryptoCurrency.Coin && this == Blockchain.Cardano) { - currency.network.derivationPath.value?.let { + private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? { + return if (this == Blockchain.Cardano) { + network.derivationPath.value?.let { CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it)) } } else { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt index 2c2c8d95ad..724649c959 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt @@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency.ID import com.tangem.domain.tokens.model.Network import com.tangem.blockchain.common.Token as SdkToken @@ -65,10 +64,10 @@ fun getCoinIconUrl(blockchain: Blockchain): String? { return coinId?.let(::getTokenIconUrlFromDefaultHost) } -fun List.hasCoinForToken(token: CryptoCurrency.Token): Boolean { +fun List.hasCoinForToken(network: Network): Boolean { return any { - val blockchain = getBlockchain(networkId = token.network.id) - val tokenDerivation = token.network.derivationPath.value + val blockchain = getBlockchain(networkId = network.id) + val tokenDerivation = network.derivationPath.value it.id == blockchain.toCoinId() && it.derivationPath == tokenDerivation } } diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index 137a893c81..a6acc9a452 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -16,10 +16,12 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.legacy) /** Project - Data */ implementation(projects.core.datasource) implementation(projects.data.common) + implementation(projects.data.tokens) /** Project - Utils */ implementation(projects.core.utils) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index bb45239024..caea014d2c 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -3,15 +3,27 @@ package com.tangem.data.managetokens import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.getBlockchain +import com.tangem.data.common.currency.hasCoinForToken import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory +import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.TangemExpressValues +import com.tangem.datasource.api.express.models.request.AssetsRequestBody +import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver @@ -20,7 +32,11 @@ import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.pagination.BatchFetchResult @@ -28,16 +44,27 @@ import com.tangem.pagination.BatchListSource import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +@Suppress("LongParameterList") internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, - private val managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, + private val walletManagerFacade: WalletManagersFacade, + private val tangemExpressApi: TangemExpressApi, + private val expressAssetsStore: ExpressAssetsStore, ) : ManageTokensRepository { + private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory() + private val cryptoCurrencyFactory = CryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() + + // region getTokenListBatchFlow override fun getTokenListBatchFlow( context: ManageTokensListBatchingContext, batchSize: Int, @@ -58,7 +85,7 @@ internal class DefaultManageTokensRepository( prefetchDistance = batchSize, batchSize = batchSize, subFetcher = { request, _, isFirstBatchFetching -> - val userWallet = getUserWallet(request.params.userWalletId) + val userWallet = request.params.userWalletId?.let { getUserWallet(it) } val supportedBlockchains = getSupportedBlockchains(userWallet) val searchText = request.params.searchText?.takeIf { it.isNotBlank() } @@ -118,13 +145,9 @@ internal class DefaultManageTokensRepository( } } - private suspend fun getUserWallet(userWalletId: UserWalletId?): UserWallet? { - if (userWalletId == null) { - return null - } - + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet not found" + "Unable to find a user wallet with provided ID: $userWalletId" } } @@ -135,4 +158,169 @@ internal class DefaultManageTokensRepository( !it.isTestnet() && it.isSupportedInApp() } } + // endregion + + // region addManagedCurrencies + override suspend fun saveManagedCurrencies( + userWalletId: UserWalletId, + currenciesToAdd: Map>, + currenciesToRemove: Map>, + ) { + return withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, + ) + + val filteredTokens = currenciesToAdd.mapToTokenDataList() + .filterNot { tokenData -> + val blockchain = getBlockchain(networkId = tokenData.sourceNetwork.network.id) + val networkId = blockchain.toNetworkId() + val contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress + savedCurrencies.tokens.firstOrNull { token -> + token.contractAddress == contractAddress && + token.networkId == networkId && + token.derivationPath == tokenData.sourceNetwork.network.derivationPath.value + } != null + } + val newCoins = createCoinsForNewTokens( + userWalletId = userWalletId, + newTokensNetworks = filteredTokens.mapNotNull { tokenData -> + (tokenData.sourceNetwork as? SourceNetwork.Default)?.network + }, + savedCurrencies = savedCurrencies.tokens, + ) + val tokensToSave = filteredTokens.mapToUserResponseToken() + + val tokensToRemove = currenciesToRemove.mapToTokenDataList() + removeCurrenciesFromWalletManager(userWalletId = userWalletId, tokensDataList = tokensToRemove) + + val newCurrencies = (newCoins + tokensToSave).distinct() + val updatedResponse = savedCurrencies.copy( + tokens = savedCurrencies.tokens + newCurrencies - tokensToRemove.mapToUserResponseToken().toSet(), + ) + storeAndPushTokens(userWalletId = userWalletId, response = updatedResponse) + fetchExchangeableUserMarketCoinsByIds(userWalletId = userWalletId, userTokens = updatedResponse) + } + } + + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) + + pushTokens(userWalletId, response) + } + + private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) { + Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}") + } + } + + private suspend fun fetchExchangeableUserMarketCoinsByIds( + userWalletId: UserWalletId, + userTokens: UserTokensResponse, + ) { + try { + val tokensList = userTokens.tokens + .map { + LeastTokenInfo( + contractAddress = it.contractAddress ?: TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE, + network = it.networkId, + ) + } + + if (tokensList.isNotEmpty()) { + val response = tangemExpressApi.getAssets( + AssetsRequestBody( + tokensList = tokensList, + ), + ) + + expressAssetsStore.store(userWalletId, response.getOrThrow()) + } + } catch (e: Throwable) { + Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") + } + } + + private suspend fun removeCurrenciesFromWalletManager( + userWalletId: UserWalletId, + tokensDataList: List, + ) { + walletManagerFacade.removeTokensByTokenInfo( + userWalletId = userWalletId, + tokenInfos = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData -> + val contractAddress = + (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress ?: return@mapNotNullTo null + TokenInfo( + network = tokenData.sourceNetwork.network, + name = tokenData.token.name, + symbol = tokenData.token.symbol, + contractAddress = contractAddress, + decimals = tokenData.sourceNetwork.decimals, + id = tokenData.token.id.value, + ) + }, + ) + walletManagerFacade.remove( + userWalletId = userWalletId, + networks = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData -> + (tokenData.sourceNetwork as? SourceNetwork.Main)?.network + }, + ) + } + + private suspend fun createCoinsForNewTokens( + userWalletId: UserWalletId, + newTokensNetworks: List, + savedCurrencies: List, + ): List { + return newTokensNetworks + .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins + .mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = getBlockchain(networkId = it.id), + extraDerivationPath = it.derivationPath.value, + derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, + ) + } + .distinct() + .map(userTokensResponseFactory::createResponseToken) + } + + private fun List.mapToUserResponseToken(): List { + return map { tokenData -> + val blockchain = getBlockchain(tokenData.sourceNetwork.network.id) + UserTokensResponse.Token( + id = tokenData.token.id.value, + networkId = blockchain.toNetworkId(), + derivationPath = tokenData.sourceNetwork.network.derivationPath.value, + name = tokenData.token.name, + symbol = tokenData.token.symbol, + decimals = tokenData.sourceNetwork.decimals, + contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress, + ) + } + } + + private fun Map>.mapToTokenDataList(): List { + return flatMap { (token, networks) -> + token.availableNetworks + .filter { sourceNetwork -> networks.contains(sourceNetwork.network) } + .map { sourceNetwork -> TokenWithSourceNetwork(token = token, sourceNetwork = sourceNetwork) } + } + } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } + // endregion + + data class TokenWithSourceNetwork(val token: ManagedCryptoCurrency.Token, val sourceNetwork: SourceNetwork) } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 64d77526c1..37d4a334fb 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -3,12 +3,14 @@ package com.tangem.data.managetokens.di import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher -import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory +import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,18 +27,22 @@ internal object ManageTokensDataModule { fun provideManageTokensRepository( tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, - managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, + walletManagersFacade: WalletManagersFacade, + tangemExpressApi: TangemExpressApi, + expressAssetsStore: ExpressAssetsStore, ): ManageTokensRepository { return DefaultManageTokensRepository( tangemTechApi, userWalletsStore, - managedCryptoCurrencyFactory, manageTokensUpdateFetcher, appPreferencesStore, dispatchers, + walletManagersFacade, + tangemExpressApi, + expressAssetsStore, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt index 6c23f37b0b..66bfc00bc5 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt @@ -31,9 +31,9 @@ internal class ManageTokensUpdateFetcher @Inject constructor() : is ManagedCryptoCurrency.Token -> { currency.copy( addedIn = if (updateRequest.isSelected) { - currency.addedIn + updateRequest.networkId + currency.addedIn + updateRequest.network } else { - currency.addedIn - updateRequest.networkId + currency.addedIn - updateRequest.network }, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index de239a1f09..c848b36699 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -13,11 +13,8 @@ import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.tokens.model.Network -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -internal class ManagedCryptoCurrencyFactory @Inject constructor() { +internal class ManagedCryptoCurrencyFactory { fun create( coinsResponse: CoinsResponse, @@ -100,7 +97,7 @@ internal class ManagedCryptoCurrencyFactory @Inject constructor() { availableNetworks = coinResponse.networks.mapNotNull { network -> createSource(network, derivationStyleProvider) }, - addedIn = findAddedInNetworksIds(coinResponse.id, tokensResponse), + addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, derivationStyleProvider), ) } @@ -119,26 +116,35 @@ internal class ManagedCryptoCurrencyFactory @Inject constructor() { return if (contractAddress.isNullOrBlank()) { SourceNetwork.Main( network = network, + decimals = blockchain.decimals(), ) } else { SourceNetwork.Default( network = network, + decimals = requireNotNull(networkResponse.decimalCount?.toInt()), contractAddress = contractAddress, ) } } - private fun findAddedInNetworksIds(currencyId: String, tokensResponse: UserTokensResponse?): Set { + private fun findAddedInNetworks( + currencyId: String, + tokensResponse: UserTokensResponse?, + derivationStyleProvider: DerivationStyleProvider?, + ): Set { if (tokensResponse == null) return emptySet() return tokensResponse.tokens .filter { it.id == currencyId } - .map { it.networkId } - .mapNotNullTo(mutableSetOf()) { networkId -> - val blockchain = Blockchain.fromNetworkId(networkId) + .mapNotNullTo(mutableSetOf()) { token -> + val blockchain = Blockchain.fromNetworkId(token.networkId) if (blockchain != null && blockchain.isSupportedInApp()) { - Network.ID(blockchain.id) + getNetwork( + blockchain = blockchain, + extraDerivationPath = token.derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) } else { null } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 83d87b583b..93ad36b66f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -132,7 +132,7 @@ internal class DefaultCurrenciesRepository( savedCurrencies: List, ): List { return newTokens - .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins + .filterNot { savedCurrencies.hasCoinForToken(it.network) } // tokens without coins .mapNotNull { cryptoCurrencyFactory.createCoin( blockchain = getBlockchain(networkId = it.network.id), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt index ec40d6e52c..a4b3f4116d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt @@ -9,7 +9,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse * Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens * in new application with new IDs */ -internal class UserTokensBackwardCompatibility { +class UserTokensBackwardCompatibility { fun applyCompatibilityAndGetUpdated(userTokensResponse: UserTokensResponse): UserTokensResponse { return userTokensResponse.copy( diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt index d6b474839a..5c6512a557 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt @@ -3,6 +3,7 @@ package com.tangem.domain.card.repository import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -11,6 +12,9 @@ interface DerivationsRepository { @Throws suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) + @Throws + suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) + @Throws suspend fun derivePublicKeys( userWalletId: UserWalletId, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 7b187af646..6d6c0b3a4b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -31,6 +31,7 @@ import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.SmartContractMethod +import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.walletmanager.utils.* import com.tangem.domain.walletmanager.utils.WalletManagerFactory @@ -103,24 +104,45 @@ class DefaultWalletManagersFacade( tokens .groupBy(CryptoCurrency.Token::network) .forEach { (network, networkTokens) -> - removeTokens(userWalletId, network, networkTokens) + removeTokens( + userWalletId = userWalletId, + network = network, + networkTokens = sdkTokenConverter.convertList(networkTokens), + ) } } - private suspend fun removeTokens( - userWalletId: UserWalletId, - network: Network, - networkTokens: List, - ) { + override suspend fun removeTokensByTokenInfo(userWalletId: UserWalletId, tokenInfos: Set) { + if (tokenInfos.isEmpty()) return + + tokenInfos + .groupBy { it.network } + .forEach { (network, tokenInfoList) -> + removeTokens( + userWalletId = userWalletId, + network = network, + networkTokens = tokenInfoList.map { + Token( + name = it.name, + symbol = it.symbol, + contractAddress = it.contractAddress, + decimals = it.decimals, + id = it.id, + ) + }, + ) + } + } + + private suspend fun removeTokens(userWalletId: UserWalletId, network: Network, networkTokens: List) { withContext(dispatchers.io) { val walletManager = walletManagersStore.getSyncOrNull( userWalletId = userWalletId, blockchain = Blockchain.fromId(network.id.value), derivationPath = network.derivationPath.value, ) ?: return@withContext - val tokensToRemove = sdkTokenConverter.convertList(networkTokens) - tokensToRemove.forEach { token -> + networkTokens.forEach { token -> walletManager.removeToken(token) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index e55a359854..5104db3554 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -17,6 +17,7 @@ import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState +import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -54,6 +55,8 @@ interface WalletManagersFacade { suspend fun removeTokens(userWalletId: UserWalletId, tokens: Set) + suspend fun removeTokensByTokenInfo(userWalletId: UserWalletId, tokenInfos: Set) + /** * Returns [UpdateWalletManagerResult] with last pending transactions * diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt new file mode 100644 index 0000000000..9d1952b890 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/TokenInfo.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.walletmanager.model + +import com.tangem.domain.tokens.model.Network + +data class TokenInfo( + val network: Network, + val name: String, + val symbol: String, + val contractAddress: String, + val decimals: Int, + val id: String? = null, +) \ No newline at end of file diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index b24f993032..51a0e5f6ab 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -1,8 +1,13 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.managetokens" +} + dependencies { /* Domain */ @@ -10,6 +15,8 @@ dependencies { api(projects.domain.core) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.card) + implementation(projects.domain.legacy) /* Core */ api(projects.core.pagination) diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index f7f82002cf..595a8e1077 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -42,7 +42,7 @@ sealed class ManagedCryptoCurrency { override val symbol: String, override val iconUrl: String, val availableNetworks: List, - val addedIn: Set, + val addedIn: Set, ) : ManagedCryptoCurrency() { val isAdded: Boolean = addedIn.isNotEmpty() @@ -54,6 +54,7 @@ sealed class ManagedCryptoCurrency { sealed class SourceNetwork { abstract val network: Network + abstract val decimals: Int val id: Network.ID get() = network.id @@ -73,10 +74,12 @@ sealed class ManagedCryptoCurrency { data class Main( override val network: Network, + override val decimals: Int, ) : SourceNetwork() data class Default( override val network: Network, + override val decimals: Int, val contractAddress: String, ) : SourceNetwork() diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt new file mode 100644 index 0000000000..5a6ad0a7ce --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.flatten +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class SaveManagedTokensUseCase( + private val manageTokensRepository: ManageTokensRepository, + private val derivationsRepository: DerivationsRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currenciesToAdd: Map>, + currenciesToRemove: Map>, + ): Either { + return Either.catch { + derivationsRepository.derivePublicKeysByNetworks( + userWalletId = userWalletId, + networks = currenciesToAdd.values.flatten(), + ) + manageTokensRepository.saveManagedCurrencies( + userWalletId = userWalletId, + currenciesToAdd = currenciesToAdd, + currenciesToRemove = currenciesToRemove, + ) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt index 75b1b2e4ad..0d3af7a007 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt @@ -6,7 +6,7 @@ sealed class ManageTokensUpdateAction { data class AddCurrency( val currencyId: ManagedCryptoCurrency.ID, - val networkId: Network.ID, + val network: Network, val isSelected: Boolean, ) : ManageTokensUpdateAction() } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt index 3104d98d64..fab91ff3bb 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt @@ -2,8 +2,17 @@ package com.tangem.domain.managetokens.repository import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId interface ManageTokensRepository { fun getTokenListBatchFlow(context: ManageTokensListBatchingContext, batchSize: Int): ManageTokensListBatchFlow + + suspend fun saveManagedCurrencies( + userWalletId: UserWalletId, + currenciesToAdd: Map>, + currenciesToRemove: Map>, + ) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 7d596d3f8d..1ebcff8b5d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -51,7 +51,17 @@ internal class PreviewCustomTokenSelectorComponent( ) CurrencyNetworkUM( - networkId = n.id, + network = Network( + id = n.id, + backendId = n.id.value, + name = "", + currencySymbol = "", + derivationPath = Network.DerivationPath.Card(""), + isTestnet = false, + standardType = Network.StandardType.ERC20, + hasFiatFeeRate = false, + canHandleTokens = false, + ), name = "Network $index", type = "N$index", iconResId = R.drawable.ic_eth_16, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index efa89d5b8e..76be782c89 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -54,6 +54,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { isNextBatchLoading = true, loadMore = { false }, saveChanges = {}, + isSavingInProgress = false, ), ) @@ -134,7 +135,17 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> CurrencyNetworkUM( - networkId = Network.ID(networkIndex.toString()), + network = Network( + id = Network.ID(networkIndex.toString()), + backendId = networkIndex.toString(), + name = "", + currencySymbol = "", + derivationPath = Network.DerivationPath.Card(""), + isTestnet = false, + standardType = Network.StandardType.ERC20, + hasFiatFeeRate = false, + canHandleTokens = false, + ), name = "NETWORK$networkIndex", type = "N$networkIndex", iconResId = R.drawable.ic_eth_16, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt index 84f3aabb87..b666a09c2a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.managetokens.entity.item import com.tangem.domain.tokens.model.Network internal data class CurrencyNetworkUM( - val networkId: Network.ID, + val network: Network, val name: String, val type: String, val iconResId: Int, @@ -12,5 +12,5 @@ internal data class CurrencyNetworkUM( override val onSelectedStateChange: (Boolean) -> Unit, ) : SelectableItemUM { - override val id: String = networkId.value + override val id: String = network.id.value } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index 4fc9d627c7..33f1524452 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -36,6 +36,7 @@ internal sealed class ManageTokensUM { override val loadMore: () -> Boolean, val saveChanges: () -> Unit, val hasChanges: Boolean, + val isSavingInProgress: Boolean, ) : ManageTokensUM() fun copySealed( @@ -44,6 +45,7 @@ internal sealed class ManageTokensUM { hasChanges: Boolean = this is ManageContent && this.hasChanges, isInitialBatchLoading: Boolean = this.isInitialBatchLoading, isNextBatchLoading: Boolean = this.isNextBatchLoading, + isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -52,6 +54,7 @@ internal sealed class ManageTokensUM { hasChanges = hasChanges, isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, + isSavingInProgress = isSavingInProgress, ) is ReadContent -> copy( search = search, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 941fdad919..87e6636653 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -21,12 +22,14 @@ import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.utils.list.ChangedCurrencies import com.tangem.features.managetokens.utils.list.ManageTokensListManager +import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ComponentScoped @@ -35,6 +38,7 @@ internal class ManageTokensModel @Inject constructor( private val router: Router, private val manageTokensListManager: ManageTokensListManager, private val messageSender: UiMessageSender, + private val saveManagedTokensUseCase: SaveManagedTokensUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -76,7 +80,7 @@ internal class ManageTokensModel @Inject constructor( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, - items = getInitialItems(), + items = persistentListOf(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_search_tokens), onBackButtonClick = router::pop, @@ -97,7 +101,7 @@ internal class ManageTokensModel @Inject constructor( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, - items = getInitialItems(), + items = persistentListOf(), topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, @@ -116,6 +120,7 @@ internal class ManageTokensModel @Inject constructor( hasChanges = false, saveChanges = ::saveChanges, loadMore = ::loadMoreItems, + isSavingInProgress = false, ) } @@ -157,9 +162,16 @@ internal class ManageTokensModel @Inject constructor( isNextBatchLoading = false, ) } - is PaginationStatus.Paginating, - is PaginationStatus.EndOfPagination, - -> state.copySealed( + is PaginationStatus.Paginating -> { + (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> + Timber.e(fetchError.throwable) + } + state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + is PaginationStatus.EndOfPagination -> state.copySealed( isInitialBatchLoading = false, isNextBatchLoading = false, ) @@ -189,10 +201,6 @@ internal class ManageTokensModel @Inject constructor( return true } - private fun getInitialItems(): ImmutableList { - return persistentListOf() - } - private fun navigateToAddCustomToken() { params.userWalletId?.let { bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it)) @@ -200,7 +208,18 @@ internal class ManageTokensModel @Inject constructor( } private fun saveChanges() { - // TODO: [REDACTED_JIRA] + modelScope.launch { + state.update { state -> state.copySealed(isSavingInProgress = true) } + saveManagedTokensUseCase.invoke( + userWalletId = requireNotNull(params.userWalletId), + currenciesToAdd = manageTokensListManager.currenciesToAdd.value, + currenciesToRemove = manageTokensListManager.currenciesToRemove.value, + ).fold( + ifLeft = { Timber.e(it, "Failed to save changes") }, + ifRight = { router.pop() }, + ) + state.update { state -> state.copySealed(isSavingInProgress = false) } + } } private fun searchCurrencies(query: String) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 547963c3b0..c8771d1fa6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -102,6 +102,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), isVisible = state.hasChanges, + showProgress = state.isSavingInProgress, onClick = state.saveChanges, ) } @@ -133,7 +134,12 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM } @Composable -private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun SaveChangesButton( + isVisible: Boolean, + showProgress: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { AnimatedVisibility( modifier = modifier, visible = isVisible, @@ -144,6 +150,7 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: PrimaryButtonIconEnd( text = stringResource(id = R.string.common_save), iconResId = R.drawable.ic_tangem_24, + showProgress = showProgress, onClick = onClick, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt index 3e2705f9e6..32aa63bf2a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt @@ -5,54 +5,54 @@ import com.tangem.domain.tokens.model.Network import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -internal typealias ChangedCurrencies = Map> +internal typealias ChangedCurrencies = Map> internal class ChangedCurrenciesManager { val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) - fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + fun addCurrency(currency: ManagedCryptoCurrency.Token, network: Network) { + updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd) } - fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + fun removeCurrency(currency: ManagedCryptoCurrency.Token, network: Network) { + updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove) } - fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean { - return networkId in currenciesToAdd.value[currencyId].orEmpty() || - networkId in currenciesToRemove.value[currencyId].orEmpty() + fun containsCurrency(currency: ManagedCryptoCurrency.Token, network: Network): Boolean { + return network in currenciesToAdd.value[currency].orEmpty() || + network in currenciesToRemove.value[currency].orEmpty() } private fun updateChangedItems( - currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, + currency: ManagedCryptoCurrency.Token, + network: Network, removeFromIfPresent: MutableStateFlow, addToIfNotPresent: MutableStateFlow, ) { - val present = removeFromIfPresent.value[currencyId].orEmpty() + val present = removeFromIfPresent.value[currency].orEmpty() - if (networkId in present) { + if (network in present) { removeFromIfPresent.update { items -> items.toMutableMap().apply { - val ids = present - networkId + val ids = present - network if (ids.isEmpty()) { - remove(currencyId) + remove(currency) } else { - set(currencyId, ids) + set(currency, ids) } } } } else { addToIfNotPresent.update { items -> - val alreadyAdded = items[currencyId] ?: emptySet() - if (networkId in alreadyAdded) { + val alreadyAdded = items[currency] ?: emptySet() + if (network in alreadyAdded) { return@update items } - items + (currencyId to alreadyAdded + networkId) + items + (currency to alreadyAdded + network) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 62ab953402..92dc53736c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -59,8 +59,8 @@ internal class ManageTokensListManager @Inject constructor( scopeProvider = Provider { scope }, ) - val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd - val currenciesToRemove: StateFlow = changedCurrenciesManager.currenciesToRemove + val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd.asStateFlow() + val currenciesToRemove: StateFlow = changedCurrenciesManager.currenciesToRemove.asStateFlow() @OptIn(ExperimentalCoroutinesApi::class) val paginationStatus: Flow> = state @@ -148,32 +148,32 @@ internal class ManageTokensListManager @Inject constructor( } } - override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - changedCurrenciesManager.addCurrency(currencyId, networkId) + override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { + changedCurrenciesManager.addCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true) + sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true) } - override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { - changedCurrenciesManager.removeCurrency(currencyId, networkId) + override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { + changedCurrenciesManager.removeCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false) + sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) } override fun checkNeedToShowRemoveNetworkWarning( - currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, - ): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId) + currency: ManagedCryptoCurrency.Token, + network: Network, + ): Boolean = !changedCurrenciesManager.containsCurrency(currency, network) private fun sendSelectCurrencyAction( batchKey: Int, currencyId: ManagedCryptoCurrency.ID, - networkId: Network.ID, + network: Network, isSelected: Boolean, ) { val request = ManageTokensUpdateAction.AddCurrency( currencyId = currencyId, - networkId = networkId, + network = network, isSelected = isSelected, ) val action = BatchAction.UpdateBatches( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt index 9d73ea1e76..7b9ab64f3f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -6,11 +6,11 @@ import com.tangem.domain.wallets.models.UserWalletId internal interface ManageTokensUiActions { - fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) - fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) - fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean + fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 546761dcfb..3ca2075789 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -142,19 +142,19 @@ internal class ManageTokensUiManager( if (currency !is ManagedCryptoCurrency.Token) return@launch if (isSelected) { - actions.addCurrency(batchKey, currency.id, source.id) + actions.addCurrency(batchKey, currency, source.network) } else { - if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { + if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) { showRemoveNetworkWarning( currency = currency, network = source.network, isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, onConfirm = { - actions.removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency, source.network) }, ) } else { - actions.removeCurrency(batchKey, currency.id, source.id) + actions.removeCurrency(batchKey, currency, source.network) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt index 71d9885d89..6fdacc516a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -16,7 +16,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( NetworksUM.Expanded( networks = availableNetworks.map { it.toUiModel( - isSelected = it.id in addedIn, + isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, ) @@ -33,7 +33,7 @@ private fun SourceNetwork.toUiModel( onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, ): CurrencyNetworkUM { return CurrencyNetworkUM( - networkId = id, + network = network, name = network.name.uppercase(), iconResId = id.getIconRes(isColored = isSelected || !isEditable), isSelected = isSelected || !isEditable, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt index d8b0a83776..557b49d960 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt @@ -55,10 +55,10 @@ private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency. is NetworksUM.Collapsed -> networks is NetworksUM.Expanded -> networks.copy( networks = networks.networks.map { network -> - val isSelected = network.networkId in currency.addedIn + val isSelected = network.network in currency.addedIn network.copy( - iconResId = network.networkId.getIconRes(isSelected), + iconResId = network.network.id.getIconRes(isSelected), isSelected = isSelected, ) }.toImmutableList(), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt index 562f0db8a2..fbc93fce2c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt @@ -8,7 +8,7 @@ import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM { return copy( - iconResId = networkId.getIconRes(isSelected), + iconResId = network.id.getIconRes(isSelected), isSelected = isSelected, ) } From a67d482c3aa3e2292701957367fb457377bdc457 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 14:51:47 +0400 Subject: [PATCH 063/197] Updated on 2026-08-14 --- .../converter/UserWalletItemUMConverter.kt | 2 + .../tokens/GetWalletTotalBalanceUseCase.kt | 4 +- .../portfolio/impl/loader/PortfolioData.kt | 26 +++++ .../impl/loader/PortfolioDataLoader.kt | 105 ++++++++++++++++++ .../impl/ui/WalletSelectorBottomSheet.kt | 2 - .../ui/state/WalletSelectorBSContentUM.kt | 1 - 6 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index 704315fc82..163168be81 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -30,6 +30,7 @@ class UserWalletItemUMConverter( private val balance: TotalFiatBalance? = null, private val isLoading: Boolean = true, private val isBalanceHidden: Boolean = false, + private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None, ) : Converter { override fun convert(value: UserWallet): UserWalletItemUM { @@ -45,6 +46,7 @@ class UserWalletItemUMConverter( ), imageUrl = artworkUrl, isEnabled = !isLocked, + endIcon = endIcon, onClick = { onClick(value.walletId) }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 0f4d3477da..012dd2847a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -29,7 +29,7 @@ class GetWalletTotalBalanceUseCase( private val stakingRepository: StakingRepository, ) { - suspend operator fun invoke( + operator fun invoke( userTallestIds: Collection, ): LceFlow> { val flows = userTallestIds.distinct() @@ -54,7 +54,7 @@ class GetWalletTotalBalanceUseCase( } @OptIn(ExperimentalCoroutinesApi::class) - suspend operator fun invoke(userWalletId: UserWalletId): LceFlow { + operator fun invoke(userWalletId: UserWalletId): LceFlow { val currenciesStatuses = getStatuses(userWalletId) return currenciesStatuses.transformLatest { maybeStatuses -> diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt new file mode 100644 index 0000000000..8ac05a4e90 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt @@ -0,0 +1,26 @@ +package com.tangem.features.markets.portfolio.impl.loader + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Portfolio data. Combined data from all flows that required to setup portfolio + * + * @property walletsWithCurrencyStatuses wallets with crypto currency statuses + * @property appCurrency app currency + * @property isBalanceHidden flag that indicates if balance should be hidden + * @property walletsWithBalance wallets with total balance + * +[REDACTED_AUTHOR] + */ +internal data class PortfolioData( + val walletsWithCurrencyStatuses: Map>, + val appCurrency: AppCurrency, + val isBalanceHidden: Boolean, + val walletsWithBalance: Map>, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt new file mode 100644 index 0000000000..e2e08f4b70 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -0,0 +1,105 @@ +package com.tangem.features.markets.portfolio.impl.loader + +import arrow.core.getOrElse +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +/** + * Loader of portfolio data + * + * @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses + * @property getSelectedAppCurrencyUseCase use case for getting selected app currency + * @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings + * @property getWalletTotalBalanceUseCase use case for getting wallet total balance + * +[REDACTED_AUTHOR] + */ +internal class PortfolioDataLoader @Inject constructor( + private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, +) { + + /** Load data by [currencyRawId] */ + @OptIn(ExperimentalCoroutinesApi::class) + fun load(currencyRawId: String): Flow { + return combine( + flow = getAllWalletsCryptoCurrencyStatusesFlow(currencyRawId = currencyRawId), + flow2 = getSelectedAppCurrencyFlow(), + flow3 = getBalanceHidingSettingsFlow(), + ) { walletsWithCurrencyStatuses, appCurrency, isBalanceHidden -> + PortfolioData( + walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden, + walletsWithBalance = emptyMap(), + ) + } + // setup balances for wallets from walletsWithCurrencyStatuses + .flatMapLatest { portfolioData -> + getWalletsWithTotalBalanceFlow( + ids = portfolioData.walletsWithCurrencyStatuses.keys.map(UserWallet::walletId), + ) + .map { portfolioData.copy(walletsWithBalance = it) } + } + } + + private fun getAllWalletsCryptoCurrencyStatusesFlow( + currencyRawId: String, + ): Flow>> { + return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) + .distinctUntilChanged() + .map { walletsWithMaybeStatuses -> + walletsWithMaybeStatuses.mapValues { entry -> + entry.value.mapNotNull { it.getOrNull() } + } + } + } + + private fun getSelectedAppCurrencyFlow(): Flow { + return getSelectedAppCurrencyUseCase() + .map { + it.getOrElse { e -> + Timber.e("Failed to load app currency: $e") + AppCurrency.Default + } + } + .distinctUntilChanged() + } + + private fun getBalanceHidingSettingsFlow(): Flow { + return getBalanceHidingSettingsUseCase() + .map { it.isBalanceHidden } + .distinctUntilChanged() + } + + private fun getWalletsWithTotalBalanceFlow( + ids: List, + ): Flow>> { + return combine( + flows = ids + .map { userWalletId -> + getWalletTotalBalanceUseCase(userWalletId) + .map { userWalletId to it } + .distinctUntilChanged() + }, + transform = { it.toMap() }, + ) + .distinctUntilChanged() + .onEmpty { ids.associateWith { Lce.Loading(partialContent = null) } } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt index a9e190928f..22ba89a8f7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt @@ -109,7 +109,6 @@ private fun Preview() { ), ), onBack = {}, - onWalletSelected = {}, ), ), ) @@ -135,7 +134,6 @@ private fun PreviewContent() { ), ), onBack = {}, - onWalletSelected = {}, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt index ee727b3608..fddc12c25e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt @@ -6,6 +6,5 @@ import kotlinx.collections.immutable.ImmutableList internal data class WalletSelectorBSContentUM( val userWallets: ImmutableList, - val onWalletSelected: (UserWalletItemUM) -> Unit, val onBack: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file From 0cea2545a7c4ce53d8a62e9b6367ea1d5b76aa0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 13:09:30 +0500 Subject: [PATCH 064/197] Updated on 2026-08-14 --- .../NavigationButtonsBlock.kt | 31 ------- .../NavigationButtonsState.kt | 1 - .../preview/NavigationButtonsPreview.kt | 9 -- core/res/src/main/res/values-de/strings.xml | 42 ++++----- core/res/src/main/res/values-fr/strings.xml | 25 ++--- core/res/src/main/res/values-it/strings.xml | 12 --- core/res/src/main/res/values-ja/strings.xml | 60 ++++++------ core/res/src/main/res/values-ru/strings.xml | 11 ++- .../src/main/res/values-uk-rUA/strings.xml | 42 +++------ .../src/main/res/values-zh-rTW/strings.xml | 14 --- core/res/src/main/res/values/strings.xml | 8 +- .../ui/components/inputrow/InputRowDefault.kt | 16 ++-- .../impl/presentation/state/StakingUiState.kt | 3 +- ...StakingActionSelectionBottomSheetConfig.kt | 11 +++ .../ConfirmationStatePreviewData.kt | 5 +- .../state/stub/StakingClickIntentsStub.kt | 5 +- .../SetButtonsStateTransformer.kt | 61 +----------- .../SetConfirmationStateAssentTransformer.kt | 5 +- ...tConfirmationStateInProgressTransformer.kt | 6 +- .../SetConfirmationStateLoadingTransformer.kt | 3 +- .../SetInitialDataStateTransformer.kt | 3 +- ...howActionSelectorBottomSheetTransformer.kt | 30 ++++++ .../state/utils/StakingPendingActionUtils.kt | 27 ++++++ .../impl/presentation/ui/StakingScreen.kt | 3 + .../ui/StakingValidatorListContent.kt | 2 +- .../StakingActionSelectorBottomSheet.kt | 92 +++++++++++++++++++ .../viewmodel/StakingClickIntents.kt | 9 +- .../viewmodel/StakingViewModel.kt | 64 ++++++++----- 28 files changed, 310 insertions(+), 290 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index 92a791ad8a..58cc324958 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -43,8 +43,6 @@ fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifi PreviousButton(state?.prevButton) PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) } - - SecondaryButton(state?.secondaryButton) } } @@ -79,35 +77,6 @@ private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = } } -@Composable -private fun SecondaryButton(secondaryButton: NavigationButton?) { - val wrappedButton by rememberNavigationButton(secondaryButton) - AnimatedContent( - targetState = wrappedButton, - transitionSpec = { navigationButtonsTransition() }, - contentAlignment = Alignment.Center, - label = "Animate show secondary button", - modifier = Modifier.fillMaxWidth(), - ) { button -> - if (button != null && button.textReference != TextReference.EMPTY) { - val icon = button.iconRes?.let { TangemButtonIconPosition.End(iconResId = it) } - ?: TangemButtonIconPosition.None - - TangemButton( - text = button.textReference.resolveReference(), - enabled = button.isEnabled, - onClick = button.onClick, - icon = icon, - showProgress = button.showProgress, - colors = TangemButtonsDefaults.secondaryButtonColors, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), - ) - } else { - Spacer(modifier = Modifier.fillMaxWidth()) - } - } -} - @Composable private fun PreviousButton(prevButton: NavigationButton?) { AnimatedVisibility( diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index c125a871d1..e9a1fd8a83 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -10,7 +10,6 @@ sealed class NavigationButtonsState { data class Data( val primaryButton: NavigationButton, val prevButton: NavigationButton?, - val secondaryButton: NavigationButton?, val extraButtons: ImmutableList, val txUrl: String? = null, ) : NavigationButtonsState() diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 738598de82..b98e5de371 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -30,14 +30,6 @@ internal object NavigationButtonsPreview { ), ) - private val next = NavigationButton( - textReference = resourceReference(R.string.common_next), - isSecondary = false, - isIconVisible = false, - showProgress = false, - isEnabled = true, - onClick = {}, - ) private val prev = NavigationButton( textReference = TextReference.EMPTY, iconRes = R.drawable.ic_back_24, @@ -60,7 +52,6 @@ internal object NavigationButtonsPreview { val allButtons = NavigationButtonsState.Data( primaryButton = finished, prevButton = prev, - secondaryButton = next, extraButtons = extraButtons, txUrl = "https://tangem.com", ) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 81ebcec61e..54492f5825 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -104,7 +104,6 @@ Erkunden Transaktionsverlauf einsehen Explorer - Gebühr Netzwerkgebühren sind Gebühren, die Nutzer für die Verarbeitung und Bestätigung von Transaktionen zahlen. Die Höhe der Gebühren kann von der Überlastung des Netzes, der Größe der Transaktion und der Ausführungspriorität abhängen. %s Schnell Markt @@ -114,6 +113,10 @@ Zum Anbieter gehen Zum Token Importieren + + in %d Tag + in %d Tagen + Später Gesperrt Hauptnetz @@ -169,7 +172,9 @@ Vertragsadresse Vertragsadresse ist ungültig Bitte wähle das Netzwerk - Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %li + Dieses Token wurde bereits zur Liste hinzugefügt + Token existiert bereits + Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %d Benutzerdefinierte Ableitung(derivation) E. g. m/00\'/0000\'/0\'/0/0 Benutzerdefinierte Ableitung (Derivation) eingeben @@ -339,12 +344,14 @@ Um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen, füge diesen Token zu mindestens 1 Netzwerk hinzu Dieses Asset ist nicht verfügbar Zum Portfolio hinzufügen - Token hinzufügen + Hinzufügen Verfügbare Netzwerke Mein Portfolio Markt Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen + Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s Die Daten konnten nicht geladen werden... + Keine Daten Schnelle Aktionen Ergebnis Token unter 100k Marktkapitalisierung anzeigen @@ -370,7 +377,7 @@ Bewertung, basierend auf %d Bewertungen, basierend auf %d - Blockchain-Site + Webseite Kaufdruck Die Differenz zwischen Käufer- und Verkäufervolumen Umlaufmenge @@ -466,7 +473,7 @@ Seed-Phrase verwenden Ungültige Seed-Phrase. Bitte überprüfe die Wortreihenfolge. Ungültige Seed-Phrase. Bitte überprüfe die Rechtschreibung. - veralteter Standard + Veralteter Standard Um zu überprüfen, ob du deine Seed-Phrase richtig aufgeschrieben hast, gib bitte das 2., 7. und 11 Wort ein. Eine letzte Prüfung! Um den Sicherungsvorgang zu starten, füge bis zu zwei Sicherungskarten hinzu. @@ -572,18 +579,11 @@ Die Gebühr, die für die Nutzung jeder nicht ausgegebenen Transaktionsausgabe (UTXO) im Kaspa-Netzwerk erforderlich ist. Je mehr UTXOs du in einer Transaktion verwenden, desto höher ist die Gebühr. KAS per UTXO %1$s, %2$s - Adresse Ziel-Tag Adresse eingeben Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein - Ungültiges Tag. Es wird der Transaktion nicht hinzugefügt. Ungültiges Memo. Sie wird der Transaktion nicht hinzugefügt. - Tag Memo - inkl. Gebühr - Niedrig - Normal - Priorität Überprüfe deine Netzwerkverbindung Informationen zur Netzwerkgebühr nicht erreichbar Von @@ -598,7 +598,7 @@ Abdeckung der Netzgebühren Unzureichende Mittel für die Überweisung, da die Summe aus Gebühr und Überweisungsbetrag das bestehende Guthaben übersteigt Gesamtbetrag übersteigt den Saldo - Das Konto wird von der Blockchain gelöscht, wenn der Kontostand unter die Mindesteinlage fällt. Bitte belasse %s auf deinem Konto. + Ein Guthaben von mindestens %s ist erforderlich, um dein Konto in der Blockchain aktiv zu halten und Sicherheitsrisiken zu vermeiden. Dieser Betrag verbleibt auf deinem Guthaben und kann nicht abgehoben werden. Mindesteinlage Der Kommissionsbetrag ist %s mal der empfohlene Betrag. Stelle sicher, dass die benutzerdefinierten Einstellungen korrekt sind. Die individuelle Gebühr ist hoch @@ -632,21 +632,15 @@ Versende %s Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s Du sendest **%1$s** und %2$s - Senden %s - Gesamt - %1$s und %2$s werden gesendet - ≈ %1$s (inkl. Gebühr: %2$s) - %s wird gesendet Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. - Ungültige Adresse Transaktion gesendet Bereite das Scannen der Karte vor, die du einrichten möchtest. Entferne diese Wallet Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name Aktiv - Um deine Kryptos zu unstaken, klick hier. + Um deine Vermögenswerte freizugeben, tippe auf den Block oben Die Anzahl der zu stakenden Krypros muss mindesten %s betragen Nicht gestakte beanspruche Jährliche prozentuale Rendite @@ -659,12 +653,12 @@ Marktbewertung Metriken Mindestanforderungen - Keine Belohnungen zu beanspruchen. + Keine Belohnungen Belohnungen beanspruchen Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden. Belohnungszeitplan Dabei handelt es sich um einen Zeitplan, der festlegt, wann die Teilnehmer am Staking ihre Belohnungen erhalten. - Belohnungen, die du beanspruchen kannst: %s + Belohnungen %s Staking %s Entbindungsdauer Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. @@ -678,6 +672,7 @@ Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. + Bereit zum Abheben Erneut binden Erneut staken Belohnungen erneut staken @@ -698,6 +693,7 @@ Mehr staken Stake %s Staking beenden%s + Lösen der Bindungen Gelocktes unlocken Unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen @@ -862,7 +858,7 @@ Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s Die Gebühr %s kann nicht gedeckt werden Der zu erhaltende Betrag muss mindestens %s betragen - Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %@) + Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %s) Ausgewähltes Paar vorübergehend nicht verfügbar Service vorübergehend nicht verfügbar Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 581c5d1af7..188ec9511e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -101,7 +101,6 @@ Explorez Explorez l\'historique des transactions Explorateur - Commissions Les frais de réseau sont des charges que les utilisateurs paient pour traiter et confirmer les transactions. Le montant des frais peut être affecté par la congestion du réseau, la taille de la transaction et la priorité d\'exécution. %s Rapide Marché @@ -164,7 +163,7 @@ Adresse du contrat L\'adresse du contrat n\'est pas valide Veuillez sélectionner le réseau - Les décimales doivent être un entier valide, jusqu\'à %li + Les décimales doivent être un entier valide, jusqu\'à %d Dérivation personnalisée Par exemple m/00\'/0000\'/0\'/0/0 Entrez une dérivation personnalisée @@ -508,18 +507,11 @@ Les frais requis pour l\'utilisation de chaque sortie de transaction non dépensée (UTXO) dans le réseau Kaspa. Plus vous utilisez d’UTXO dans une transaction, plus les frais seront élevés. KAS pour UTXO %1$s, %2$s - Adresse ID de destination Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille - Tag invalide. Il ne sera pas ajouté à la transaction. Mémo invalide. Il ne sera pas ajouté à la transaction. - Tag Memo - Inclure les commissions - Bas - Normal - Priorité Vérifiez votre connexion réseau Informations sur les frais de réseau inaccessibles De @@ -534,7 +526,7 @@ Couverture des frais de réseau Fonds insuffisants pour le transfert, car le total des frais et du montant du transfert dépasse le solde existant Le total dépasse le solde - Le compte sera effacé de la blockchain si un solde descend en dessous du dépôt existentiel. Veuillez en laisser %s sur votre solde. + Un solde d\'au moins %s est requis pour conserver votre compte sur la blockchain afin d\'éviter les risques de sécurité. Ce montant restera sur votre solde et ne pourra pas être retiré. Dépôt existentiel Le montant de la commission est %s fois le montant recommandé. Assurez-vous que les paramètres personnalisés sont corrects. Les frais de douane sont élevés @@ -568,21 +560,16 @@ Envoyer %s Vous envoyez **%1$s** incluant des frais de réseau de %2$s Vous envoyez **%1$s** et %2$s - Envoi de %s - Total - Sera envoyé %1$s et %2$s - ≈ %1$s (incl. les commissions : %2$s) - Sera envoyé %s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. - Adresse incorrecte Transaction envoyée Scannez la carte que vous souhaitez configurer. Oublier le portefeuille Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Nom Actif - Afin d\'unstaker vos actifs, cliquez ici. + Afin d\'unstaker vos actifs, appuyez sur le bloc ci-dessus + Pourcentage de rendement annuel Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. APR Disponible @@ -604,6 +591,7 @@ Le temps imparti pour activer la participation au staking. Native staking Gagnez des récompenses de staking + Les récompenses cessent de s\'accumuler immédiatement après que vous ayez commencé le unstaking. Le processus de unstaking prend %s. Récompenses Stake verrouillé Staker plus @@ -761,7 +749,8 @@ Pour effectuer une transaction, vous devez déposer %1$s %2$s Impossible de couvrir %s frais Le montant à recevoir doit être d\'au moins %s - Cela peut se produire car le fournisseur n\'est actuellement pas en mesure d\'échanger la paire que vous avez sélectionnée. Veuillez patienter un instant et réessayer. (Code %@) + Cela peut se produire car le fournisseur n\'est actuellement pas en mesure d\'échanger la paire que vous avez sélectionnée. Veuillez patienter un instant et réessayer. (Code %s) + La paire sélectionnée est temporairement indisponible Service temporairement indisponible Le nombre de jetons à échanger ne doit pas dépasser %s Le montant à échanger doit être d\'au moins %s diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index e7caa7829e..900ef94aa5 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -8,7 +8,6 @@ Rimuovere Fatto Errore - Commissione Costi della rete OK Mantieni le modifiche @@ -35,21 +34,10 @@ Avvicina la carta Crea portafoglio Importo - Indirizzo L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio - Tag Memo - Includi commissione - Insufficiente - Normale - Prioritario Importo totale - Totale - Sarà inviato %1$s e %2$s - ≈ %1$s (inc. commissione: %2$s) - Sarà inviato %s La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo - Indirizzo non valido Tangem Twin WalletConnect L\'indirizzo è stato copiato con successo diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index f27b090b8a..025170ee41 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -39,7 +39,7 @@ 試行回数が多すぎます お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 バックアップ処理を開始する - 法定通貨カードまたは銀行口座から + 銀行カードまたは銀行口座から %d カード @@ -102,7 +102,6 @@ 移動する 取引履歴を調べる エクスプローラー - 手数料 ネットワーク手数料は、取引の処理と確認のためにユーザーが支払う料金です。手数料の額は、ネットワークの混雑さ、取引のサイズ、実行の優先度によって左右されます。 %s 速い マーケット @@ -112,6 +111,9 @@ プロバイダーへ移動 トークンへ移動 インポート + + %d日で + 後で ロックされています メインネットワーク @@ -167,7 +169,9 @@ コントラクトアドレス コントラクトアドレスが無効です ネットワークを選択してください - 小数は%liまでの有効な整数である必要があります + このトークンはすでにリストに追加されています + トークンはすでに存在します + 小数は%dまでの有効な整数である必要があります カスタム派生パス 例:m/00\'/0000\'/0\'/0/0 カスタム派生パスを入力 @@ -333,15 +337,17 @@ 賛成票を投じる ウォレットを選択 ウォレットは複数のネットワークをサポートしていません。 - このアセットの購入、交換、受け取りを開始するには、このトークンを少なくとも1つのネットワークに追加してください。 + このアセットの購入、交換、受け取りを開始するには、ポートフォリオに追加してください。 このアセットは利用できません ポートフォリオに追加 - トークンを追加 + 追加 利用可能なネットワーク 私のポートフォリオ マーケット 選択したネットワークのアドレスを生成するには、Tangemカードをスキャンする必要があります。 + このセクションのデータは、次のネットワークから取得されています: %s データを読み込めません… + データなし クイックアクション 結果 時価総額10万ドル以下のトークンを見る @@ -366,7 +372,7 @@ %d のレーティングに基づいて - ブロックチェーンサイト + ウェブサイト 買い圧力 買い手と売り手の取引量の差 循環供給量 @@ -447,7 +453,7 @@ あなたのシードフレーズ - %d 単語 + %d 単語 ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 シードフレーズを生成する @@ -498,7 +504,7 @@ カメラへのアクセスが拒否されました %3$sネットワーク上の%1$s ( %2$s ) このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。 - QRコードを表示するか、アドレスを共有します + QRコードを表示するか、アドレスを共有してください 参加する 紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。 紹介プログラムに関する情報を読み込めませんでした。エラー コード: %s 。しばらくしてからもう一度お試しください。 @@ -560,18 +566,11 @@ Kaspaネットワークで未使用の取引出力(UTXO)を使用するために必要な手数料です。取引で使用するUTXOが多ければ多いほど、手数料は高くなります。 UTXOあたりのKAS %1$s 、 %2$s - アドレス 宛先タグ アドレスを入力 アドレスはウォレットアドレスと同じです - 無効なタグです。取引には追加されません。 無効なメモです。取引には追加されません。 - タグ メモ - 手数料込み - 低い - 普通 - 優先 ネットワーク接続を確認してください ネットワーク手数料についての情報にアクセスできません より @@ -586,7 +585,7 @@ ネットワーク手数料のカバー 手数料と送金額の合計が残高を超えているため、送金に必要な資金が不足しています。 合計が残高を超えています - 残高が最低量を下回ると、当アカウントはブロックチェーンから消去されます。残高に%sを残しておいてください。 + セキュリティリスクを防ぐため、ブロックチェーン上にアカウントを維持するには、少なくとも%s の残高が必要です。この金額は残高に残り、引き出すことはできません。 アカウント維持に必要な最低残高 手数料額が推奨額の%s倍となっています。カスタム設定が正しいことを再度確認してください。 カスタム手数料が高くなっています @@ -620,21 +619,15 @@ %sを送金する **%1$s** を送金する (ネットワーク手数料%2$sを含む) **%1$s** と %2$s を送金しています。 - %sを送信しています - 合計 - %1$sと%2$sが送信されます - ≈ %1$s (%2$s: 手数料を含む) - %sが送信されます 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 - 無効なアドレス 取引が送信されました セットアップしたいカードをスキャンするために準備してください。 ウォレット削除 これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 名前 アクティブ - 資産のステーキングを解除するには、ここをクリックしてください。 + 資産のステーキングを解除するには、上のブロックをタップしてください ステーキング金額は %s 以上である必要があります ステーキング解除分を請求する 年率 @@ -647,25 +640,27 @@ 市場評価 指標 最低要件 - 請求できる報酬はありません + 報酬なし 請求中の報酬 - ステーキング報酬を受け取る方法。自動または手動で請求できます。 + ステーキング報酬の受け取り方法。\n自動であなたのアドレスに報酬が入金される方法と、手動で取引を生成して報酬を引き出す方法があります。 報酬スケジュール これは、ステーキングの参加者がいつ報酬を受け取るかを決定するスケジュールです。 - 受け取る報酬: %s + 報酬: %s ステーキング%s 解約完了までの期間 ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。 ウォームアップ期間 ステーキングへの参加を有効にするために割り当てられた時間。 + ロック中 移行 ネイティブステーキング ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 - ステーキングにより%1$sを獲得できます。ステーキング報酬は1時間ごとに受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎時間受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 - ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。 + ステーキング解除プロセスの開始後、報酬の獲得はすぐに停止します。ステーキング解除には%sかかります。 + 引き出し準備完了 再結束 再度ステーキングする 報酬をステーキングする @@ -686,8 +681,9 @@ もっとステーキングする %sをステーキングする %sのステーキング解除 + ステーキング解約中 ステーキング解除はロックされています - スタックされていない + ステーキングされていない 資産を請求するために、unstakedを確認してください ステーキング解除 バリデーター @@ -842,7 +838,7 @@ ネットワークには最低残高が必要です スワップは、%s の取引完了後に利用可能となります。 アクティブな取引があります - スワップの承認は現在進行中で、まもなく完了する予定です。 + スワップ承認は現在進行中で、まもなく完了する予定です。 承認が進行中 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 あなたのリストには、交換可能な %s トークンがありません。 @@ -850,7 +846,7 @@ 取引を行うには、 %1$s %2$sを入金する必要があります。 %s 手数料を支払えません 受け取る金額は、 %s 以上である必要があります。 - これは、選択したペアがプロバイダーでは現在交換できないために発生する可能性があります。しばらく待ってからもう一度お試しください。(コード%@ ) + これは、選択したペアがプロバイダーでは現在交換できないために発生する可能性があります。しばらく待ってからもう一度お試しください。(コード%s ) 選択されたペアは一時的に利用できません サービスは一時的に利用できません スワップするトークンの量は %s を超えないでください @@ -866,6 +862,8 @@ このカードには%sの署名のみが残っています。資金をすべて引き出す必要があります。 署名数が少ないです 異なるネットワーク上のトークンは、異なるアドレスを持つ場合があります。資金を送金する際には、アドレスがネットワークと一致していることを再確認してください。 + MATICはPOLに移行中です。ただし、期限は設定されておらず、MATICはまだ廃止されていません。MATICトークンを引き続き安全に使用することも、取引所でPOLに交換することもできます。 + MATICからPOLへの移行 %d ネットワークのアドレスを取得するために、カードを利用してください diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0a7ca9ff35..50e75be7a4 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -71,6 +71,7 @@ Доступ запрещен Все Разрешить + Аналитика Применить Одобрение Подтвердить @@ -177,7 +178,7 @@ Адрес контракта Адрес контракта некорректен Пожалуйста, выберите сеть - Десятичное число должно быть действительным целым числом, до %li + Десятичное число должно быть действительным целым числом, до %d Своя деривация Например m/00\'/0000\'/0\'/0/0 Введите свою деривацию @@ -353,6 +354,7 @@ Рынок Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту Tangem кошелька Данные раздела получены из следующих сетей: %s + Нет данных Быстрые действия Результат Токены с капитализацией меньше 100к @@ -647,7 +649,7 @@ Для завершения стейкинга нажмите на блок выше Сумма для стейкинга должна быть не менее %s Забрать средства - Годовая процентная ставка + Процентная ставка Годовой процентный доход, который вы можете получить от участия в стейкинге. APR Доступно @@ -668,6 +670,7 @@ Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. Период прогрева Время, необходимое для начала процесса стейкинга и активации процесса начисления наград + Заблокировано Переместить Нативный стейкинг Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый день. @@ -696,7 +699,7 @@ Застейкать %s Вывести %s Разблокировать - Выведено из стейкинга + Вывод из стейкинга Проверьте процесс завершения стейкинга, чтобы вывести свои средства. Завершение стейкинга Валидатор @@ -860,7 +863,7 @@ Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s Невозможно покрыть комиссию %s Сумма получения не может быть менее %s - Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %@) + Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s) Выбранная пара временно недоступна Cервис временно недоступен Сумма для обмена должна быть не более %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index d0ff9d566d..767c26f4fd 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -78,8 +78,8 @@ Увага Баланс: %s Баланс - біометрична автентифікація - біометрією + біометричну автентифікацію + біометрії Купити Перейдіть до %1$s Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності @@ -107,7 +107,6 @@ Оглядач Переглянути історію транзакцій Оглядач - Комісія Мережеві комісії – це збори, які користувачі сплачують за обробку та підтвердження транзакцій. На розмір комісії може впливати перевантаження мережі, розмір транзакції та пріоритет виконання. %s Швидко За ринком @@ -152,7 +151,7 @@ Застейкати Стейкінг Почати - Надіслати + Продовжити Успіх Підтримка Обмін @@ -172,7 +171,7 @@ Адреса контракту Адреса контракту недійсна Будь ласка, оберіть мережу - Десяткове число повинно бути дійсним цілим числом, до %li + Десяткове число повинно бути дійсним цілим числом, до %d Власна деривація Наприклад, m/00\'/0000\'/0\'/0/0 Введіть власну деривацію @@ -377,7 +376,7 @@ На основі %d оцінок На основі %d оцінок - Блокчейн сайт + Веб-сайт Давлення покупця Різниця між обсягом покупців та обсягом продавців Циркуляційний запас @@ -473,7 +472,7 @@ Використовувати seed-фразу Невірна seed-фраза. Будь ласка, перевірте порядок слів. Невірна seed-фраза. Будь ласка, перевірте правопис. - Застарілий + Застаріло Щоб перевірити чи правильно ви записали seed-фразу, введіть 2-е, 7-е та 11-те слова Отже, давайте перевіримо Щоб почати процес резервного копіювання, додайте одну або дві резервні картки. @@ -530,7 +529,7 @@ за %d гаманців за %d гаманців - Отримайте ^^%1$s^^ на вашу адресу в мережі %2$s%3$s ^^через 30 днів^^ за кожен гаманець, який придбає ваш друг + Отримаєте ^^%1$s^^ на вашу адресу в мережі %2$s %3$s ^^через 30 днів^^ за кожен гаманець, який придбає ваш друг Ви Отримає при купівлі гаманця на сайті tangem.com @@ -583,18 +582,11 @@ Комісія, необхідна за використання кожної невитраченої транзакції (UTXO) у мережі Kaspa. Чим більше UTXO ви використовуєте в транзакції, тим вищою буде комісія. KAS за UTXO %1$s, %2$s - Адреса Тег призначення Введіть адресу Адреса збігається з адресою гаманця - Недопустимий Tag. Він не буде доданий у транзакцію. Недопустимий Memo. Він не буде доданий до транзакції. - Tag Memo - Включаючи комісію - Низька - Нормальна - Пріоритетна Перевірте підключення до мережі Інформація щодо комісії в мережі недоступна Із @@ -609,7 +601,7 @@ Покриття мережевої комісії Недостатньо коштів для здійснення переказу, оскільки загальна сума комісії та переказу перевищує наявний баланс Сума перевищує баланс - Рахунок буде видалено з блокчейну, якщо баланс стане нижчим за екзистенційний депозит. Будь ласка, залиште %s на своєму балансі. + Для збереження вашого акаунту у блокчейні та захисту від можливих ризиків необхідний баланс не менше %s. Ця сума залишиться на вашому рахунку та не може бути знята. Екзистенційний депозит Сума комісії в %s разів перевищує рекомендовану. Переконайтеся, що користувацькі налаштування вірні. Встановлена комісія завелика @@ -643,23 +635,18 @@ Надіслати %s Ви надсилаєте **%1$s**, включно з комісію мережі %2$s Ви надсилаєте **%1$s** і %2$s - Надсилання %s - Всього - %1$s та %2$s буде надіслано - ≈ %1$s (вкл. комісію: %2$s ) - %s буде надіслано Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. - Недійсна адреса Трансакцію надіслано - Підготуйтеся до сканування картку, яку потрібно налаштувати. + Підготуйте до сканування картку, яку потрібно налаштувати. Забути гаманець Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. Ім\'я Активний - Щоб вивести активи зі стейкінгу, натисніть тут. + Для завершення стейкінгу натисніть на блок вище Сума для стейкінгу має бути не менше %s Зняти кошти + Процентна ставка Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. APR Доступно @@ -843,8 +830,6 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця - Затвердження обміну триває і незабаром буде завершено - Затвердження в процесі Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. @@ -862,13 +847,16 @@ Для роботи з мережею вимагається депозит Обмін буде доступний після завершення %s транзакції У вас є активна транзакція + Затвердження обміну триває і незабаром буде завершено + Затвердження в процесі Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s. У вашому списку немає доступних монет для обміну %s Немає доступних токенів для обміну Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s Неможливо покрити комісію %s Сума отримання не може бути меншою за %s - Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %@) + Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %s) + Обрана пара тимчасово недоступна Сервіс тимчасово недоступний Сума до обміну не повинна перевищувати %s Сума для обміну має бути не менше %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 3c2329daab..b860623796 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -57,7 +57,6 @@ 允許 啟用 錯誤 - 費用 導入 網路費 @@ -251,24 +250,11 @@ 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 準備好您的卡! 數量 - 地址 地址與錢包地址相同 - 標籤無效。它不會被添加到交易中 Memo無效。 它不會被添加到交易中 - Tag Memo - 包含費用 - - 正常 - 優先 最大值 - 發送 %s - 總計 - %1$s 和 %2$s 將被發送 - ≈ %1$s (費用包含: %2$s) - %s 將被發送 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 - 無效地址 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 創新式的硬體錢包 最多 3張實體卡片 到一個錢包 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index fce9968300..314a9a07d4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -70,6 +70,7 @@ Access denied All Allow + Analytics Apply Approval Approve @@ -82,6 +83,7 @@ Go to %1$s You have not given access to your camera, please adjust your privacy settings Cancel + Choose action Claim rewards Close Continue @@ -174,7 +176,7 @@ Please select the network This token has already been added to the list Token already exists - Decimal must be a valid integer, up to %li + Decimal must be a valid integer, up to %d Custom derivation E. g. m/00\'/0000\'/0\'/0/0 Enter custom derivation @@ -351,6 +353,7 @@ To generate addresses for selected networks, you must scan your Tangem Wallet card This section’s data is sourced from the following networks: %s Unable to load the data… + No data Quick actions Result See tokens under 100k market cap @@ -659,6 +662,7 @@ The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period The allocated time for activating participation in staking. + Locked Migrate Native staking Staking allows you to earn %1$s. Your staking rewards arrive every day. @@ -854,7 +858,7 @@ To make a transaction you need to deposit some %1$s %2$s Unable to cover %s fee The amount to receive must be at least %s - This may occur because the provider is currently unable to exchange your selected pair. Please wait a moment and try again. (Code %@) + This may occur because the provider is currently unable to exchange your selected pair. Please wait a moment and try again. (Code %s) Selected pair temporarily unavailable Service temporarily unavailable The amount of tokens to be swapped must not exceed %s diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 1eff068d9f..3ef8fefbc8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -43,9 +43,9 @@ import com.tangem.core.ui.res.TangemThemePreview */ @Composable fun InputRowDefault( - title: TextReference, text: TextReference, modifier: Modifier = Modifier, + title: TextReference? = null, titleColor: Color = TangemTheme.colors.text.secondary, textColor: Color = TangemTheme.colors.text.primary1, iconRes: Int? = null, @@ -66,16 +66,18 @@ fun InputRowDefault( modifier = Modifier .weight(1f), ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = titleColor, - ) + title?.let { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = titleColor, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8), + ) + } Text( text = text.resolveReference(), style = TangemTheme.typography.body2, color = textColor, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } iconRes?.let { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 0e08d89b1c..55e576c90f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -83,11 +83,10 @@ internal sealed class StakingStates { val innerState: InnerConfirmationStakingState, val feeState: FeeState, val validatorState: ValidatorState, - val pendingActions: ImmutableList, + val pendingAction: PendingAction?, val notifications: ImmutableList, val footerText: String, val transactionDoneState: TransactionDoneState, - val pendingActionInProgress: PendingAction? = null, val isApprovalNeeded: Boolean, ) : ConfirmationState() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt new file mode 100644 index 0000000000..873ba40422 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt @@ -0,0 +1,11 @@ +package com.tangem.features.staking.impl.presentation.state.bottomsheet + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.staking.model.stakekit.PendingAction + +internal data class StakingActionSelectionBottomSheetConfig( + val title: TextReference, + val actions: List, + val onActionSelect: (PendingAction) -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index f8a64a3695..ef4234d37c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -9,9 +9,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingNotification -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.ValidatorState import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -90,7 +87,7 @@ internal object ConfirmationStatePreviewData { ), ), transactionDoneState = TransactionDoneState.Empty, - pendingActions = persistentListOf(), + pendingAction = null, isApprovalNeeded = false, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 7220af1f41..8876bdabdb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -6,16 +6,15 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents -import kotlinx.collections.immutable.ImmutableList @Suppress("TooManyFunctions") internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList) {} + override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) {} - override fun onActionClick(pendingAction: PendingAction?) {} + override fun onActionClick() {} override fun onPrevClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 3f8b253474..6bd2c902fa 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -5,10 +5,9 @@ import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -22,7 +21,6 @@ internal class SetButtonsStateTransformer : Transformer { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), prevButton = getPrevButton(prevState), - secondaryButton = getSecondaryButton(prevState), extraButtons = getExtraButtons(prevState), txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, ) @@ -37,48 +35,22 @@ internal class SetButtonsStateTransformer : Transformer { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data val innerConfirmState = confirmState?.innerState - val isPrimaryInProgress = - confirmState?.pendingActions?.getPrimaryAction() == confirmState?.pendingActionInProgress val isConfirmation = prevState.currentStep == StakingStep.Confirmation val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED val isIconVisible = isConfirmation && !isCompleted - val isShowProgress = isInProgress && isPrimaryInProgress return NavigationButton( textReference = prevState.getButtonText(), iconRes = R.drawable.ic_tangem_24, isSecondary = false, isIconVisible = isIconVisible, - showProgress = isShowProgress, + showProgress = isInProgress, isEnabled = prevState.isButtonEnabled(), onClick = { prevState.onPrimaryClick() }, ) } - private fun getSecondaryButton(prevState: StakingUiState): NavigationButton? { - val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - val innerConfirmState = confirmState?.innerState - - val isConfirmation = prevState.currentStep == StakingStep.Confirmation - val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS - val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED - - return confirmState?.pendingActions?.getSecondaryAction()?.let { pendingAction -> - val isSecondaryInProgress = pendingAction == confirmState.pendingActionInProgress - val isShowProgress = isInProgress && isSecondaryInProgress - NavigationButton( - textReference = getPendingActionTitle(pendingAction.type), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = isShowProgress, - isEnabled = prevState.isButtonEnabled(), - onClick = { prevState.clickIntents.onActionClick(pendingAction) }, - ).takeIf { isConfirmation && !isCompleted } - } - } - private fun getPrevButton(prevState: StakingUiState): NavigationButton? { return NavigationButton( textReference = TextReference.EMPTY, @@ -114,10 +86,6 @@ internal class SetButtonsStateTransformer : Transformer { ) } - private fun List.getPrimaryAction(): PendingAction? = getOrNull(0) - - private fun List.getSecondaryAction(): PendingAction? = getOrNull(1) - private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) { StakingStep.RewardsValidators -> false else -> true @@ -158,7 +126,7 @@ internal class SetButtonsStateTransformer : Transformer { StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake) StakingActionCommonType.PENDING_OTHER, StakingActionCommonType.PENDING_REWARDS, - -> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type) + -> confirmationState.pendingAction?.type.getPendingActionTitle() } } } else { @@ -192,7 +160,7 @@ internal class SetButtonsStateTransformer : Transformer { if (isEnterAction && isApproveNeeded) { clickIntents.showApprovalBottomSheet() } else { - clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull()) + clickIntents.onActionClick() } } } else { @@ -219,25 +187,4 @@ internal class SetButtonsStateTransformer : Transformer { StakingStep.Validators -> true } } - - @Suppress("CyclomaticComplexMethod") - private fun getPendingActionTitle(type: StakingActionType?): TextReference = when (type) { - StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards) - StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards) - StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw) - StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake) - StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked) - StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked) - StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked) - StakingActionType.VOTE -> resourceReference(R.string.staking_vote) - StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke) - StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked) - StakingActionType.REVOTE -> resourceReference(R.string.staking_revote) - StakingActionType.REBOND -> resourceReference(R.string.staking_rebond) - StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate) - StakingActionType.STAKE -> resourceReference(R.string.common_stake) - StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake) - StakingActionType.UNKNOWN -> TextReference.EMPTY - null -> TextReference.EMPTY - } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 6a214fde9a..ee19ad787a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -10,13 +10,12 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, - private val pendingActionList: ImmutableList, + private val action: PendingAction?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -38,7 +37,7 @@ internal class SetConfirmationStateAssentTransformer( isFeeApproximate = false, ), validatorState = validatorState.copySealed(isClickable = true), - pendingActions = pendingActionList, + pendingAction = action, isPrimaryButtonEnabled = true, isApprovalNeeded = false, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt index 817ff94491..e17aad84b4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -1,14 +1,11 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer -internal class SetConfirmationStateInProgressTransformer( - private val pendingAction: PendingAction?, -) : Transformer { +internal class SetConfirmationStateInProgressTransformer : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( @@ -22,7 +19,6 @@ internal class SetConfirmationStateInProgressTransformer( isPrimaryButtonEnabled = false, innerState = InnerConfirmationStakingState.IN_PROGRESS, validatorState = validatorState.copySealed(isClickable = false), - pendingActionInProgress = pendingAction, ) } else { this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 07ba1f9fce..9e35b20442 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -32,8 +32,7 @@ internal class SetConfirmationStateLoadingTransformer( notifications = getNotifications(prevState), footerText = "", transactionDoneState = TransactionDoneState.Empty, - pendingActions = persistentListOf(), - pendingActionInProgress = null, + pendingAction = null, isApprovalNeeded = false, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 6402fc548e..19f1c44544 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -212,8 +212,7 @@ internal class SetInitialDataStateTransformer( notifications = persistentListOf(), footerText = "", transactionDoneState = TransactionDoneState.Empty, - pendingActions = persistentListOf(), - pendingActionInProgress = null, + pendingAction = null, isApprovalNeeded = isApprovalNeeded, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt new file mode 100644 index 0000000000..2c927652c7 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class ShowActionSelectorBottomSheetTransformer( + private val pendingActions: ImmutableList, + private val onActionSelect: (PendingAction) -> Unit, + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + content = StakingActionSelectionBottomSheetConfig( + title = resourceReference(R.string.common_choose_action), + actions = pendingActions, + onActionSelect = onActionSelect, + ), + onDismissRequest = onDismiss, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt new file mode 100644 index 0000000000..0116e0d6d5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -0,0 +1,27 @@ +package com.tangem.features.staking.impl.presentation.state.utils + +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.action.StakingActionType + +@Suppress("CyclomaticComplexMethod") +internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) { + StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards) + StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards) + StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw) + StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake) + StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked) + StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked) + StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked) + StakingActionType.VOTE -> resourceReference(R.string.staking_vote) + StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke) + StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked) + StakingActionType.REVOTE -> resourceReference(R.string.staking_revote) + StakingActionType.REBOND -> resourceReference(R.string.staking_rebond) + StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate) + StakingActionType.STAKE -> resourceReference(R.string.common_stake) + StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake) + StakingActionType.UNKNOWN -> TextReference.EMPTY + null -> TextReference.EMPTY +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 2bbb05b0c2..9fad6829f4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -24,7 +24,9 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig +import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingActionSelectorBottomSheet import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first @@ -74,6 +76,7 @@ fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { when (bottomSheetConfig.content) { is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig) is GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheet(bottomSheetConfig) + is StakingActionSelectionBottomSheetConfig -> StakingActionSelectorBottomSheet(bottomSheetConfig) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index e6ed596800..57ad4dd680 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -58,7 +58,7 @@ internal fun StakingValidatorListContent( InputRowImageSelector( subtitle = stringReference(item.name), caption = combinedReference( - resourceReference(R.string.staking_details_apr), + resourceReference(R.string.staking_details_annual_percentage_rate), annotatedReference { appendSpace() appendColored( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt new file mode 100644 index 0000000000..8f4434e496 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt @@ -0,0 +1,92 @@ +package com.tangem.features.staking.impl.presentation.ui.bottomsheet + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig +import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun StakingActionSelectorBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + title = { content -> + TangemBottomSheetTitle(title = content.title) + }, + containerColor = TangemTheme.colors.background.tertiary, + ) { content -> + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing32, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + content.actions.forEachIndexed { index, action -> + InputRowDefault( + text = action.type.getPendingActionTitle(), + textColor = TangemTheme.colors.text.primary1, + showDivider = index != content.actions.lastIndex, + modifier = Modifier + .fillMaxWidth() + .clickable { + content.onActionSelect(action) + }, + ) + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_StakingActionSelectorBottomSheet() { + TangemThemePreview { + StakingActionSelectorBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = StakingActionSelectionBottomSheetConfig( + title = resourceReference(R.string.common_select_action), + actions = persistentListOf( + PendingAction( + type = StakingActionType.CLAIM_REWARDS, + passthrough = "", + args = null, + ), + PendingAction( + type = StakingActionType.RESTAKE_REWARDS, + passthrough = "", + args = null, + ), + ), + onActionSelect = {}, + ), + ), + ) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index fee1e2115d..ad7b9ff1c6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -6,19 +6,14 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick( - actionType: StakingActionCommonType? = null, - pendingActions: ImmutableList = persistentListOf(), - ) + fun onNextClick(actionType: StakingActionCommonType? = null, pendingAction: PendingAction? = null) - fun onActionClick(pendingAction: PendingAction?) + fun onActionClick() fun onPrevClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 8dea58c5cd..523380111a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -63,7 +63,6 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -157,25 +156,25 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList) { + override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) { if (actionType != null) { stateController.update { it.copy(actionType = actionType) } } stakingStateRouter.onNextClick() if (isAssentState()) { - getFee(pendingActions) + getFee(pendingAction) } } - override fun onActionClick(pendingAction: PendingAction?) { - handleOnNextConfirmationClick(pendingAction) + override fun onActionClick() { + handleOnNextConfirmationClick() stakingStateRouter.onNextClick() } - private fun handleOnNextConfirmationClick(pendingAction: PendingAction?) { + private fun handleOnNextConfirmationClick() { if (isAssentState()) { viewModelScope.launch { - stateController.update(SetConfirmationStateInProgressTransformer(pendingAction)) + stateController.update(SetConfirmationStateInProgressTransformer()) val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") @@ -186,6 +185,7 @@ internal class StakingViewModel @Inject constructor( val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address") + val pendingAction = confirmationState.pendingAction val stakingTransactions = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -230,13 +230,13 @@ internal class StakingViewModel @Inject constructor( sendStakingTransaction( fullTransactionsData = fullTransactionsData, fee = fee, - pendingActionList = confirmationState.pendingActions, + pendingAction = confirmationState.pendingAction, ) } } } - private fun getFee(pendingActions: ImmutableList) { + private fun getFee(pendingAction: PendingAction?) { viewModelScope.launch { stateController.update( SetConfirmationStateLoadingTransformer( @@ -270,7 +270,7 @@ internal class StakingViewModel @Inject constructor( ) } else { estimateGas( - pendingActions = pendingActions, + pendingAction = pendingAction, amount = amount, sourceAddress = sourceAddress, validatorAddress = validatorAddress, @@ -278,7 +278,7 @@ internal class StakingViewModel @Inject constructor( } } else { estimateGas( - pendingActions = pendingActions, + pendingAction = pendingAction, amount = amount, sourceAddress = sourceAddress, validatorAddress = validatorAddress, @@ -288,12 +288,11 @@ internal class StakingViewModel @Inject constructor( } private suspend fun estimateGas( - pendingActions: ImmutableList, + pendingAction: PendingAction?, amount: BigDecimal, sourceAddress: String, validatorAddress: String, ) { - val pendingAction = pendingActions.firstOrNull() val gasEstimate = estimateGasUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, @@ -323,7 +322,7 @@ internal class StakingViewModel @Inject constructor( decimals = gasEstimate.token.decimals, ), ), - pendingActionList = pendingActions, + action = pendingAction, ), ) } @@ -397,14 +396,29 @@ internal class StakingViewModel @Inject constructor( } override fun onActiveStake(activeStake: BalanceState) { - val actionType = if (activeStake.pendingActions.isEmpty()) { - StakingActionCommonType.EXIT + if (activeStake.pendingActions.size > 1) { + stateController.update( + ShowActionSelectorBottomSheetTransformer( + pendingActions = activeStake.pendingActions, + onActionSelect = { + onNextClick(actionType = StakingActionCommonType.PENDING_OTHER, pendingAction = it) + stateController.update(DismissBottomSheetStateTransformer()) + }, + onDismiss = { + stateController.update(DismissBottomSheetStateTransformer()) + }, + ), + ) } else { - StakingActionCommonType.PENDING_OTHER + val actionType = if (activeStake.pendingActions.isEmpty()) { + StakingActionCommonType.EXIT + } else { + StakingActionCommonType.PENDING_OTHER + } + stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) + onNextClick(actionType, activeStake.pendingActions.firstOrNull()) } - stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - onNextClick(actionType, activeStake.pendingActions) } override fun showApprovalBottomSheet() { @@ -479,13 +493,13 @@ internal class StakingViewModel @Inject constructor( ifRight = { stateController.update(SetApprovalInProgressTransformer) stateController.update(DismissBottomSheetStateTransformer()) - awaitForAllowance(confirmationState.pendingActions) + awaitForAllowance(confirmationState.pendingAction) }, ) }.saveIn(approvalJobHolder) } - private fun awaitForAllowance(pendingActions: ImmutableList) { + private fun awaitForAllowance(pendingAction: PendingAction?) { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( scope = viewModelScope, @@ -504,7 +518,7 @@ internal class StakingViewModel @Inject constructor( val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") if (allowance >= amount) { - getFee(pendingActions) + getFee(pendingAction) allowanceTaskScheduler.cancelTask() } }, @@ -643,7 +657,7 @@ internal class StakingViewModel @Inject constructor( private suspend fun sendStakingTransaction( fullTransactionsData: List, fee: Fee, - pendingActionList: ImmutableList, + pendingAction: PendingAction?, ) { sendMultipleTransactionUseCase( txsData = fullTransactionsData.map { it.tangemTransaction }, @@ -657,7 +671,7 @@ internal class StakingViewModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = fee, - pendingActionList = pendingActionList, + action = pendingAction, ), ) stakingEventFactory.createSendTransactionErrorAlert(error) From 473a936863ef6a87ef7f8a99debdd16fddc5cbad Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 15:44:47 +0400 Subject: [PATCH 065/197] Updated on 2026-08-14 --- .../core/ui/components/rows/BlockchainRow.kt | 3 + .../components/rows/model/BlockchainRowUM.kt | 1 + .../managetokens/ui/ManageTokensScreen.kt | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 113 ++++++++++++++++++ .../impl/model/BlockchainRowUMConverter.kt | 38 ++++++ .../impl/model/PortfolioBSVisibilityModel.kt | 14 +++ .../impl/model/SelectNetworkUMConverter.kt | 32 +++++ .../impl/ui/AddToPortfolioBottomSheet.kt | 11 +- .../PreviewAddToPortfolioBSContentProvider.kt | 3 +- .../ui/state/AddToPortfolioBSContentUM.kt | 1 - .../com/tangem/lib/crypto/BlockchainUtils.kt | 37 +++++- 11 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index 035be68d2e..70150328ce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -137,6 +137,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid private class BlockchainRowParameterProvider : CollectionPreviewParameterProvider( collection = listOf( BlockchainRowUM( + id = "0", name = "BNB BEACON CHAIN", type = "BEP20", iconResId = R.drawable.img_bsc_22, @@ -144,6 +145,7 @@ private class BlockchainRowParameterProvider : CollectionPreviewParameterProvide isSelected = true, ), BlockchainRowUM( + id = "1", name = "1234567890111213141516171819", type = "BEP20", iconResId = R.drawable.ic_bsc_16, @@ -151,6 +153,7 @@ private class BlockchainRowParameterProvider : CollectionPreviewParameterProvide isSelected = false, ), BlockchainRowUM( + id = "2", name = "BNB BEACON CHAIN", type = "1234567890111213141516171819", iconResId = R.drawable.ic_bsc_16, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt index f8e756a9ae..700b4bc377 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable @Immutable data class BlockchainRowUM( + val id: String, val name: String, val type: String, val iconResId: Int, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index c8771d1fa6..430896ed3a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -348,6 +348,7 @@ private fun NetworksList( modifier = Modifier.padding(end = TangemTheme.dimens.spacing8), model = with(network) { BlockchainRowUM( + id = id, name = name, type = type, iconResId = iconResId, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt new file mode 100644 index 0000000000..9983cd1bb2 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -0,0 +1,113 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM +import kotlinx.collections.immutable.toImmutableList + +/** + * Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig] + * + * @property token token params + * @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed + * @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed + * @property onNetworkSwitchClick callback is invoked when network switch is clicked + * @property onWalletSelect callback is invoked when wallet is selected + * @property onContinueClick callback is invoked when continue button is clicked + * +[REDACTED_AUTHOR] + */ +internal class AddToPortfolioBSContentUMFactory( + private val token: TokenMarketParams, + private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, + private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, + private val onNetworkSwitchClick: (String, Boolean) -> Unit, + private val onWalletSelect: (UserWalletId) -> Unit, + private val onContinueClick: () -> Unit, +) { + + /** + * Create [TangemBottomSheetConfig] + * + * @param portfolioData portfolio data + * @param portfolioBSVisibilityModel portfolio bottom sheet visibility model + * @param selectedWallet selected wallet + * @param networksWithToggle networks with toggle + * @param isUserChangedNetworks flag indicates if user changed networks + */ + fun create( + portfolioData: PortfolioData, + portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + selectedWallet: UserWallet, + networksWithToggle: Map, + isUserChangedNetworks: Boolean, + ): TangemBottomSheetConfig { + return TangemBottomSheetConfig( + isShow = portfolioBSVisibilityModel.addToPortfolioBSVisibility, + onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, + content = AddToPortfolioBSContentUM( + selectedWallet = selectedWallet.toSelectedUserWalletItemUM(), + selectNetworkUM = SelectNetworkUMConverter( + networksWithToggle = networksWithToggle, + onNetworkSwitchClick = onNetworkSwitchClick, + ).convert(value = token), + isScanCardNotificationVisible = false, // TODO [REDACTED_JIRA] + continueButtonEnabled = isUserChangedNetworks, + onContinueButtonClick = onContinueClick, + walletSelectorConfig = crateWalletSelectorBSConfig( + isShow = portfolioBSVisibilityModel.walletSelectorBSVisibility, + portfolioData = portfolioData, + selectedWalletId = selectedWallet.walletId, + ), + ), + ) + } + + private fun UserWallet.toSelectedUserWalletItemUM(): UserWalletItemUM { + return UserWalletItemUMConverter( + onClick = { onWalletSelectorVisibilityChange(true) }, + endIcon = UserWalletItemUM.EndIcon.Arrow, + ).convert(value = this) + } + + private fun crateWalletSelectorBSConfig( + isShow: Boolean, + portfolioData: PortfolioData, + selectedWalletId: UserWalletId, + ): TangemBottomSheetConfig { + return TangemBottomSheetConfig( + isShow = isShow, + onDismissRequest = { onWalletSelectorVisibilityChange(false) }, + content = WalletSelectorBSContentUM( + userWallets = portfolioData.walletsWithCurrencyStatuses + .filterKeys(UserWallet::isMultiCurrency) + .map { it.key } + .map { userWallet -> + val balance = portfolioData.walletsWithBalance[userWallet.walletId] + + UserWalletItemUMConverter( + onClick = onWalletSelect, + appCurrency = portfolioData.appCurrency, + balance = balance?.getOrNull(), + isLoading = balance?.isLoading() == true, + isBalanceHidden = portfolioData.isBalanceHidden, + endIcon = if (userWallet.walletId == selectedWalletId) { + UserWalletItemUM.EndIcon.Checkmark + } else { + UserWalletItemUM.EndIcon.None + }, + ).convert(userWallet) + } + .toImmutableList(), + onBack = { onWalletSelectorVisibilityChange(false) }, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt new file mode 100644 index 0000000000..b80d71c2d1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt @@ -0,0 +1,38 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter + +/** + * Converter from [TokenMarketInfo.Network] to [BlockchainRowUM] + * +[REDACTED_AUTHOR] + */ +internal object BlockchainRowUMConverter : Converter, BlockchainRowUM> { + + override fun convert(value: Pair): BlockchainRowUM { + val (network, isSelected) = value + + val blockchainInfo = BlockchainUtils.getNetworkInfo(networkId = network.networkId) + ?: error("Can't find blockchain info for ${network.networkId}") + + val isMainNetwork = network.contractAddress == null + + return BlockchainRowUM( + id = network.networkId, + name = blockchainInfo.name, + type = if (isMainNetwork) "MAIN" else blockchainInfo.protocolName, + iconResId = if (isSelected) { + getActiveIconRes(blockchainInfo.blockchainId) + } else { + getGreyedOutIconRes(blockchainInfo.blockchainId) + }, + isMainNetwork = isMainNetwork, + isSelected = isSelected, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt new file mode 100644 index 0000000000..b2d43ea69a --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt @@ -0,0 +1,14 @@ +package com.tangem.features.markets.portfolio.impl.model + +/** + * Model for portfolio bottom sheet visibility + * + * @property addToPortfolioBSVisibility visibility of add to portfolio bottom sheet + * @property walletSelectorBSVisibility visibility of wallet selector bottom sheet + * +[REDACTED_AUTHOR] + */ +internal data class PortfolioBSVisibilityModel( + val addToPortfolioBSVisibility: Boolean = false, + val walletSelectorBSVisibility: Boolean = false, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt new file mode 100644 index 0000000000..ea02d5c805 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter from [TokenMarketParams] to [SelectNetworkUM] + * + * @property networksWithToggle map of networks with toggles + * @property onNetworkSwitchClick callback is called when network switch is clicked + * +[REDACTED_AUTHOR] + */ +internal class SelectNetworkUMConverter( + private val networksWithToggle: Map, + private val onNetworkSwitchClick: (String, Boolean) -> Unit, +) : Converter { + + override fun convert(value: TokenMarketParams): SelectNetworkUM { + return SelectNetworkUM( + tokenId = value.id, + iconUrl = value.imageUrl, + tokenName = value.name, + tokenCurrencySymbol = value.symbol, + networks = BlockchainRowUMConverter.convertList(networksWithToggle.toList()).toImmutableList(), + onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um.id, isChecked) }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 422bcdb678..315718d001 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -35,7 +35,6 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -214,15 +213,7 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi modifier = Modifier.padding( end = TangemTheme.dimens.spacing4, ), - model = with(network) { - BlockchainRowUM( - name = name, - type = type, - iconResId = iconResId, - isMainNetwork = isMainNetwork, - isSelected = isSelected, - ) - }, + model = network, action = { TangemSwitch( checked = network.isSelected, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index d218f4f8db..725f9518be 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -15,6 +15,7 @@ import kotlinx.collections.immutable.persistentListOf internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { private val blockchainRow = BlockchainRowUM( + id = "1", name = "Etherium 3", type = "TEST", iconResId = R.drawable.ic_eth_16, @@ -60,7 +61,6 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider onDismissRequest = {}, content = TangemBottomSheetConfigContent.Empty, ), - onWalletClick = {}, ), AddToPortfolioBSContentUM( selectedWallet = userWallet, @@ -88,7 +88,6 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider onDismissRequest = {}, content = TangemBottomSheetConfigContent.Empty, ), - onWalletClick = {}, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt index 7369b5cf69..ee30ab8615 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -10,6 +10,5 @@ internal data class AddToPortfolioBSContentUM( val isScanCardNotificationVisible: Boolean, val continueButtonEnabled: Boolean, val onContinueButtonClick: () -> Unit, - val onWalletClick: () -> Unit, val walletSelectorConfig: TangemBottomSheetConfig, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index dd253860fd..830677086d 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -35,12 +35,6 @@ object BlockchainUtils { return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } - /** If current [networkId] is Dogecoin */ - fun isDogecoin(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) - return blockchain == Blockchain.Dogecoin - } - /** If current [networkId] is Tezos */ fun isTezos(networkId: String): Boolean { val blockchain = Blockchain.fromId(networkId) @@ -61,4 +55,35 @@ object BlockchainUtils { fun isSupportedNetworkId(networkId: String): Boolean { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } + + data class BlockchainInfo( + val blockchainId: String, + val name: String, + val protocolName: String, + ) + + fun getNetworkInfo(networkId: String): BlockchainInfo? { + val blockchain = Blockchain.fromNetworkId(networkId) ?: return null + + return BlockchainInfo( + blockchainId = blockchain.id, + name = getNetworkNameWithoutTestnet(blockchain), + protocolName = getNetworkStandardName(blockchain), + ) + } + + private fun getNetworkStandardName(blockchain: Blockchain): String { + return when (blockchain) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20" + Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20" + Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2" + Blockchain.Tron, Blockchain.TronTestnet -> "TRC20" + Blockchain.TON -> "TON" + else -> "" + } + } + + private fun getNetworkNameWithoutTestnet(blockchain: Blockchain): String { + return blockchain.getNetworkName().replace(oldValue = " Testnet", newValue = "") + } } \ No newline at end of file From df938519a2324f579537619b732ea770e8ad6df3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 18:44:22 +0500 Subject: [PATCH 066/197] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 8 ++ .../tangemTech/models/UserTokensResponse.kt | 19 ++++- .../DefaultCustomTokensRepository.kt | 80 ++++++++++++++----- .../managetokens/di/ManageTokensDataModule.kt | 2 + .../utils/ManagedCryptoCurrencyFactory.kt | 1 + .../utils/TokenAddressesConverter.kt | 29 +++++++ .../repository/DefaultCurrenciesRepository.kt | 10 +-- .../model/ManagedCryptoCurrency.kt | 1 + ...emoveCustomManagedCryptoCurrencyUseCase.kt | 18 +++++ .../repository/CustomTokensRepository.kt | 5 +- .../utils/list/ManageTokensListManager.kt | 9 +++ .../utils/list/ManageTokensUiActions.kt | 2 + .../utils/list/ManageTokensUiManager.kt | 3 +- 13 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 1513f4d484..576da07163 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -48,6 +48,14 @@ internal object ManageTokensDomainModule { return CheckIsCurrencyNotAddedUseCase(customTokensRepository) } + @Provides + @Singleton + fun provideRemoveCustomManagedCryptoCurrencyUseCase( + customTokensRepository: CustomTokensRepository, + ): RemoveCustomManagedCryptoCurrencyUseCase { + return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository) + } + @Provides @Singleton fun provideSaveManagedTokensUseCase( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index c4b95bba88..dcd3107e06 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.tangem.common.extensions.calculateHashCode data class UserTokensResponse( @Json(name = "version") val version: Int = 0, @@ -17,7 +18,23 @@ data class UserTokensResponse( @Json(name = "symbol") val symbol: String, @Json(name = "decimals") val decimals: Int, @Json(name = "contractAddress") val contractAddress: String?, - ) + ) { + override fun equals(other: Any?): Boolean { + val otherToken = other as? Token ?: return false + + return otherToken.contractAddress == this.contractAddress && + otherToken.networkId == this.networkId && + otherToken.derivationPath == this.derivationPath && + otherToken.decimals == this.decimals + } + + override fun hashCode(): Int = calculateHashCode( + contractAddress.hashCode(), + networkId.hashCode(), + derivationPath.hashCode(), + decimals.hashCode(), + ) + } enum class GroupType { @Json(name = "none") diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index eabde921a1..0ec95b565f 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -1,37 +1,46 @@ package com.tangem.data.managetokens -import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter -import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.data.common.currency.getBlockchain +import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.getNetwork +import com.tangem.data.managetokens.utils.TokenAddressesConverter +import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, private val appPreferencesStore: AppPreferencesStore, + private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, ) : CustomTokensRepository { private val cryptoCurrencyFactory = CryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + private val tokenAddressConverter = TokenAddressesConverter() + private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean = withContext(dispatchers.io) { @@ -73,7 +82,7 @@ internal class DefaultCustomTokensRepository( val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") val network = getNetwork(networkId, derivationPath) - val tokenAddress = convertTokenAddress( + val tokenAddress = tokenAddressConverter.convertTokenAddress( networkId, contractAddress, symbol = null, @@ -112,10 +121,7 @@ internal class DefaultCustomTokensRepository( } } - override suspend fun createCoin( - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin { + override fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin { val network = getNetwork(networkId, derivationPath) return cryptoCurrencyFactory.createCoin(network) @@ -127,7 +133,7 @@ internal class DefaultCustomTokensRepository( formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { val network = getNetwork(networkId, derivationPath) - val tokenAddress = convertTokenAddress( + val tokenAddress = tokenAddressConverter.convertTokenAddress( networkId, formValues.contractAddress, formValues.symbol, @@ -143,20 +149,54 @@ internal class DefaultCustomTokensRepository( ) } - private fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { - val convertedAddress = when (getBlockchain(networkId)) { - Blockchain.Hedera, - Blockchain.HederaTestnet, - -> HederaTokenAddressConverter().convertToTokenId(contractAddress) - Blockchain.Cardano -> { - // TODO: [REDACTED_JIRA] - CardanoTokenAddressConverter().convertToFingerprint(contractAddress, symbol) + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) = + withContext(dispatchers.io) { + val cryptoCurrency = when (currency) { + is ManagedCryptoCurrency.Custom.Coin -> createCoin(currency.network.id, currency.network.derivationPath) + is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken( + network = currency.network, + rawId = currency.currencyId.rawCurrencyId, + name = currency.name, + symbol = currency.symbol, + decimals = currency.decimals, + contractAddress = currency.contractAddress, + ) + } + + val savedCurrencies = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, + ) + val token = userTokensResponseFactory.createResponseToken(cryptoCurrency) + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }), + ) + when (cryptoCurrency) { + is CryptoCurrency.Coin -> walletManagersFacade.remove(userWalletId, setOf(cryptoCurrency.network)) + is CryptoCurrency.Token -> walletManagersFacade.removeTokens(userWalletId, setOf(cryptoCurrency)) } - else -> contractAddress } - return requireNotNull(convertedAddress) { - "Token contract address is invalid" + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) + + pushTokens(userWalletId, response) + } + + private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) { + Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}") } } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 37d4a334fb..64317a96e8 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -52,12 +52,14 @@ internal object ManageTokensDataModule { tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, appPreferencesStore: AppPreferencesStore, + walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, ): CustomTokensRepository { return DefaultCustomTokensRepository( tangemTechApi, userWalletsStore, appPreferencesStore, + walletManagersFacade, dispatchers, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index c848b36699..5089f51294 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -77,6 +77,7 @@ internal class ManagedCryptoCurrencyFactory { iconUrl = token.id?.let { getIconUrl(it, imageHost) }, contractAddress = contractAddress, network = network, + decimals = token.decimals, ) } } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt new file mode 100644 index 0000000000..9503e4a6c7 --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.managetokens.utils + +import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter +import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.common.currency.getBlockchain +import com.tangem.domain.tokens.model.Network + +internal class TokenAddressesConverter { + private val hederaTokenAddressConverter = HederaTokenAddressConverter() + private val cardanoTokenAddressConverter = CardanoTokenAddressConverter() + + fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { + val convertedAddress = when (getBlockchain(networkId)) { + Blockchain.Hedera, + Blockchain.HederaTestnet, + -> hederaTokenAddressConverter.convertToTokenId(contractAddress) + Blockchain.Cardano -> { + // TODO: [REDACTED_JIRA] + cardanoTokenAddressConverter.convertToFingerprint(contractAddress, symbol) + } + else -> contractAddress + } + + return requireNotNull(convertedAddress) { + "Token contract address is invalid" + } + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 93ad36b66f..8fda73fff1 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -153,15 +153,7 @@ internal class DefaultCurrenciesRepository( val token = userTokensResponseFactory.createResponseToken(currency) storeAndPushTokens( userWalletId = userWalletId, - response = savedCurrencies.copy( - tokens = savedCurrencies.tokens.filterNot { - // it's better to compare by fields, to support renaming and etc - it.contractAddress == token.contractAddress && - it.networkId == token.networkId && - it.derivationPath == token.derivationPath && - it.decimals == token.decimals - }, - ), + response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }), ) } diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index 595a8e1077..848616a5b2 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -25,6 +25,7 @@ sealed class ManagedCryptoCurrency { override val iconUrl: String?, override val network: Network, val contractAddress: String, + val decimals: Int, ) : Custom() data class Coin( diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt new file mode 100644 index 0000000000..61e33c875c --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.wallets.models.UserWalletId + +class RemoveCustomManagedCryptoCurrencyUseCase(private val repository: CustomTokensRepository) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + customCurrency: ManagedCryptoCurrency.Custom, + ): Either { + return Either.catch { + repository.removeCurrency(userWalletId, customCurrency) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index b578627da0..5f84209200 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.managetokens.repository import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -23,11 +24,13 @@ interface CustomTokensRepository { derivationPath: Network.DerivationPath, ): CryptoCurrency.Token? - suspend fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin + fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin suspend fun createCustomToken( networkId: Network.ID, derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token + + suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 92dc53736c..12c248b792 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManageTokensUpdateAction @@ -29,6 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -36,6 +38,7 @@ import javax.inject.Inject internal class ManageTokensListManager @Inject constructor( private val getManagedTokensUseCase: GetManagedTokensUseCase, private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, + private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, ) : ManageTokensUiActions { @@ -160,6 +163,12 @@ internal class ManageTokensListManager @Inject constructor( sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) } + override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) { + scope.launch { + removeCustomCurrencyUseCase.invoke(userWalletId, currency) + } + } + override fun checkNeedToShowRemoveNetworkWarning( currency: ManagedCryptoCurrency.Token, network: Network, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt index 7b9ab64f3f..7b7548289a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -10,6 +10,8 @@ internal interface ManageTokensUiActions { fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) + fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) + fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 3ca2075789..78e3d1011b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -104,7 +104,8 @@ internal class ManageTokensUiManager( network = currency.network, isCoin = currency is ManagedCryptoCurrency.Custom.Coin, onConfirm = { - // TODO: [REDACTED_JIRA] + val userWalletId = requireNotNull(state.value.userWalletId) { "UserWalletId is null. Can not remove" } + actions.removeCustomCurrency(userWalletId = userWalletId, currency = currency) }, ) } From d18d87f41e98062c20bcdbb7866d0023ac770f64 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 18:09:23 +0400 Subject: [PATCH 067/197] Updated on 2026-08-14 --- .../entity/customtoken/CustomTokenFormUM.kt | 2 + .../managetokens/ui/CustomTokenFormContent.kt | 52 +++++++++++++++---- .../managetokens/ui/ManageTokensScreen.kt | 8 +++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index 902c7de226..efa5adecf9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -11,6 +11,7 @@ internal data class CustomTokenFormUM( val tokenForm: TokenFormUM?, val notifications: ImmutableList = persistentListOf(), val canAddToken: Boolean = false, + val isValidating: Boolean = false, val saveToken: () -> Unit, ) { @@ -32,6 +33,7 @@ internal data class TextInputFieldUM( val placeholder: TextReference, val value: String = "", val error: TextReference? = null, + val isEnabled: Boolean = true, val onValueChange: (String) -> Unit, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index 76ae892e76..3565f4c326 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -11,11 +11,11 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -82,6 +82,7 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier .fillMaxWidth(), text = stringResource(id = R.string.custom_token_add_token), enabled = model.canAddToken, + showProgress = model.isValidating, onClick = model.saveToken, ) } @@ -159,14 +160,25 @@ private fun TextField( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, ) { + var isFocused by remember { mutableStateOf(value = false) } + InformationBlock( modifier = modifier, title = { val color by animateColorAsState( - targetValue = if (model.error != null) { - TangemTheme.colors.text.warning - } else { - TangemTheme.colors.text.tertiary + targetValue = when { + !model.isEnabled -> { + TangemTheme.colors.text.disabled + } + model.error != null -> { + TangemTheme.colors.text.warning + } + model.value.isNotBlank() || isFocused -> { + TangemTheme.colors.text.tertiary + } + else -> { + TangemTheme.colors.text.disabled + } }, label = "Field label color", ) @@ -178,14 +190,27 @@ private fun TextField( ) }, content = { + val color by animateColorAsState( + targetValue = if (model.isEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.disabled + }, + label = "Field value color", + ) + SimpleTextField( modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing12) - .fillMaxWidth(), + .fillMaxWidth() + .onFocusChanged { + isFocused = it.isFocused + }, value = model.value, + color = color, onValueChange = model.onValueChange, - readOnly = false, placeholder = model.placeholder, + readOnly = !model.isEnabled && !isFocused, singleLine = true, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, @@ -236,7 +261,16 @@ private class PreviewCustomTokenFormComponentProvider : override val values: Sequence get() = sequenceOf( - PreviewCustomTokenFormComponent(), + PreviewCustomTokenFormComponent( + tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy( + contractAddress = TextInputFieldUM( + label = stringReference("Contract address"), + value = "0x1234567890", + placeholder = stringReference("0x1234567890"), + onValueChange = {}, + ), + ), + ), PreviewCustomTokenFormComponent( tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy( contractAddress = TextInputFieldUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 430896ed3a..9769590b55 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -44,8 +44,10 @@ import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero @@ -93,6 +95,12 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi state = state, ) }, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + hostState = LocalSnackbarHostState.current, + ) + }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { if (state is ManageTokensUM.ManageContent) { From 76300e781976a3a232909a1bf320af49d17a383a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Aug 2024 18:57:51 +0400 Subject: [PATCH 068/197] Updated on 2026-08-14 --- .../impl/DefaultCustomTokenFormComponent.kt | 129 +------- .../PreviewCustomTokenFormComponent.kt | 6 +- .../entity/customtoken/CustomTokenFormUM.kt | 4 +- .../customtoken/CustomTokenFormValues.kt | 14 + .../model/CustomTokenFormModel.kt | 311 ++++++++++++++++++ .../utils/CustomCurrencyValidator.kt | 214 ++++++++++++ .../utils/mapper/TokenFormMapper.kt | 13 + .../utils/ui/CustomCurrencyFormOperations.kt | 155 +++++++++ 8 files changed, 716 insertions(+), 130 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt index 2ca0b7e1a1..f2413c6b88 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt @@ -5,33 +5,24 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.managetokens.component.CustomTokenFormComponent -import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM -import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM -import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues -import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM -import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.model.CustomTokenFormModel import com.tangem.features.managetokens.ui.CustomTokenFormContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update internal class DefaultCustomTokenFormComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: CustomTokenFormComponent.Params, ) : CustomTokenFormComponent, AppComponentContext by context { - private val state: MutableStateFlow = MutableStateFlow( - value = getInitialState(), - ) + private val model: CustomTokenFormModel = getOrCreateModel(params) @Composable override fun Content(modifier: Modifier) { - val state by state.collectAsStateWithLifecycle() + val state by model.state.collectAsStateWithLifecycle() CustomTokenFormContent( modifier = modifier, @@ -39,114 +30,6 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor( ) } - private fun getInitialState(): CustomTokenFormUM { - return CustomTokenFormUM( - networkName = ClickableFieldUM( - label = resourceReference(R.string.custom_token_network_input_title), - value = params.network.name, - onClick = ::selectNetwork, - ), - tokenForm = getInitialTokenForm(), - derivationPath = ClickableFieldUM( - label = resourceReference(R.string.custom_token_derivation_path), - value = params.derivationPath.networkName, - onClick = ::selectDerivationPath, - ), - saveToken = { - // TODO: Save token: [REDACTED_JIRA] - }, - ) - } - - private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM { - val formValues = params.formValues - - val form = CustomTokenFormUM.TokenFormUM( - contractAddress = TextInputFieldUM( - label = resourceReference(R.string.custom_token_contract_address_input_title), - placeholder = stringReference(value = "0x000000000000000000000000000..."), - onValueChange = ::updateContractAddress, - ), - name = TextInputFieldUM( - label = resourceReference(R.string.custom_token_name_input_title), - placeholder = resourceReference(R.string.custom_token_name_input_placeholder), - onValueChange = ::updateTokenName, - ), - symbol = TextInputFieldUM( - label = resourceReference(R.string.custom_token_token_symbol_input_title), - placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder), - onValueChange = ::updateTokenSymbol, - ), - decimals = TextInputFieldUM( - label = resourceReference(R.string.custom_token_decimals_input_title), - placeholder = stringReference(value = "0"), - onValueChange = ::updateDecimals, - ), - ) - - return formValues.fillValues(form) - } - - private fun updateContractAddress(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - contractAddress = tokenForm.contractAddress.copy(value = value), - ), - ) - } - } - - private fun updateTokenName(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - name = tokenForm.name.copy(value = value), - ), - ) - } - } - - private fun updateTokenSymbol(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - symbol = tokenForm.symbol.copy(value = value), - ), - ) - } - } - - private fun updateDecimals(value: String) { - // TODO: Add field validation: [REDACTED_JIRA] - state.update { state -> - val tokenForm = state.tokenForm ?: return@update state - - state.copy( - tokenForm = tokenForm.copy( - decimals = tokenForm.decimals.copy(value = value), - ), - ) - } - } - - private fun selectNetwork() { - params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) - } - - private fun selectDerivationPath() { - params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm)) - } - @AssistedFactory interface Factory : CustomTokenFormComponent.Factory { override fun create( @@ -154,8 +37,4 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor( params: CustomTokenFormComponent.Params, ): DefaultCustomTokenFormComponent } - - private companion object { - const val FORM_VALUES_KEY = "form_values" - } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt index 1c69d487de..fa909e5257 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt @@ -11,7 +11,7 @@ import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.CustomTokenFormContent -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf internal class PreviewCustomTokenFormComponent( @@ -19,7 +19,7 @@ internal class PreviewCustomTokenFormComponent( derivationPath: ClickableFieldUM = PreviewCustomTokenFormComponent.derivationPath, canAddToken: Boolean = false, tokenForm: CustomTokenFormUM.TokenFormUM? = PreviewCustomTokenFormComponent.tokenForm, - notifications: ImmutableList = PreviewCustomTokenFormComponent.notifications, + notifications: PersistentList = PreviewCustomTokenFormComponent.notifications, ) : CustomTokenFormComponent { private val previewState = CustomTokenFormUM( @@ -73,7 +73,7 @@ internal class PreviewCustomTokenFormComponent( onValueChange = {}, ), ) - val notifications: ImmutableList = persistentListOf( + val notifications: PersistentList = persistentListOf( CustomTokenFormUM.NotificationUM( id = "1", config = NotificationConfig( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index efa5adecf9..1fbd8e4de3 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -2,14 +2,14 @@ package com.tangem.features.managetokens.entity.customtoken import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf internal data class CustomTokenFormUM( val networkName: ClickableFieldUM, val derivationPath: ClickableFieldUM, val tokenForm: TokenFormUM?, - val notifications: ImmutableList = persistentListOf(), + val notifications: PersistentList = persistentListOf(), val canAddToken: Boolean = false, val isValidating: Boolean = false, val saveToken: () -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt index 1551b4bc30..983eb8e61a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt @@ -1,5 +1,6 @@ package com.tangem.features.managetokens.entity.customtoken +import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM import kotlinx.serialization.Serializable @@ -28,4 +29,17 @@ internal value class CustomTokenFormValues private constructor(private val value symbol = to.symbol.copy(value = values.getOrElse(index = 2) { "" }), decimals = to.decimals.copy(value = values.getOrElse(index = 3) { "" }), ) + + fun toDomainModel(): AddCustomTokenForm.Raw? { + return if (values.isEmpty()) { + null + } else { + AddCustomTokenForm.Raw( + contractAddress = values.getOrElse(index = 0) { "" }, + name = values.getOrElse(index = 1) { "" }, + symbol = values.getOrElse(index = 2) { "" }, + decimals = values.getOrElse(index = 3) { "" }, + ) + } + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt new file mode 100644 index 0000000000..0565adb15e --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -0,0 +1,311 @@ +package com.tangem.features.managetokens.model + +import androidx.compose.ui.res.stringResource +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.SimpleOkDialog +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.managetokens.component.CustomTokenFormComponent +import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.CustomCurrencyValidator +import com.tangem.features.managetokens.utils.mapper.mapToDomainModel +import com.tangem.features.managetokens.utils.ui.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class CustomTokenFormModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val customCurrencyValidator: CustomCurrencyValidator, + private val messageSender: UiMessageSender, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: CustomTokenFormComponent.Params = paramsContainer.require() + + val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + init { + observeValidatorUpdates() + + if (params.network.canHandleTokens) { + observeTokenFormUpdates() + } else { + modelScope.launch { + customCurrencyValidator.createCoin( + userWalletId = params.userWalletId, + networkId = params.network.id, + derivationPath = params.derivationPath.value, + ) + } + } + } + + private fun getInitialState(): CustomTokenFormUM { + return CustomTokenFormUM( + networkName = ClickableFieldUM( + label = resourceReference(R.string.custom_token_network_input_title), + value = params.network.name, + onClick = ::selectNetwork, + ), + tokenForm = if (params.network.canHandleTokens) { + getInitialTokenForm() + } else { + null + }, + derivationPath = ClickableFieldUM( + label = resourceReference(R.string.custom_token_derivation_path), + value = if (params.derivationPath.id == params.network.id) { + resourceReference(R.string.custom_token_derivation_path_default) + } else { + params.derivationPath.networkName + }, + onClick = ::selectDerivationPath, + ), + saveToken = { + // TODO: Save token: [REDACTED_JIRA] + val dialog = ContentMessage { onDismiss -> + SimpleOkDialog( + message = "Not yet implemented", + onDismissDialog = onDismiss, + ) + } + + messageSender.send(dialog) + }, + ) + } + + @OptIn(FlowPreview::class) + private fun observeTokenFormUpdates() { + state + .sample(periodMillis = 1_000) + .transform { + val values = it.tokenForm?.mapToDomainModel() + + if (values != null) { + emit(values) + } + } + .distinctUntilChanged() + .onEach { formValues -> + customCurrencyValidator.validateForm( + userWalletId = params.userWalletId, + networkId = params.network.id, + derivationPath = params.derivationPath.value, + formValues = formValues, + ) + } + .launchIn(modelScope) + } + + private fun observeValidatorUpdates() = modelScope.launch { + customCurrencyValidator.consumeUpdates { validatorState -> + when (validatorState) { + is CustomCurrencyValidator.State.NotStarted -> Unit + is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching() + is CustomCurrencyValidator.State.UnexpectedException -> showErrorDialog() + is CustomCurrencyValidator.State.FormValidationException -> updateStateWithExceptions( + exceptions = validatorState.exceptions, + ) + is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification() + is CustomCurrencyValidator.State.Validated -> updateStateWithCurrency( + currency = validatorState.currency, + fillForm = validatorState.fillForm, + isAlreadyAdded = validatorState.isAlreadyAdded, + isCustom = validatorState.isCustom, + ) + } + } + } + + private fun updateStateWithCurrency( + currency: CryptoCurrency, + fillForm: Boolean, + isAlreadyAdded: Boolean, + isCustom: Boolean, + ) { + state.update { state -> + var updatedState = state + .updateWithProgress( + showProgress = false, + canAddToken = !isAlreadyAdded, + clearNotifications = true, + clearFieldErrors = true, + disableSecondaryFields = !isCustom, + ) + + if (fillForm) { + updatedState = updatedState.updateWithCurrency(currency) + } + + if (isAlreadyAdded) { + updatedState = updatedState.updateWithCurrencyAlreadyAddedNotification() + } + + if (isCustom) { + updatedState = updatedState.updateWithCurrencyNotFoundNotification() + } + + updatedState + } + } + + private fun updateStateWithNotFoundNotification() { + state.update { state -> + state + .updateWithProgress( + showProgress = false, + canAddToken = false, + clearNotifications = true, + clearFieldErrors = true, + ) + .updateWithCurrencyNotFoundNotification() + } + } + + private fun updateStateWithSearching() { + state.update { state -> + state.updateWithProgress( + showProgress = true, + canAddToken = false, + clearNotifications = false, + clearFieldErrors = false, + ) + } + } + + private fun showErrorDialog() { + val dialog = ContentMessage { onDismiss -> + SimpleOkDialog( + message = stringResource(R.string.common_unknown_error), + onDismissDialog = onDismiss, + ) + } + + messageSender.send(dialog) + } + + private fun updateStateWithExceptions(exceptions: List) { + state.update { state -> + val validatedState = state + .updateWithProgress( + showProgress = false, + canAddToken = false, + clearNotifications = true, + clearFieldErrors = true, + ) + + exceptions.fold(validatedState) { stateAcc, exception -> + when (exception) { + is CustomTokenFormValidationException.ContractAddress -> { + stateAcc.updateWithContractAddressException(exception) + } + is CustomTokenFormValidationException.Decimals -> { + stateAcc.updateWithDecimalsException(exception) + } + is CustomTokenFormValidationException.EmptyName -> { + stateAcc // No need to display error + } + is CustomTokenFormValidationException.EmptySymbol -> { + stateAcc // No need to display error + } + is CustomTokenFormValidationException.DataError -> { + Timber.e(exception.cause, "Unable to validate custom currency") + stateAcc + } + } + } + } + } + + private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM { + val formValues = params.formValues + + val form = CustomTokenFormUM.TokenFormUM( + contractAddress = TextInputFieldUM( + label = resourceReference(R.string.custom_token_contract_address_input_title), + placeholder = stringReference(CONTRACT_ADDRESS_PLACEHOLDER), + onValueChange = ::updateContractAddress, + ), + name = TextInputFieldUM( + label = resourceReference(R.string.custom_token_name_input_title), + placeholder = resourceReference(R.string.custom_token_name_input_placeholder), + onValueChange = ::updateTokenName, + ), + symbol = TextInputFieldUM( + label = resourceReference(R.string.custom_token_token_symbol_input_title), + placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder), + onValueChange = ::updateTokenSymbol, + ), + decimals = TextInputFieldUM( + label = resourceReference(R.string.custom_token_decimals_input_title), + placeholder = stringReference(DECIMALS_PLACEHOLDER), + onValueChange = ::updateDecimals, + ), + ) + + return formValues.fillValues(form) + } + + private fun updateContractAddress(value: String) { + state.update { state -> + state.updateTokenForm { + copy(contractAddress = contractAddress.updateValue(value)) + } + } + } + + private fun updateTokenName(value: String) { + state.update { state -> + state.updateTokenForm { + copy(name = name.updateValue(value)) + } + } + } + + private fun updateTokenSymbol(value: String) { + state.update { state -> + state.updateTokenForm { + copy(symbol = symbol.updateValue(value)) + } + } + } + + private fun updateDecimals(value: String) { + state.update { state -> + state.updateTokenForm { + copy(decimals = decimals.updateValue(value)) + } + } + } + + private fun selectNetwork() { + params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) + } + + private fun selectDerivationPath() { + params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm)) + } + + private companion object { + const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..." + const val DECIMALS_PLACEHOLDER = "0" + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt new file mode 100644 index 0000000000..373b4640c7 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -0,0 +1,214 @@ +package com.tangem.features.managetokens.utils + +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase +import com.tangem.domain.managetokens.CreateCurrencyUseCase +import com.tangem.domain.managetokens.FindTokenUseCase +import com.tangem.domain.managetokens.ValidateTokenFormUseCase +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.managetokens.model.exceptoin.FindTokenException +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class CustomCurrencyValidator @Inject constructor( + private val validateTokenFormUseCase: ValidateTokenFormUseCase, + private val createCustomCurrencyUseCase: CreateCurrencyUseCase, + private val findTokenUseCase: FindTokenUseCase, + private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, +) { + + private val state: MutableStateFlow = MutableStateFlow(State.NotStarted) + + suspend fun consumeUpdates(block: suspend (State) -> Unit) { + state.collectLatest { s -> + block(s) + } + } + + suspend fun validateForm( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Raw, + ) { + val result = validateTokenFormUseCase( + networkId = networkId, + formValues = formValues, + ) + + val validatedForm = result.getOrElse { e -> + state.value = State.FormValidationException(e) + return + } + + when (validatedForm) { + is AddCustomTokenForm.Validated.All -> { + findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm) + } + is AddCustomTokenForm.Validated.ContractAddress -> { + findToken(userWalletId, networkId, derivationPath, validatedForm) + } + } + } + + suspend fun createCoin(userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath) { + createCurrency(userWalletId, networkId, derivationPath, validatedForm = null) + } + + private suspend fun findOrCreateCurrency( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + validatedForm: AddCustomTokenForm.Validated.All, + ) { + state.value = State.SearchingToken + + val foundToken = findTokenUseCase( + userWalletId = userWalletId, + contractAddress = validatedForm.contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ).getOrElse { e -> + when (e) { + is FindTokenException.DataError -> { + Timber.e(e.cause, "Unable to find custom currency") + state.value = State.UnexpectedException(e.cause) + return + } + is FindTokenException.NotFound -> { + null + } + } + } + + if (foundToken != null) { + updateStateToValidated(userWalletId, foundToken, fillForm = true, isCustom = false) + } else { + createCurrency(userWalletId, networkId, derivationPath, validatedForm) + } + } + + private suspend fun findToken( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + validatedForm: AddCustomTokenForm.Validated.ContractAddress, + ) { + state.value = State.SearchingToken + + val token = findTokenUseCase( + userWalletId = userWalletId, + contractAddress = validatedForm.contractAddress, + networkId = networkId, + derivationPath = derivationPath, + ).getOrElse { e -> + state.value = when (e) { + is FindTokenException.DataError -> { + Timber.e(e.cause, "Unable to find custom currency") + State.UnexpectedException(e.cause) + } + is FindTokenException.NotFound -> { + State.TokenNotFound + } + } + + return + } + + updateStateToValidated(userWalletId, token, fillForm = true, isCustom = false) + } + + private suspend fun createCurrency( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + validatedForm: AddCustomTokenForm.Validated.All?, + ) { + val currency = createCustomCurrencyUseCase( + networkId = networkId, + derivationPath = derivationPath, + formValues = validatedForm, + ).getOrElse { e -> + Timber.e(e, "Unable to create custom currency") + state.value = State.UnexpectedException(e) + return + } + + updateStateToValidated(userWalletId, currency, fillForm = false, isCustom = validatedForm != null) + } + + private suspend fun updateStateToValidated( + userWalletId: UserWalletId, + currency: CryptoCurrency, + fillForm: Boolean, + isCustom: Boolean, + ) { + val isNotAdded = checkIsCurrencyNotAddedUseCase( + userWalletId = userWalletId, + networkId = currency.network.id, + derivationPath = currency.network.derivationPath, + contractAddress = when (currency) { + is CryptoCurrency.Coin -> null + is CryptoCurrency.Token -> currency.contractAddress + }, + ).getOrElse { e -> + Timber.e(e, "Unable to check if currency is already added") + state.value = State.UnexpectedException(e) + + return + } + + state.value = State.Validated( + currency = currency, + fillForm = fillForm, + isAlreadyAdded = !isNotAdded, + isCustom = isCustom, + ) + } + + sealed class State { + + abstract val isFinished: Boolean + + data object NotStarted : State() { + override val isFinished: Boolean = false + } + + data object SearchingToken : State() { + override val isFinished: Boolean = false + } + + data class Validated( + val currency: CryptoCurrency, + val fillForm: Boolean, + val isAlreadyAdded: Boolean, + val isCustom: Boolean, + ) : State() { + override val isFinished: Boolean = true + } + + data class FormValidationException( + val exceptions: List, + ) : State() { + override val isFinished: Boolean = true + } + + data object TokenNotFound : State() { + override val isFinished: Boolean = true + } + + data class UnexpectedException( + val cause: Throwable, + ) : State() { + override val isFinished: Boolean = true + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt new file mode 100644 index 0000000000..fd9a7bd140 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt @@ -0,0 +1,13 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM + +internal fun CustomTokenFormUM.TokenFormUM.mapToDomainModel(): AddCustomTokenForm.Raw { + return AddCustomTokenForm.Raw( + contractAddress = contractAddress.value, + symbol = symbol.value, + name = name.value, + decimals = decimals.value, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt new file mode 100644 index 0000000000..71ecd3cd8d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt @@ -0,0 +1,155 @@ +package com.tangem.features.managetokens.utils.ui + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.managetokens.ValidateTokenFormUseCase +import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf + +internal fun CustomTokenFormUM.updateTokenForm( + block: CustomTokenFormUM.TokenFormUM.() -> CustomTokenFormUM.TokenFormUM, +): CustomTokenFormUM { + val form = tokenForm ?: return this + + val updatedForm = form.block() + + return copy(tokenForm = updatedForm) +} + +internal fun TextInputFieldUM.updateValue( + value: String = this.value, + error: TextReference? = this.error, + isEnabled: Boolean = this.isEnabled, + clearError: Boolean = false, +): TextInputFieldUM { + return copy( + value = value, + error = if (clearError) null else error, + isEnabled = isEnabled, + ) +} + +internal fun CustomTokenFormUM.updateWithProgress( + showProgress: Boolean, + canAddToken: Boolean = this.canAddToken, + clearNotifications: Boolean = false, + clearFieldErrors: Boolean = false, + disableSecondaryFields: Boolean = false, +): CustomTokenFormUM { + return copy( + isValidating = showProgress, + canAddToken = canAddToken, + notifications = if (clearNotifications) persistentListOf() else notifications, + ).updateTokenForm { + copy( + contractAddress = contractAddress.updateValue( + clearError = clearFieldErrors, + ), + name = name.updateValue( + isEnabled = !showProgress && !disableSecondaryFields, + clearError = clearFieldErrors, + ), + symbol = symbol.updateValue( + isEnabled = !showProgress && !disableSecondaryFields, + clearError = clearFieldErrors, + ), + decimals = decimals.updateValue( + isEnabled = !showProgress && !disableSecondaryFields, + clearError = clearFieldErrors, + ), + ) + } +} + +internal fun CustomTokenFormUM.updateWithCurrency(currency: CryptoCurrency): CustomTokenFormUM { + return updateTokenForm { + copy( + contractAddress = contractAddress.updateValue(error = null), + name = name.updateValue(currency.name), + symbol = symbol.updateValue(currency.symbol), + decimals = decimals.updateValue(currency.decimals.toString()), + ) + } +} + +internal fun CustomTokenFormUM.updateWithContractAddressException( + exception: CustomTokenFormValidationException.ContractAddress, +): CustomTokenFormUM { + return updateTokenForm { + copy( + contractAddress = contractAddress.updateValue( + error = when (exception) { + CustomTokenFormValidationException.ContractAddress.Empty -> { + null + } + CustomTokenFormValidationException.ContractAddress.Invalid -> { + resourceReference(R.string.custom_token_creation_error_invalid_contract_address) + } + }, + ), + ) + } +} + +internal fun CustomTokenFormUM.updateWithDecimalsException( + exception: CustomTokenFormValidationException.Decimals, +): CustomTokenFormUM { + return updateTokenForm { + copy( + decimals = decimals.updateValue( + error = when (exception) { + is CustomTokenFormValidationException.Decimals.Empty -> { + null + } + is CustomTokenFormValidationException.Decimals.Invalid -> { + resourceReference( + R.string.custom_token_creation_error_wrong_decimals, + wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), + ) + } + }, + ), + ) + } +} + +internal fun CustomTokenFormUM.updateWithCurrencyNotFoundNotification(): CustomTokenFormUM { + val notification = CustomTokenFormUM.NotificationUM( + id = "currency_not_found", + config = NotificationConfig( + title = resourceReference(R.string.custom_token_validation_error_not_found_title), + subtitle = resourceReference(R.string.custom_token_validation_error_not_found_description), + iconResId = R.drawable.img_attention_20, + ), + ) + + return copy( + notifications = notifications.mutate { + it.add(notification) + }, + ) +} + +internal fun CustomTokenFormUM.updateWithCurrencyAlreadyAddedNotification(): CustomTokenFormUM { + val notification = CustomTokenFormUM.NotificationUM( + id = "currency_already_added", + config = NotificationConfig( + title = resourceReference(R.string.custom_token_creation_error_token_already_exist_title), + subtitle = resourceReference(R.string.custom_token_creation_error_token_already_exist_message), + iconResId = R.drawable.img_attention_20, + ), + ) + + return copy( + notifications = notifications.mutate { + it.add(notification) + }, + ) +} \ No newline at end of file From 6cd6210a05d137805da29eab42c09ae15aa33190 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 16:16:48 +0400 Subject: [PATCH 069/197] Updated on 2026-08-14 --- .../DefaultMarketsTokenDetailsComponent.kt | 2 +- .../api/MarketsPortfolioComponent.kt | 3 +- .../impl/model/MarketsPortfolioModel.kt | 167 +++++++++++++----- .../impl/model/MyPortfolioUMMFactory.kt | 156 ++++++++++++---- .../impl/model/PortfolioTokenUMConverter.kt | 4 +- .../impl/model/TokensPortfolioUMConverter.kt | 52 +++--- .../markets/portfolio/impl/ui/MyPortfolio.kt | 6 +- .../preview/PreviewMyPortfolioUMProvider.kt | 22 +++ .../portfolio/impl/ui/state/MyPortfolioUM.kt | 9 +- 9 files changed, 292 insertions(+), 129 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index a1ea813365..0d758ce282 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -35,7 +35,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( private val portfolioComponent = portfolioComponentFactory.create( context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params(params.token.id), + params = MarketsPortfolioComponent.Params(params.token), ) init { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt index e23fee1426..c736781c03 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt @@ -4,13 +4,14 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams import kotlinx.serialization.Serializable @Stable interface MarketsPortfolioComponent : ComposableContentComponent { @Serializable - data class Params(val tokenId: String) + data class Params(val token: TokenMarketParams) fun setTokenNetworks(networks: List) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 83fd674f85..219da20190 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -5,13 +5,12 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList @@ -23,9 +22,8 @@ import javax.inject.Inject @ComponentScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, - getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val portfolioDataLoader: PortfolioDataLoader, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -34,59 +32,134 @@ internal class MarketsPortfolioModel @Inject constructor( private val params = paramsContainer.require() - private val availableNetworks = MutableStateFlow?>(value = null) + private val availableNetworksFlow = MutableStateFlow?>(value = null) - private val factory = MyPortfolioUMMFactory(::onAddClick, ::onTokenItemClick) + /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ + private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) + + /** Map of [UserWalletId] and network ids that user is changed */ + private val walletsWithChangedNetworksFlow = MutableStateFlow>>(value = emptyMap()) + + private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) + + private val factory = MyPortfolioUMMFactory( + onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) }, + onTokenItemClick = ::onTokenItemClick, + addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( + token = params.token, + onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, + onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, + onNetworkSwitchClick = ::onNetworkSwitchClick, + onWalletSelect = ::onWalletSelect, + onContinueClick = ::onContinueClick, + ), + ) init { - combine( - flow = getAllWalletsCryptoCurrencyStatusesUseCase(params.tokenId), - flow2 = availableNetworks, - flow3 = getSelectedAppCurrencyUseCase(), - flow4 = getBalanceHidingSettingsUseCase(), - ) { walletsWithCurrencyStatuses, networks, maybeAppCurrency, isBalanceHidden -> - val appCurrency = maybeAppCurrency.getOrElse { e -> - Timber.e("Failed to load app currency: $e") - AppCurrency.Default - } + // Subscribe on selected wallet flow to support actual selected wallet + subscribeOnSelectedMultiWalletUpdates() - factory.create( - walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, - availableNetworks = networks, - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden.isBalanceHidden, - ) - } + subscribeOnStateUpdates() + } + + fun setTokenNetworks(networks: List) { + availableNetworksFlow.value = networks + } + + fun setNoNetworksAvailable() { + availableNetworksFlow.value = emptyList() + } + + private fun subscribeOnSelectedMultiWalletUpdates() { + getSelectedWalletUseCase() + .getOrElse { e -> + Timber.e("Failed to load selected wallet: $e") + error("Failed to load selected wallet") + } + .onEach { + selectedMultiWalletIdFlow.value = it.takeIf { it.isMultiCurrency }?.walletId + } + .launchIn(modelScope) + } + + private fun subscribeOnStateUpdates() { + combine( + flow = portfolioDataLoader.load(params.token.id), + flow2 = portfolioBSVisibilityModelFlow, + flow3 = availableNetworksFlow, + flow4 = selectedMultiWalletIdFlow, + flow5 = walletsWithChangedNetworksFlow, + transform = factory::create, + ) .onEach { _state.value = it } .launchIn(modelScope) } - fun setTokenNetworks(networks: List) { - availableNetworks.value = networks - } - - fun setNoNetworksAvailable() { - availableNetworks.value = emptyList() - } - - private fun onAddClick() { - // TODO [REDACTED_JIRA] - } - - private fun onTokenItemClick(token: CryptoCurrencyStatus) { + private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) { _state.update { - (_state.value as? MyPortfolioUM.Tokens)?.let { state -> - state.copy( - tokens = state.tokens.map { tokenUM -> - if (tokenUM.tokenItemState.id == token.currency.id.value) { + val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it + + state.copy( + tokens = state.tokens + .mapIndexed { i, tokenUM -> + // check by index and id because id can be the same + if (index == i && id.value == tokenUM.tokenItemState.id) { tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown) } else { tokenUM } } - .toImmutableList(), - ) - } ?: it + .toImmutableList(), + ) + } + } + + private fun onNetworkSwitchClick(id: String, isChecked: Boolean) { + walletsWithChangedNetworksFlow.update { + val selectedWalletId = selectedMultiWalletIdFlow.value + + if (selectedWalletId == null) { + Timber.e("Impossible ti switch network when selected wallet is null") + return@update it + } + + it.toMutableMap().apply { + val networkIds = this[selectedWalletId] ?: emptyList() + + if (isChecked) { + this[selectedWalletId] = networkIds + id + } else { + this[selectedWalletId] = networkIds - id + } + } + } + } + + private fun onWalletSelect(userWalletId: UserWalletId) { + selectedMultiWalletIdFlow.update { prevUserWalletId -> + + // Clear user changes if user select another wallet + walletsWithChangedNetworksFlow.update { + it.toMutableMap().apply { remove(prevUserWalletId) } + } + + userWalletId + } + } + + private fun onContinueClick() { + // TODO [REDACTED_JIRA] + } + + private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { + portfolioBSVisibilityModelFlow.update { + it.copy(addToPortfolioBSVisibility = isShow, walletSelectorBSVisibility = false) + } + } + + private fun onWalletSelectorVisibilityChange(isShow: Boolean) { + portfolioBSVisibilityModelFlow.update { + it.copy(addToPortfolioBSVisibility = true, walletSelectorBSVisibility = isShow) } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt index bd9042cac0..91072c3906 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt @@ -1,11 +1,12 @@ package com.tangem.features.markets.portfolio.impl.model -import arrow.core.Either -import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM /** @@ -18,63 +19,142 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM */ internal class MyPortfolioUMMFactory( private val onAddClick: () -> Unit, - private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, + private val onTokenItemClick: (Int, CryptoCurrency.ID) -> Unit, + private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, ) { - /** - * Create [MyPortfolioUM] from data set - * - * @param walletsWithCurrencyStatuses wallets with currency statuses - * @param availableNetworks available networks - * @param appCurrency app currency - * @param isBalanceHidden flag that indicates if balance should be hidden - */ fun create( - walletsWithCurrencyStatuses: Map>>, + portfolioModel: PortfolioData, + portfolioBSVisibilityModel: PortfolioBSVisibilityModel, availableNetworks: List?, - appCurrency: AppCurrency, - isBalanceHidden: Boolean, + selectedWalletId: UserWalletId?, + walletsWithChangedNetworks: Map>, ): MyPortfolioUM { if (availableNetworks == null) return MyPortfolioUM.Loading if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable - val availableWalletsWithStatuses = walletsWithCurrencyStatuses - .filterAvailable(networks = availableNetworks) - .flatMapStatuses() + val walletsWithStatuses = portfolioModel.walletsWithCurrencyStatuses + .filterAvailableNetworks(networks = availableNetworks) - if (availableWalletsWithStatuses.isEmpty()) { - return MyPortfolioUM.AddFirstToken(onAddClick = onAddClick) + val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty() + if (isPortfolioEmpty) { + val hasMultiWallets = walletsWithStatuses.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() + + return if (hasMultiWallets) { + MyPortfolioUM.AddFirstToken( + bsConfig = createAddToPortfolioBSConfig( + portfolioModel = portfolioModel, + selectedWalletId = selectedWalletId, + availableNetworks = availableNetworks, + walletsWithChangedNetworks = walletsWithChangedNetworks, + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + ), + onAddClick = onAddClick, + ) + } else { + MyPortfolioUM.Unavailable + } } return TokensPortfolioUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - availableNetworks = availableNetworks, + appCurrency = portfolioModel.appCurrency, + isBalanceHidden = portfolioModel.isBalanceHidden, + isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks), + bsConfig = createAddToPortfolioBSConfig( + portfolioModel = portfolioModel, + selectedWalletId = selectedWalletId, + availableNetworks = availableNetworks, + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + walletsWithChangedNetworks = walletsWithChangedNetworks, + ), onAddClick = onAddClick, onTokenItemClick = onTokenItemClick, ) - .convert(availableWalletsWithStatuses) + .convert(walletsWithStatuses) } - private fun Map>>.filterAvailable( - networks: List, - ): Map> { - val networkIds = networks.map { it.networkId } + private fun createAddToPortfolioBSConfig( + portfolioModel: PortfolioData, + selectedWalletId: UserWalletId?, + availableNetworks: List, + portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + walletsWithChangedNetworks: Map>, + ): TangemBottomSheetConfig { + val walletId = requireNotNull(selectedWalletId) { + "Selected wallet must be not null in state with AddToPortfolio bottom sheet" + } - return mapValues { entry -> - entry.value.mapNotNull { maybeStatus -> - maybeStatus.getOrNull() - ?.takeIf { networkIds.contains(it.currency.network.backendId) } - } + val selectedWallet = portfolioModel.walletsWithCurrencyStatuses.keys + .firstOrNull { it.walletId == walletId } + ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") + + val changedNetworks = walletsWithChangedNetworks[selectedWalletId] + val alreadyAddedNetworks = requireNotNull( + value = portfolioModel.walletsWithCurrencyStatuses[selectedWallet], + lazyMessage = { + "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" + }, + ) + .filterAvailableNetworks(availableNetworks) + .map { it.currency.network.backendId } + + return addToPortfolioBSContentUMFactory.create( + portfolioData = portfolioModel, + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + selectedWallet = selectedWallet, + networksWithToggle = availableNetworks.associateWithToggle( + changedNetworks = changedNetworks, + alreadyAddedNetworks = alreadyAddedNetworks, + ), + isUserChangedNetworks = changedNetworks != null && alreadyAddedNetworks != changedNetworks, + ) + } + + private fun List.associateWithToggle( + changedNetworks: List?, + alreadyAddedNetworks: List, + ): Map { + // Use user choice or check already added networks + return associateWith { network -> + val isSelectedByUser = changedNetworks?.contains(network.networkId) + + if (isSelectedByUser != null) return@associateWith isSelectedByUser + + val isAlreadyAdded = alreadyAddedNetworks.any { it == network.networkId } + + isAlreadyAdded } } - private fun Map>.flatMapStatuses(): Map { + private fun Map>.isAllAvailableNetworksAdded( + availableNetworks: List, + ): Boolean { + val networkIds = availableNetworks.map { it.networkId } + return this - .flatMap { entry -> - entry.value.map { entry.key to it } - } - .toMap() + // User can add currencies only in multi-currency wallets + .filterKeys(UserWallet::isMultiCurrency) + .mapValues { it.value.map { it.currency.network.backendId } } + // Each wallets contains all available networks? + .all { it.value.containsAll(networkIds) } + } + + /** Filter map values by available networks [networks] */ + private fun Map>.filterAvailableNetworks( + networks: List, + ): Map> { + return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } + } + + /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ + private fun List.filterAvailableNetworks( + networks: List, + ): List { + val networkIds = networks.map(TokenMarketInfo.Network::networkId) + + return mapNotNull { + it.takeIf { networkIds.contains(it.currency.network.backendId) } + } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index d299172177..5e05440a85 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -17,9 +17,9 @@ internal class PortfolioTokenUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, -) : Converter, PortfolioTokenUM> { +) : Converter, PortfolioTokenUM> { - override fun convert(value: Map.Entry): PortfolioTokenUM { + override fun convert(value: Pair): PortfolioTokenUM { val (userWallet, cryptoCurrencyStatus) = value val tokenItemStateConverter = TokenItemStateConverter( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index aa3b90ea8f..e9e61f5618 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -1,7 +1,8 @@ package com.tangem.features.markets.portfolio.impl.model +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @@ -17,42 +18,29 @@ import kotlinx.collections.immutable.toImmutableList internal class TokensPortfolioUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, - private val availableNetworks: List, + private val isAllAvailableNetworksAdded: Boolean, + private val bsConfig: TangemBottomSheetConfig, private val onAddClick: () -> Unit, - private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, -) : Converter, MyPortfolioUM.Tokens> { + private val onTokenItemClick: (index: Int, id: CryptoCurrency.ID) -> Unit, +) : Converter>, MyPortfolioUM.Tokens> { - override fun convert(value: Map): MyPortfolioUM.Tokens { + override fun convert(value: Map>): MyPortfolioUM.Tokens { return MyPortfolioUM.Tokens( - tokens = PortfolioTokenUMConverter(appCurrency, isBalanceHidden, onTokenItemClick) - .convertList(value.entries) + tokens = value + .flatMap { entry -> + entry.value.map { entry.key to it } + } + .mapIndexed { index, status -> + PortfolioTokenUMConverter( + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden, + onTokenItemClick = { onTokenItemClick(index, it.currency.id) }, + ).convert(status) + } .toImmutableList(), - buttonState = getAddButtonState( - availableWalletsWithStatuses = value, - availableNetworks = availableNetworks, - ), + buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available, + bsConfig = bsConfig, onAddClick = onAddClick, ) } - - private fun getAddButtonState( - availableWalletsWithStatuses: Map, - availableNetworks: List, - ): AddButtonState { - val networkIds = availableNetworks.map { it.networkId } - - val allNetworksAdded = availableWalletsWithStatuses - // User can add currencies only in multi-currency wallets - .filterKeys(UserWallet::isMultiCurrency) - .entries - .groupBy( - keySelector = { it.key }, - valueTransform = { it.value.currency.network.backendId }, - ) - .mapValues { it.value.toSet() } - // Each wallets contains all available networks? - .all { it.value.containsAll(networkIds) } - - return if (allNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available - } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 6f5c6f01db..8c3b6118fd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -53,7 +53,11 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { } } - AddToPortfolioBottomSheet(config = state.addToPortfolioBS) + when (state) { + is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.bsConfig) + is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.bsConfig) + else -> Unit + } } @Composable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 1f21b46f5b..02339b5257 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -1,6 +1,8 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @@ -14,19 +16,39 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider, val buttonState: AddButtonState, + val bsConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() { @@ -28,6 +22,7 @@ internal sealed class MyPortfolioUM { } data class AddFirstToken( + val bsConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() From ab4ed3efc6c6e79e402481603e5302da3bcb7c47 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 21:13:32 +0400 Subject: [PATCH 070/197] Updated on 2026-08-14 --- .../block/information/InformationBlock.kt | 4 +- .../ui/CustomTokenSelectorContent.kt | 84 +++++++++---------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 4565c520d9..25bac7feca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R @@ -32,12 +33,13 @@ fun InformationBlock( title: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12, + shape: Shape = TangemTheme.shapes.roundedCornersXMedium, action: (@Composable BoxScope.() -> Unit)? = null, content: (@Composable InformationBlockContentScope.() -> Unit)? = null, ) { Column( modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) + .clip(shape) .background(color = TangemTheme.colors.background.action), horizontalAlignment = Alignment.Start, ) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 67c77ff303..5984b55cf3 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.ChainRowUM @@ -34,7 +35,6 @@ import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath -import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.entity.item.DerivationPathUM import com.tangem.features.managetokens.impl.R @@ -46,7 +46,9 @@ internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier: val lastIndex = model.items.lastIndex LazyColumn( - modifier = modifier, + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + ), contentPadding = PaddingValues( bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, ), @@ -178,38 +180,47 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) @Composable private fun CustomDerivationButton(onClick: () -> Unit, modifier: Modifier = Modifier) { - // TODO: Implement in [REDACTED_JIRA] - Box( - modifier = modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size56) - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .background(color = TangemTheme.colors.background.primary) - .clickable(onClick = onClick) - .padding(all = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Text( - text = "Custom", - color = TangemTheme.colors.text.primary1, - ) - } + InformationBlock( + modifier = modifier.clickable(onClick = onClick), + title = { + Text( + text = stringResource(id = R.string.custom_token_custom_derivation), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + }, + content = { + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = stringResource(id = R.string.custom_token_custom_derivation_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + }, + ) } @Composable private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Modifier) { - // TODO: Implement in [REDACTED_JIRA] - Box( - modifier = modifier - .heightIn(TangemTheme.dimens.size56) - .padding(all = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Text( - text = "${model.networkName.resolveReference()}: ${model.value} ${if (model.isSelected) "<" else ""}", - color = TangemTheme.colors.text.primary1, - ) - } + InformationBlock( + modifier = modifier, + shape = RectangleShape, + title = { + Text( + text = model.networkName.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + }, + content = { + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = model.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + }, + ) } // region Preview @@ -229,19 +240,6 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - PreviewCustomTokenSelectorComponent(), - PreviewCustomTokenSelectorComponent( - params = CustomTokenSelectorComponent.Params.NetworkSelector( - userWalletId = UserWalletId(stringValue = "321"), - selectedNetwork = SelectedNetwork( - id = Network.ID(value = "0"), - name = stringReference(""), - derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), - canHandleTokens = false, - ), - onNetworkSelected = {}, - ), - ), PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = UserWalletId(stringValue = "321"), From 80e0e5988e6aac57e339a6ec1da2e0e500e632d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 21:14:07 +0400 Subject: [PATCH 071/197] Updated on 2026-08-14 --- .../CustomTokenDerivationInputComponent.kt | 16 +++++ ...viewCustomTokenDerivationInputComponent.kt | 34 +++++++++ .../customtoken/CustomDerivationInputUM.kt | 12 ++++ .../ui/dialog/CustomDerivationInputDialog.kt | 71 +++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt new file mode 100644 index 0000000000..1530c3d205 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.managetokens.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.domain.wallets.models.UserWalletId + +internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent { + + data class Params( + val userWalletId: UserWalletId, + val onConfirm: (String) -> Unit, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt new file mode 100644 index 0000000000..a91caf00e0 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenDerivationInputComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent +import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM +import com.tangem.features.managetokens.ui.dialog.CustomDerivationInputDialog + +internal class PreviewCustomTokenDerivationInputComponent( + private val value: String = "", + private val error: String? = null, +) : CustomTokenDerivationInputComponent { + + override fun dismiss() { + /* no-op */ + } + + @Composable + override fun Dialog() { + val model = CustomDerivationInputUM( + value = TextFieldValue(value), + error = error?.let(::stringReference), + updateValue = {}, + isConfirmEnabled = false, + onConfirm = {}, + ) + + CustomDerivationInputDialog( + model = model, + onDismiss = ::dismiss, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt new file mode 100644 index 0000000000..0a0e28431a --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomDerivationInputUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.managetokens.entity.customtoken + +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.extensions.TextReference + +internal data class CustomDerivationInputUM( + val value: TextFieldValue, + val error: TextReference? = null, + val updateValue: (value: TextFieldValue) -> Unit, + val isConfirmEnabled: Boolean, + val onConfirm: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt new file mode 100644 index 0000000000..be41894576 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CustomDerivationInputDialog.kt @@ -0,0 +1,71 @@ +package com.tangem.features.managetokens.ui.dialog + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.AdditionalTextInputDialogUM +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent +import com.tangem.features.managetokens.component.preview.PreviewCustomTokenDerivationInputComponent +import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun CustomDerivationInputDialog(model: CustomDerivationInputUM, onDismiss: () -> Unit) { + val value by rememberUpdatedState(newValue = model.value) + + TextInputDialog( + title = stringResource(id = R.string.custom_token_custom_derivation_title), + fieldValue = value, + confirmButton = DialogButtonUM( + title = stringResource(id = R.string.common_ok), + enabled = model.isConfirmEnabled, + onClick = model.onConfirm, + ), + dismissButton = DialogButtonUM( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + onValueChange = model.updateValue, + textFieldParams = AdditionalTextInputDialogUM( + label = model.error?.resolveReference() ?: stringResource(id = R.string.custom_token_derivation_path), + isError = model.error != null, + ), + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CustomDerivationInputDialog( + @PreviewParameter(ComponentPreviewProvider::class) component: CustomTokenDerivationInputComponent, +) { + TangemThemePreview { + component.Dialog() + } +} + +private class ComponentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewCustomTokenDerivationInputComponent(), + PreviewCustomTokenDerivationInputComponent( + value = "m/44'/60'/0'/0/0", + ), + PreviewCustomTokenDerivationInputComponent( + value = "m/44'/60'/0'/0/0", + error = "Invalid derivation path", + ), + ) +} +// endregion Preview \ No newline at end of file From c10f747de8d8ec3247d86a47f301822a89e242c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 18:05:46 +0500 Subject: [PATCH 072/197] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../common/ui/notifications/NotificationUM.kt | 251 ++++++++++++ .../ui/notifications/NotificationsFactory.kt | 362 ++++++++++++++++++ .../domain/transaction/error/GetFeeError.kt | 0 .../ui/block/NotificationsBlock.kt | 12 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 + 6 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt rename domain/transaction/{src/main/java => models/src/main/kotlin}/com/tangem/domain/transaction/error/GetFeeError.kt (100%) diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 9380922c04..315574334e 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt new file mode 100644 index 0000000000..20dad94e6b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -0,0 +1,251 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.ui.R +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import java.math.BigDecimal + +sealed class NotificationUM(val config: NotificationConfig) { + + open class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonState, + onCloseClick = onCloseClick, + ), + ) { + + data object TotalExceedsBalance : Error( + title = resourceReference(R.string.send_notification_exceed_balance_title), + subtitle = resourceReference(R.string.send_notification_exceed_balance_text), + ) + + data object InvalidAmount : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference(R.string.send_notification_invalid_amount_text), + ) + + data class MinimumAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference( + R.string.send_notification_invalid_minimum_amount_text, + wrappedList(amount, amount), + ), + ) + + data class TransactionLimitError( + val cryptoCurrency: String, + val utxoLimit: String, + val amountLimit: String, + val onConfirmClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.send_notification_transaction_limit_title), + subtitle = resourceReference( + R.string.send_notification_transaction_limit_text, + wrappedList(cryptoCurrency, utxoLimit, amountLimit), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)), + onClick = onConfirmClick, + ), + ) + + data class ExceedsBalance( + val networkIconId: Int, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val mergeFeeNetworkName: Boolean = false, + val onClick: (() -> Unit)? = null, + ) : Error( + title = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_title, + wrappedList(feeName), + ), + subtitle = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), + ), + iconResId = networkIconId, + buttonState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference( + R.string.common_buy_currency, + wrappedList( + if (mergeFeeNetworkName) { + "$currencyName ($feeSymbol)" + } else { + feeName + }, + ), + ), + onClick = onClick, + ) + }, + ) + + data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error( + title = resourceReference(R.string.send_notification_existential_deposit_title), + subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), + onClick = onConfirmClick, + ), + ) + + data class ReserveAmount(val amount: String) : Error( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + wrappedList(amount), + ), + subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), + ) + } + + open class Warning( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.img_attention_20, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ), + ) { + data class HighFeeError( + val currencyName: String, + val amount: String, + val onConfirmClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), + onClick = onConfirmClick, + ), + onCloseClick = onCloseClick, + ) + + data object FeeTooLow : Warning( + title = resourceReference(id = R.string.send_notification_transaction_delay_title), + subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), + ) + + data class TooHigh( + val value: String, + ) : Warning( + title = resourceReference(id = R.string.send_notification_fee_too_high_title), + subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), + ) + + data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onRefresh, + ), + ) + + data class TronAccountNotActivated(val tokenName: String) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference( + R.string.send_tron_account_activation_error, + wrappedList(tokenName), + ), + ) + + data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( + title = resourceReference(R.string.send_network_fee_warning_title), + subtitle = resourceReference( + R.string.common_network_fee_warning_content, + wrappedList(cryptoAmount, fiatAmount), + ), + ) + } + + sealed interface Cardano { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + ) + + data object InsufficientBalanceToTransferCoin : Error( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + ) + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Error( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + ) + } + + sealed interface Koinos { + data class InsufficientRecoverableMana( + val mana: BigDecimal, + val maxMana: BigDecimal, + ) : Error( + title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title), + subtitle = resourceReference( + R.string.koinos_insufficient_mana_to_send_koin_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()), + BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()), + ), + ), + ) + + data object InsufficientBalance : Error( + title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title), + subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description), + ) + + data class ManaExceedsBalance( + val availableKoinForTransfer: BigDecimal, + val onReduceClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title), + subtitle = resourceReference( + R.string.koinos_mana_exceeds_koin_balance_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmount( + availableKoinForTransfer, + Blockchain.Koinos.currency, + Blockchain.Koinos.decimals(), + ), + ), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)), + onClick = onReduceClick, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt new file mode 100644 index 0000000000..526007b989 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -0,0 +1,362 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.error.GetFeeError +import java.math.BigDecimal + +@Suppress("LargeClass") +object NotificationsFactory { + + fun MutableList.addFeeUnreachableNotification( + feeError: GetFeeError, + tokenName: String, + onReload: () -> Unit, + ) { + when (feeError) { + is GetFeeError.BlockchainErrors.TronActivationError -> add( + NotificationUM.Warning.TronAccountNotActivated(tokenName), + ) + is GetFeeError.DataError, + is GetFeeError.UnknownError, + -> add( + NotificationUM.Warning.NetworkFeeUnreachable(onReload), + ) + else -> { + /* do nothing */ + } + } + } + + fun MutableList.addExceedBalanceNotification( + feeAmount: BigDecimal, + sendingAmount: BigDecimal, + isSubtractionAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + + if (!isSubtractionAvailable) return + + val showNotification = sendingAmount + feeAmount > balance + if (showNotification) { + add(NotificationUM.Error.TotalExceedsBalance) + } + } + + fun MutableList.addReserveAmountErrorNotification( + reserveAmount: BigDecimal?, + sendingAmount: BigDecimal, + cryptoCurrency: CryptoCurrency, + isAccountFunded: Boolean, + ) { + if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) { + add( + NotificationUM.Error.ReserveAmount( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } + + fun MutableList.addTransactionLimitErrorNotification( + utxoLimit: UtxoAmountLimit?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + if (utxoLimit != null) { + add( + NotificationUM.Error.TransactionLimitError( + cryptoCurrency = cryptoCurrency.name, + utxoLimit = utxoLimit.maxLimit.toPlainString(), + amountLimit = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = utxoLimit.maxAmount, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + utxoLimit.maxAmount, + NotificationUM.Error.TransactionLimitError::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addExistentialWarningNotification( + existentialDeposit: BigDecimal?, + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + onReduceClick: ( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) -> Unit, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val balance = cryptoCurrencyStatus.value.amount ?: return + val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { + feeAmount + } else { + receivedAmount + } + val diff = balance.minus(spendingAmount) + if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) { + add( + NotificationUM.Error.ExistentialDeposit( + deposit = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = existentialDeposit, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + existentialDeposit, + existentialDeposit.minus(diff), + NotificationUM.Error.ExistentialDeposit::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addFeeCoverageNotification( + isFeeCoverage: Boolean, + amountField: AmountFieldModel, + sendingValue: BigDecimal, + appCurrency: AppCurrency, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val amountValue = amountField.cryptoAmount.value ?: return + + val cryptoDiff = amountValue.minus(sendingValue) + if (isFeeCoverage) { + add( + NotificationUM.Warning.FeeCoverageNotification( + cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = cryptoDiff, + cryptoCurrency = cryptoCurrency, + ), + fiatAmount = getFiatString( + value = cryptoDiff, + rate = fiatRate, + appCurrency = appCurrency, + ), + ), + ) + } + } + + fun MutableList.addDustWarningNotification( + dustValue: BigDecimal?, + feeValue: BigDecimal, + sendingAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ) { + if (dustValue == null) return + val isExceedsLimit = checkDustLimits( + feeAmount = feeValue, + receivedAmount = sendingAmount, + dustValue = dustValue, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + ) + if (isExceedsLimit) { + add( + NotificationUM.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + ), + ) + } + } + + fun MutableList.addExceedsBalanceNotification( + cryptoCurrencyWarning: CryptoCurrencyWarning?, + cryptoCurrencyStatus: CryptoCurrencyStatus, + shouldMergeFeeNetworkName: Boolean, + onClick: (CryptoCurrency) -> Unit, + onAnalyticsEvent: (CryptoCurrency) -> Unit, + ) { + when (cryptoCurrencyWarning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId, + networkName = cryptoCurrencyWarning.coinCurrency.name, + currencyName = cryptoCurrencyStatus.currency.name, + feeName = cryptoCurrencyWarning.coinCurrency.name, + feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + onClick(cryptoCurrencyWarning.coinCurrency) + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyStatus.currency) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = cryptoCurrencyWarning.feeCurrency + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, + currencyName = cryptoCurrencyWarning.currency.name, + feeName = cryptoCurrencyWarning.feeCurrencyName, + feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol, + networkName = cryptoCurrencyWarning.networkName, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + currency?.let { + onClick(currency) + } + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyWarning.currency) + } + else -> Unit + } + } + + fun MutableList.addValidateTransactionNotifications( + dustValue: BigDecimal, + fee: Fee?, + validationError: Throwable?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (validationError) { + is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError( + error = validationError, + sendingCurrency = cryptoCurrency, + dustValue = dustValue, + ) + is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError( + error = validationError, + onReduceClick = onReduceClick, + ) + null -> (fee as? Fee.CardanoToken)?.let { + add( + NotificationUM.Cardano.MinAdaValueCharged( + tokenName = cryptoCurrency.name, + minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals), + ), + ) + } + else -> return + } + } + + private fun MutableList.addCardanoTransactionValidationError( + error: BlockchainSdkError.Cardano, + sendingCurrency: CryptoCurrency, + dustValue: BigDecimal?, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (sendingCurrency) { + is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name) + } + }.let(::add) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + dustValue?.let { + add( + NotificationUM.Error.MinimumAmountError( + amount = it.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + } + } + } + + private fun MutableList.addKoinosTransactionValidationError( + error: BlockchainSdkError.Koinos, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (error) { + is BlockchainSdkError.Koinos.InsufficientBalance -> { + add(NotificationUM.Koinos.InsufficientBalance) + } + is BlockchainSdkError.Koinos.InsufficientMana -> { + add( + NotificationUM.Koinos.InsufficientRecoverableMana( + mana = error.manaBalance ?: BigDecimal.ZERO, + maxMana = error.maxMana ?: BigDecimal.ZERO, + ), + ) + } + is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> { + add( + NotificationUM.Koinos.ManaExceedsBalance( + availableKoinForTransfer = error.availableKoinForTransfer, + onReduceClick = { + onReduceClick( + error.availableKoinForTransfer, + NotificationUM.Koinos.InsufficientRecoverableMana::class.java, + ) + }, + ), + ) + } + else -> {} + } + } + + private fun checkDustLimits( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + dustValue: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + val change = when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + balance - (feeAmount + receivedAmount) + } + is CryptoCurrency.Token -> { + val balance = feeCurrencyStatus?.value?.amount ?: BigDecimal.ZERO + balance - feeAmount + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + return receivedAmount < dustValue || isChangeLowerThanDust + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt similarity index 100% rename from domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt rename to domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index a9dcad67cf..dca4eefe62 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.CardWithIcon import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference @@ -13,7 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingNotification import kotlinx.collections.immutable.ImmutableList @Composable -internal fun NotificationsBlock(notifications: ImmutableList) { +internal fun NotificationsBlock(notifications: ImmutableList) { notifications.forEach { notification -> key(notification) { if (notification is StakingNotification.Warning.TransactionInProgress) { @@ -32,8 +33,15 @@ internal fun NotificationsBlock(notifications: ImmutableList TangemTheme.colors.icon.warning is StakingNotification.Warning -> TangemTheme.colors.icon.accent + + is NotificationUM.Error.ExceedsBalance, + is NotificationUM.Warning, + -> null + + is StakingNotification.Error, + is NotificationUM.Error, + -> TangemTheme.colors.icon.warning }, ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 830677086d..89f796a6b7 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -56,6 +56,11 @@ object BlockchainUtils { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } + fun isArbitrum(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Arbitrum + } + data class BlockchainInfo( val blockchainId: String, val name: String, From 52ea08c979029df6bd3ed651e225a8313ea6e272 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 18:08:43 +0500 Subject: [PATCH 073/197] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 6 + .../common/ui/feeScreen/FeeCalculation.kt | 86 ++++++++ .../model/warnings/CryptoCurrencyCheck.kt | 11 ++ .../domain/tokens/GetCurrencyCheckUseCase.kt | 41 ++++ .../staking/impl/di/StakingRouterModule.kt | 6 +- .../impl/navigation/DefaultStakingRouter.kt | 18 ++ .../impl/navigation/InnerStakingRouter.kt | 4 + .../AddStakingNotificationsTransformer.kt | 186 ++++++++++++++++++ .../amount/AmountReduceByStateTransformer.kt | 19 ++ .../amount/AmountReduceToStateTransformer.kt | 18 ++ 10 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 078e47c07e..5169a4b83a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -436,4 +436,10 @@ internal object TokensDomainModule { fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase { return CheckHasLinkedTokensUseCase(currenciesRepository) } + + @Provides + @Singleton + fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase { + return GetCurrencyCheckUseCase(currencyChecksRepository) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt new file mode 100644 index 0000000000..d1c3719c0a --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt @@ -0,0 +1,86 @@ +package com.tangem.common.ui.feeScreen + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Check and calculates subtracted amount + */ +fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, +): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } +} + +/** + * Checks if sending amount with fee is greater than balance + */ +fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, +): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue +} + +/** + * Check if custom fee is too low + */ +fun checkIfFeeTooLow(fee: TransactionFee, customValue: BigDecimal, isCustomSelected: Boolean): Boolean { + val multipleFees = fee as? TransactionFee.Choosable ?: return false + val minimumValue = multipleFees.minimum.amount.value ?: return false + + return isCustomSelected && minimumValue > customValue +} + +/** + * Check if custom fee is too high + */ +fun checkIfFeeTooHigh( + fee: TransactionFee, + customValue: BigDecimal, + isCustomSelected: Boolean, + onShow: (String) -> Unit, +): Boolean { + val multipleFees = fee as? TransactionFee.Choosable ?: return false + val highValue = multipleFees.priority.amount.value ?: return false + + val diff = customValue / highValue + val isShow = isCustomSelected && diff > FEE_MAX_DIFF + if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP)) + return isShow +} + +/** + * Checks if fee exceeds fee paid currency balance + */ +fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { + return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance +} + +private val FEE_MAX_DIFF = BigDecimal("5") +private const val ZERO_DECIMALS = 0 \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt new file mode 100644 index 0000000000..04018ed4aa --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens.model.warnings + +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import java.math.BigDecimal + +data class CryptoCurrencyCheck( + val dustValue: BigDecimal?, + val reserveAmount: BigDecimal?, + val existentialDeposit: BigDecimal?, + val utxoAmountLimit: UtxoAmountLimit?, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt new file mode 100644 index 0000000000..4e74a21591 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetCurrencyCheckUseCase( + private val currencyChecksRepository: CurrencyChecksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + amount: BigDecimal?, + fee: BigDecimal?, + ): CryptoCurrencyCheck { + val network = currencyStatus.currency.network + val dustValue = currencyChecksRepository.getDustValue(userWalletId, network) + val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network) + val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network) + val utxoAmountLimit = if (amount != null && fee != null) { + currencyChecksRepository.checkUtxoAmountLimit( + userWalletId = userWalletId, + network = network, + amount = amount, + fee = fee, + ) + } else { + null + } + + return CryptoCurrencyCheck( + dustValue = dustValue, + reserveAmount = reserveAmount, + existentialDeposit = existentialDeposit, + utxoAmountLimit = utxoAmountLimit, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt index c64b0dd4f2..1e01cf0b3d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt @@ -1,8 +1,9 @@ package com.tangem.features.staking.impl.di +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,9 +19,10 @@ internal object StakingRouterModule { @Provides @ActivityScoped - fun provideStakingRouter(urlOpener: UrlOpener): StakingRouter { + fun provideStakingRouter(urlOpener: UrlOpener, router: AppRouter): StakingRouter { return DefaultStakingRouter( urlOpener = urlOpener, + router = router, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 464d28694e..6acad0658b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,15 +1,33 @@ package com.tangem.features.staking.impl.navigation import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.impl.presentation.StakingFragment internal class DefaultStakingRouter( private val urlOpener: UrlOpener, + private val router: AppRouter, ) : InnerStakingRouter { override fun getEntryFragment(): Fragment = StakingFragment.create() override fun openUrl(url: String) { urlOpener.openUrl(url) } + + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { + router.pop { isSuccess -> + if (isSuccess) { + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index d4197489fb..e1fec4bab1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -1,8 +1,12 @@ package com.tangem.features.staking.impl.navigation +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.navigation.StakingRouter interface InnerStakingRouter : StakingRouter { fun openUrl(url: String) + + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt new file mode 100644 index 0000000000..2776bf6869 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -0,0 +1,186 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.feeScreen.checkAndCalculateSubtractedAmount +import com.tangem.common.ui.feeScreen.checkFeeCoverage +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class AddStakingNotificationsTransformer( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + private val currencyWarning: CryptoCurrencyWarning?, + private val validatorError: Throwable?, + private val feeError: GetFeeError?, + private val currencyCheck: CryptoCurrencyCheck, + private val isSubtractAvailable: Boolean, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val balance = cryptoCurrencyStatus.value.amount.orZero() + + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val amountState = prevState.amountState as? AmountState.Data ?: return prevState + val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState + + val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() + val feeValue = feeState.fee?.amount?.value.orZero() + val reduceAmountBy = confirmationState.reduceAmountBy.orZero() + + val isFeeCoverage = checkFeeCoverage( + amountValue = amountValue, + feeValue = feeValue, + balance = balance, + isSubtractAvailable = isSubtractAvailable, + reduceAmountBy = reduceAmountBy, + ) + val sendingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + val notifications = buildList { + // errors + addErrorNotifications( + prevState = prevState, + feeError = feeError, + sendingAmount = sendingAmount, + onReload = { prevState.clickIntents.loadFee(confirmationState.pendingActions) }, + feeValue = feeValue, + ) + // warnings + addWarningNotifications( + prevState = prevState, + amountState = amountState, + feeState = feeState, + sendingAmount = sendingAmount, + isFeeCoverage = isFeeCoverage, + ) + + addAll(confirmationState.notifications) + }.toImmutableList() + + return prevState.copy( + confirmationState = confirmationState.copy( + notifications = notifications.toImmutableList(), + ), + ) + } + + private fun MutableList.addErrorNotifications( + prevState: StakingUiState, + onReload: () -> Unit, + feeError: GetFeeError?, + sendingAmount: BigDecimal, + feeValue: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + val network = cryptoCurrency.network + + if (feeError != null) { + addFeeUnreachableNotification( + feeError = feeError, + tokenName = cryptoCurrencyStatusProvider().currency.name, + onReload = onReload, + ) + } + addExceedBalanceNotification( + feeAmount = feeValue, + sendingAmount = sendingAmount, + isSubtractionAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + addExceedsBalanceNotification( + cryptoCurrencyWarning = currencyWarning, + cryptoCurrencyStatus = cryptoCurrencyStatus, + shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), + onClick = prevState.clickIntents::openTokenDetails, + onAnalyticsEvent = { /* [REDACTED_TODO_COMMENT] */ }, + ) + if (!BlockchainUtils.isCardano(network.id.value)) { + addDustWarningNotification( + dustValue = currencyCheck.dustValue, + feeValue = feeValue, + sendingAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + addTransactionLimitErrorNotification( + utxoLimit = currencyCheck.utxoAmountLimit, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + addReserveAmountErrorNotification( + reserveAmount = currencyCheck.reserveAmount, + sendingAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + isAccountFunded = false, + ) + } + + private fun MutableList.addWarningNotifications( + prevState: StakingUiState, + amountState: AmountState.Data, + feeState: FeeState.Content, + sendingAmount: BigDecimal, + isFeeCoverage: Boolean, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + addExistentialWarningNotification( + existentialDeposit = currencyCheck.existentialDeposit, + feeAmount = feeState.fee?.amount?.value.orZero(), + receivedAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + onReduceClick = prevState.clickIntents::onAmountReduceByClick, + ) + addFeeCoverageNotification( + isFeeCoverage = isFeeCoverage, + amountField = amountState.amountTextField, + sendingValue = sendingAmount, + appCurrency = appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + + // blockchain specific + addValidateTransactionNotifications( + dustValue = currencyCheck.dustValue.orZero(), + fee = feeState.fee, + validationError = validatorError, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt new file mode 100644 index 0000000000..91468a21bd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountReduceByStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: ReduceByData, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceByTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt new file mode 100644 index 0000000000..b826aae59e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +internal class AmountReduceToStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: BigDecimal, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceToTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file From 33e5b99d978d76c123341d669aecdcb79741fb7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 19:14:03 +0500 Subject: [PATCH 074/197] Updated on 2026-08-14 --- .../common/ui/feeScreen/FeeCalculation.kt | 86 ------- .../presentation/state/StakingNotification.kt | 43 ++-- .../impl/presentation/state/StakingUiState.kt | 5 +- .../ConfirmationStatePreviewData.kt | 1 + .../state/stub/StakingClickIntentsStub.kt | 17 ++ .../AddStakingErrorTransformer.kt | 17 +- .../AddStakingNotificationsTransformer.kt | 25 +- ...issStakingNotificationsStateTransformer.kt | 25 ++ .../SetConfirmationStateLoadingTransformer.kt | 4 +- .../SetInitialDataStateTransformer.kt | 1 + .../state/utils/FeeCalculation.kt | 44 ++++ .../viewmodel/StakingClickIntents.kt | 18 ++ .../viewmodel/StakingViewModel.kt | 239 ++++++++++++------ 13 files changed, 327 insertions(+), 198 deletions(-) delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt deleted file mode 100644 index d1c3719c0a..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.common.ui.feeScreen - -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import java.math.BigDecimal -import java.math.RoundingMode - -/** - * Check and calculates subtracted amount - */ -fun checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable: Boolean, - cryptoCurrencyStatus: CryptoCurrencyStatus, - amountValue: BigDecimal, - feeValue: BigDecimal, - reduceAmountBy: BigDecimal, -): BigDecimal { - val balance = cryptoCurrencyStatus.value.amount ?: return amountValue - val isFeeCoverage = checkFeeCoverage( - isSubtractAvailable = isAmountSubtractAvailable, - balance = balance, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = reduceAmountBy, - ) - return if (isFeeCoverage) { - balance.minus(reduceAmountBy).minus(feeValue) - } else { - amountValue - } -} - -/** - * Checks if sending amount with fee is greater than balance - */ -fun checkFeeCoverage( - isSubtractAvailable: Boolean, - balance: BigDecimal, - amountValue: BigDecimal, - feeValue: BigDecimal, - reduceAmountBy: BigDecimal?, -): Boolean { - if (!isSubtractAvailable) return false - val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) - return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue -} - -/** - * Check if custom fee is too low - */ -fun checkIfFeeTooLow(fee: TransactionFee, customValue: BigDecimal, isCustomSelected: Boolean): Boolean { - val multipleFees = fee as? TransactionFee.Choosable ?: return false - val minimumValue = multipleFees.minimum.amount.value ?: return false - - return isCustomSelected && minimumValue > customValue -} - -/** - * Check if custom fee is too high - */ -fun checkIfFeeTooHigh( - fee: TransactionFee, - customValue: BigDecimal, - isCustomSelected: Boolean, - onShow: (String) -> Unit, -): Boolean { - val multipleFees = fee as? TransactionFee.Choosable ?: return false - val highValue = multipleFees.priority.amount.value ?: return false - - val diff = customValue / highValue - val isShow = isCustomSelected && diff > FEE_MAX_DIFF - if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP)) - return isShow -} - -/** - * Checks if fee exceeds fee paid currency balance - */ -fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { - return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance -} - -private val FEE_MAX_DIFF = BigDecimal("5") -private const val ZERO_DECIMALS = 0 \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index f9da881678..682b777b25 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -1,61 +1,56 @@ package com.tangem.features.staking.impl.presentation.state +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.* import com.tangem.features.staking.impl.R -internal sealed class StakingNotification(val config: NotificationConfig) { +internal object StakingNotification { sealed class Error( title: TextReference, subtitle: TextReference, - iconResId: Int = R.drawable.ic_alert_24, buttonState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, - ) : StakingNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = iconResId, - buttonsState = buttonState, - onCloseClick = onCloseClick, - ), + ) : NotificationUM.Error( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_24, + buttonState = buttonState, + onCloseClick = onCloseClick, ) { - data class StakedPositionNotFoundError(val message: String) : Error( + data class StakedPositionNotFoundError(val message: String) : StakingNotification.Error( title = stringReference(message), subtitle = stringReference(message), ) - data class Common(val subtitle: TextReference) : Error( + data class Common(val subtitle: TextReference) : StakingNotification.Error( title = resourceReference(R.string.common_error), subtitle = subtitle, ) } - sealed class Warning( title: TextReference, subtitle: TextReference, buttonsState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, - ) : StakingNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = buttonsState, - onCloseClick = onCloseClick, - ), + ) : NotificationUM.Warning( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = buttonsState, + onCloseClick = onCloseClick, ) { data class EarnRewards( val subtitleText: TextReference, - ) : Warning( + ) : StakingNotification.Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), subtitle = subtitleText, ) data class Unstake( val cooldownPeriodDays: Int, - ) : Warning( + ) : StakingNotification.Warning( title = resourceReference(R.string.common_unstake), subtitle = resourceReference( R.string.staking_notification_unstake_text, @@ -72,6 +67,6 @@ internal sealed class StakingNotification(val config: NotificationConfig) { data class TransactionInProgress( val title: TextReference, val description: TextReference, - ) : Warning(title = title, subtitle = description) + ) : StakingNotification.Warning(title = title, subtitle = description) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 55e576c90f..d06f0e1507 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent @@ -13,6 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal /** * Ui states of the staking screen @@ -84,10 +86,11 @@ internal sealed class StakingStates { val feeState: FeeState, val validatorState: ValidatorState, val pendingAction: PendingAction?, - val notifications: ImmutableList, + val notifications: ImmutableList, val footerText: String, val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, + val reduceAmountBy: BigDecimal?, ) : ConfirmationState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index ef4234d37c..25879ce343 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -89,5 +89,6 @@ internal object ConfirmationStatePreviewData { transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = false, + reduceAmountBy = null, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 8876bdabdb..b2edad1892 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,11 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.stub +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import java.math.BigDecimal @Suppress("TooManyFunctions") internal object StakingClickIntentsStub : StakingClickIntents { @@ -49,4 +52,18 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onFailedTxEmailClick(errorMessage: String) {} override fun onActiveStake(activeStake: BalanceState) {} + + override fun getFee(pendingAction: PendingAction?) {} + + override fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) {} + + override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) {} + + override fun onNotificationCancel(notification: Class) {} + + override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt index 69278afeb6..5acae90ea3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt @@ -1,28 +1,37 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList internal class AddStakingErrorTransformer( - private val error: StakingError, + private val error: StakingError? = null, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val notifications = buildList { + addAll(confirmationState.notifications) + error?.let { add(convertToNotification(it)) } + }.toPersistentList() + return prevState.copy( confirmationState = confirmationState.copy( - notifications = (confirmationState.notifications + convertToNotification(error)).toPersistentList(), + notifications = notifications, feeState = FeeState.Error, ), ) } - private fun convertToNotification(error: StakingError): StakingNotification { + private fun convertToNotification(error: StakingError): NotificationUM { return when (error) { is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError( message = error.toString(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 2776bf6869..fd4fb47e43 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -1,8 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.feeScreen.checkAndCalculateSubtractedAmount -import com.tangem.common.ui.feeScreen.checkFeeCoverage import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification @@ -14,13 +12,17 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountE import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount +import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero @@ -41,17 +43,17 @@ internal class AddStakingNotificationsTransformer( ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val appCurrency = appCurrencyProvider() val balance = cryptoCurrencyStatus.value.amount.orZero() val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState val amountState = prevState.amountState as? AmountState.Data ?: return prevState - val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState + val feeState = confirmationState.feeState as? FeeState.Content val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() - val feeValue = feeState.fee?.amount?.value.orZero() + val feeValue = feeState?.fee?.amount?.value.orZero() val reduceAmountBy = confirmationState.reduceAmountBy.orZero() + val isEnterAction = prevState.actionType == StakingActionCommonType.ENTER val isFeeCoverage = checkFeeCoverage( amountValue = amountValue, feeValue = feeValue, @@ -61,7 +63,7 @@ internal class AddStakingNotificationsTransformer( ) val sendingAmount = checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isSubtractAvailable, - cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, reduceAmountBy = reduceAmountBy, @@ -73,7 +75,7 @@ internal class AddStakingNotificationsTransformer( prevState = prevState, feeError = feeError, sendingAmount = sendingAmount, - onReload = { prevState.clickIntents.loadFee(confirmationState.pendingActions) }, + onReload = { prevState.clickIntents.getFee(confirmationState.pendingAction) }, feeValue = feeValue, ) // warnings @@ -82,7 +84,7 @@ internal class AddStakingNotificationsTransformer( amountState = amountState, feeState = feeState, sendingAmount = sendingAmount, - isFeeCoverage = isFeeCoverage, + isFeeCoverage = isFeeCoverage && isEnterAction, ) addAll(confirmationState.notifications) @@ -91,6 +93,7 @@ internal class AddStakingNotificationsTransformer( return prevState.copy( confirmationState = confirmationState.copy( notifications = notifications.toImmutableList(), + isPrimaryButtonEnabled = !notifications.any { it is StakingNotification.Error }, ), ) } @@ -151,7 +154,7 @@ internal class AddStakingNotificationsTransformer( private fun MutableList.addWarningNotifications( prevState: StakingUiState, amountState: AmountState.Data, - feeState: FeeState.Content, + feeState: FeeState.Content?, sendingAmount: BigDecimal, isFeeCoverage: Boolean, ) { @@ -161,7 +164,7 @@ internal class AddStakingNotificationsTransformer( addExistentialWarningNotification( existentialDeposit = currencyCheck.existentialDeposit, - feeAmount = feeState.fee?.amount?.value.orZero(), + feeAmount = feeState?.fee?.amount?.value.orZero(), receivedAmount = sendingAmount, cryptoCurrencyStatus = cryptoCurrencyStatus, onReduceClick = prevState.clickIntents::onAmountReduceByClick, @@ -177,7 +180,7 @@ internal class AddStakingNotificationsTransformer( // blockchain specific addValidateTransactionNotifications( dustValue = currencyCheck.dustValue.orZero(), - fee = feeState.fee, + fee = feeState?.fee, validationError = validatorError, cryptoCurrency = cryptoCurrency, onReduceClick = prevState.clickIntents::onAmountReduceToClick, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt new file mode 100644 index 0000000000..a5fc4d9bc5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class DismissStakingNotificationsStateTransformer( + private val notification: Class, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val updatedNotifications = confirmationState?.notifications + ?.filterNot { it::class == notification }?.toPersistentList() + ?: persistentListOf() + + return prevState.copy( + confirmationState = confirmationState?.copy( + notifications = updatedNotifications, + ) ?: StakingStates.ConfirmationState.Empty(), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 9e35b20442..e158860e89 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.Yield @@ -34,11 +35,12 @@ internal class SetConfirmationStateLoadingTransformer( transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = false, + reduceAmountBy = null, ), ) } - private fun getNotifications(prevState: StakingUiState): ImmutableList { + private fun getNotifications(prevState: StakingUiState): ImmutableList { return persistentListOf( if (prevState.actionType == StakingActionCommonType.EXIT) { StakingNotification.Warning.Unstake( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 19f1c44544..7d873f5da1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -214,6 +214,7 @@ internal class SetInitialDataStateTransformer( transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = isApprovalNeeded, + reduceAmountBy = null, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt new file mode 100644 index 0000000000..21e95ed465 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt @@ -0,0 +1,44 @@ +package com.tangem.features.staking.impl.presentation.state.utils + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import java.math.BigDecimal + +/** + * Check and calculates subtracted amount + */ +internal fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, +): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } +} + +/** + * Checks if sending amount with fee is greater than balance + */ +internal fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, +): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index ad7b9ff1c6..11910f51df 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,12 +1,16 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType +import java.math.BigDecimal +@Suppress("TooManyFunctions") internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() @@ -21,6 +25,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onInfoClick(infoType: InfoType) + fun getFee(pendingAction: PendingAction?) + override fun onAmountNext() = onNextClick(actionType = null) fun openValidators() @@ -35,9 +41,21 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onApprovalClick() + fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) + + fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) + + fun onNotificationCancel(notification: Class) + fun onExploreClick() fun onShareClick() fun onFailedTxEmailClick(errorMessage: String) + + fun openTokenDetails(cryptoCurrency: CryptoCurrency) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 523380111a..1c18173837 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -12,7 +12,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager @@ -32,16 +34,15 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType -import com.tangem.domain.tokens.FetchPendingTransactionsUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -50,10 +51,7 @@ import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.* import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalInProgressTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer @@ -103,6 +101,10 @@ internal class StakingViewModel @Inject constructor( private val feedbackManager: FeedbackManager, private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val validateTransactionUseCase: ValidateTransactionUseCase, + private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -140,6 +142,7 @@ internal class StakingViewModel @Inject constructor( ) private var stakingApproval: StakingApproval = StakingApproval.Empty + private var isAmountSubtractAvailable: Boolean = false private val allowanceTaskScheduler = SingleTaskScheduler() private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() @@ -171,6 +174,57 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onNextClick() } + override fun getFee(pendingAction: PendingAction?) { + viewModelScope.launch { + stateController.update( + SetConfirmationStateLoadingTransformer( + yield = yield, + ), + ) + val cryptoCurrencyValue = cryptoCurrencyStatus.value + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + + val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value + ?: error("No available address") + val validatorAddress = validatorState.chosenValidator.address + + val approval = stakingApproval as? StakingApproval.Needed + if (approval != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + + if (allowance < amount) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + ) + } else { + estimateGas( + pendingAction = pendingAction, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } else { + estimateGas( + pendingAction = pendingAction, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } + } + private fun handleOnNextConfirmationClick() { if (isAssentState()) { viewModelScope.launch { @@ -236,57 +290,6 @@ internal class StakingViewModel @Inject constructor( } } - private fun getFee(pendingAction: PendingAction?) { - viewModelScope.launch { - stateController.update( - SetConfirmationStateLoadingTransformer( - yield = yield, - ), - ) - val cryptoCurrencyValue = cryptoCurrencyStatus.value - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content - ?: error("No validator provided") - - val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided") - val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value - ?: error("No available address") - val validatorAddress = validatorState.chosenValidator.address - - val approval = stakingApproval as? StakingApproval.Needed - if (approval != null) { - val allowance = getAllowanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - - if (allowance < amount) { - getApproveFee( - amount = amount, - validatorAddress = validatorAddress, - ) - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } - } - } - private suspend fun estimateGas( pendingAction: PendingAction?, amount: BigDecimal, @@ -310,7 +313,6 @@ internal class StakingViewModel @Inject constructor( stateController.update(AddStakingErrorTransformer(it)) return } - stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -325,24 +327,30 @@ internal class StakingViewModel @Inject constructor( action = pendingAction, ), ) + updateNotifications() } private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) { - val approvalFee = getFeeUseCase( + getFeeUseCase( amount = amount, destination = validatorAddress, userWallet = userWallet, cryptoCurrency = cryptoCurrencyStatus.currency, - ).getOrElse { - return stakingEventFactory.createGenericErrorAlert(it.toString()) - } - - stateController.update( - SetConfirmationStateAssentApprovalTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = approvalFee, - ), + ).fold( + ifRight = { fee -> + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = fee, + ), + ) + updateNotifications() + }, + ifLeft = { + stateController.update(AddStakingErrorTransformer()) + updateNotifications(it) + }, ) } @@ -499,6 +507,85 @@ internal class StakingViewModel @Inject constructor( }.saveIn(approvalJobHolder) } + private fun updateNotifications(feeError: GetFeeError? = null) { + viewModelScope.launch { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + val feeState = confirmationState?.feeState as? FeeState.Content + val amountState = value.amountState as? AmountState.Data + + val amount = amountState?.amountTextField?.cryptoAmount?.value + val fee = feeState?.fee?.amount?.value + val currencyWarning = if (feeCryptoCurrencyStatus != null && fee != null) { + getBalanceNotEnoughForFeeWarningUseCase( + fee = fee, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, + ).getOrNull() + } else { + null + } + val validation = amount?.let { + validateTransactionUseCase( + userWalletId = userWalletId, + amount = amount.convertToSdkAmount(cryptoCurrencyStatus.currency), + fee = feeState?.fee, + memo = null, + destination = "", + network = cryptoCurrencyStatus.currency.network, + ).leftOrNull() + } + + val currencyStatus = getCurrencyCheckUseCase( + userWalletId = userWalletId, + currencyStatus = cryptoCurrencyStatus, + amount = amount, + fee = fee, + ) + stateController.update( + AddStakingNotificationsTransformer( + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + currencyWarning = currencyWarning, + validatorError = validation, + currencyCheck = currencyStatus, + isSubtractAvailable = isAmountSubtractAvailable, + feeError = feeError, + ), + ) + } + } + + override fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) { + AmountReduceByStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = AmountReduceByTransformer.ReduceByData( + reduceAmountBy = reduceAmountBy, + reduceAmountByDiff = reduceAmountByDiff, + ), + ) + onNotificationCancel(notification) + } + + override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) { + stateController.update( + AmountReduceToStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = reduceAmountTo, + ), + ) + onNotificationCancel(notification) + } + + override fun onNotificationCancel(notification: Class) { + stateController.update(DismissStakingNotificationsStateTransformer(notification)) + } + private fun awaitForAllowance(pendingAction: PendingAction?) { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( @@ -586,6 +673,10 @@ internal class StakingViewModel @Inject constructor( } } + override fun openTokenDetails(cryptoCurrency: CryptoCurrency) { + innerRouter.openTokenDetails(userWalletId, cryptoCurrency) + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -612,6 +703,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatus = it setupApprovalNeeded() + checkIfSubtractAvailable() stateController.update( transformer = SetInitialDataStateTransformer( @@ -779,6 +871,11 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } + private suspend fun checkIfSubtractAvailable() { + isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrencyStatus.currency) + .getOrElse { false } + } + private data class FullTransactionData( val stakeKitTransaction: StakingTransaction, val tangemTransaction: TransactionData.Compiled, From 4a25e38ccc6b72ef7a5cdddcbeb4668b5e2a2e4b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Aug 2024 18:25:34 +0400 Subject: [PATCH 075/197] Updated on 2026-08-14 --- .../com/tangem/core/decompose/model/Model.kt | 26 ++++++- .../java/com/tangem/utils/extensions/List.kt | 21 ++++-- .../component/AddCustomTokenComponent.kt | 1 + features/manage-tokens/impl/build.gradle.kts | 1 + .../component/CustomTokenFormComponent.kt | 1 + .../impl/DefaultAddCustomTokenComponent.kt | 6 ++ .../impl/DefaultManageTokensComponent.kt | 1 + .../entity/managetokens/ManageTokensUM.kt | 8 +++ .../model/CustomTokenFormModel.kt | 70 ++++++++++++++----- .../managetokens/model/ManageTokensModel.kt | 25 +++++++ .../managetokens/ui/ManageTokensScreen.kt | 11 ++- .../utils/list/ManageTokensUiManager.kt | 8 +-- 12 files changed, 151 insertions(+), 28 deletions(-) diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt index c845658265..b1337c72f9 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt @@ -46,14 +46,36 @@ abstract class Model : InstanceKeeper.Instance { progressFlow: MutableSharedFlow, dispatcher: CoroutineDispatcher = dispatchers.mainImmediate, crossinline block: suspend () -> Unit, + ): Job = resource( + acquire = { progressFlow.emit(true) }, + release = { progressFlow.emit(false) }, + dispatcher = dispatcher, + block = block, + ) + + /** + * Launches [block] in the model's scope and acquires a resource before executing the block and releases it after. + * + * @param acquire The block of code to acquire the resource. + * @param release The block of code to release the resource. + * @param dispatcher The [CoroutineDispatcher] to launch the coroutine. Default is [Dispatchers.Main.immediate]. + * @param block The block of code to execute. + * + * @return The [Job] of the launched coroutine. + * */ + protected inline fun resource( + crossinline acquire: suspend () -> Unit, + crossinline release: suspend () -> Unit, + dispatcher: CoroutineDispatcher = dispatchers.mainImmediate, + crossinline block: suspend () -> Unit, ): Job = modelScope.launch(dispatcher) { - progressFlow.emit(value = true) + acquire() try { block() } finally { withContext(NonCancellable) { - progressFlow.emit(value = false) + release() } } } diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt index e0fbdb5d90..86bdfe2e6e 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt @@ -44,11 +44,24 @@ inline fun MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boo */ inline fun List.addOrReplace(item: T, predicate: (T) -> Boolean): List { val mutableList = this.toMutableList() - val isReplaced = mutableList.replaceBy(item, predicate) - if (!isReplaced) { - mutableList.add(item) - } + mutableList.addOrReplace(item, predicate) return mutableList +} + +/** + * Adds the specified element to the mutable list or replaces an existing element. + * + * !!!This function is not thread-safe!!! + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + */ +inline fun MutableList.addOrReplace(item: T, predicate: (T) -> Boolean) { + val isReplaced = replaceBy(item, predicate) + + if (!isReplaced) { + add(item) + } } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt index 63a0f48e26..4539527ce5 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt @@ -9,6 +9,7 @@ interface AddCustomTokenComponent : ComposableBottomSheetComponent { data class Params( val userWalletId: UserWalletId, val onDismiss: () -> Unit, + val onCurrencyAdded: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index a8d50aa900..a506043ad2 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.manageTokens) + implementation(projects.domain.card) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index 3f6d401d30..e9fc3c448f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -16,6 +16,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { val formValues: CustomTokenFormValues, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, + val onCurrencyAdded: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 48e7a1b01f..3679e6f929 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -131,6 +131,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( formValues = config.formValues, onSelectNetworkClick = ::showNetworkSelector, onSelectDerivationPathClick = ::showDerivationPathSelector, + onCurrencyAdded = ::dismissAndNotify, ), ) } @@ -170,6 +171,11 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( navigation.replaceAll(config) } + private fun dismissAndNotify() { + dismiss() + params.onCurrencyAdded() + } + @AssistedFactory interface Factory : AddCustomTokenComponent.Factory { override fun create( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index 9d90bf8789..98df4a4346 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -62,6 +62,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( params = AddCustomTokenComponent.Params( userWalletId = config.userWalletId, onDismiss = model.bottomSheetNavigation::dismiss, + onCurrencyAdded = model::reloadList, ), ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index 33f1524452..8a0bf34154 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -2,6 +2,8 @@ package com.tangem.features.managetokens.entity.managetokens import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.features.managetokens.entity.item.CurrencyItemUM import kotlinx.collections.immutable.ImmutableList @@ -15,6 +17,7 @@ internal sealed class ManageTokensUM { abstract val topBar: ManageTokensTopBarUM abstract val search: SearchBarUM abstract val loadMore: () -> Boolean + abstract val scrollToTop: StateEvent data class ReadContent( override val popBack: () -> Unit, @@ -24,6 +27,7 @@ internal sealed class ManageTokensUM { override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, override val loadMore: () -> Boolean, + override val scrollToTop: StateEvent = consumedEvent(), ) : ManageTokensUM() data class ManageContent( @@ -34,6 +38,7 @@ internal sealed class ManageTokensUM { override val topBar: ManageTokensTopBarUM, override val search: SearchBarUM, override val loadMore: () -> Boolean, + override val scrollToTop: StateEvent = consumedEvent(), val saveChanges: () -> Unit, val hasChanges: Boolean, val isSavingInProgress: Boolean, @@ -46,6 +51,7 @@ internal sealed class ManageTokensUM { isInitialBatchLoading: Boolean = this.isInitialBatchLoading, isNextBatchLoading: Boolean = this.isNextBatchLoading, isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, + scrollToTop: StateEvent = this.scrollToTop, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -55,12 +61,14 @@ internal sealed class ManageTokensUM { isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, isSavingInProgress = isSavingInProgress, + scrollToTop = scrollToTop, ) is ReadContent -> copy( search = search, items = items, isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, + scrollToTop = scrollToTop, ) } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 0565adb15e..25fbf11d48 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.managetokens.model import androidx.compose.ui.res.stringResource +import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,7 +10,9 @@ import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM @@ -31,11 +34,14 @@ import javax.inject.Inject internal class CustomTokenFormModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val customCurrencyValidator: CustomCurrencyValidator, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, private val messageSender: UiMessageSender, paramsContainer: ParamsContainer, ) : Model() { private val params: CustomTokenFormComponent.Params = paramsContainer.require() + private var createdCurrency: CryptoCurrency? = null val state: MutableStateFlow = MutableStateFlow( value = getInitialState(), @@ -78,17 +84,7 @@ internal class CustomTokenFormModel @Inject constructor( }, onClick = ::selectDerivationPath, ), - saveToken = { - // TODO: Save token: [REDACTED_JIRA] - val dialog = ContentMessage { onDismiss -> - SimpleOkDialog( - message = "Not yet implemented", - onDismissDialog = onDismiss, - ) - } - - messageSender.send(dialog) - }, + saveToken = ::addCurrency, ) } @@ -117,6 +113,8 @@ internal class CustomTokenFormModel @Inject constructor( private fun observeValidatorUpdates() = modelScope.launch { customCurrencyValidator.consumeUpdates { validatorState -> + createdCurrency = null + when (validatorState) { is CustomCurrencyValidator.State.NotStarted -> Unit is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching() @@ -125,12 +123,16 @@ internal class CustomTokenFormModel @Inject constructor( exceptions = validatorState.exceptions, ) is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification() - is CustomCurrencyValidator.State.Validated -> updateStateWithCurrency( - currency = validatorState.currency, - fillForm = validatorState.fillForm, - isAlreadyAdded = validatorState.isAlreadyAdded, - isCustom = validatorState.isCustom, - ) + is CustomCurrencyValidator.State.Validated -> { + createdCurrency = validatorState.currency + + updateStateWithCurrency( + currency = validatorState.currency, + fillForm = validatorState.fillForm, + isAlreadyAdded = validatorState.isAlreadyAdded, + isCustom = validatorState.isCustom, + ) + } } } } @@ -296,6 +298,40 @@ internal class CustomTokenFormModel @Inject constructor( } } + private fun addCurrency() = resource( + acquire = { + state.update { state -> + state.updateWithProgress(showProgress = true) + } + }, + release = { + state.update { state -> + state.updateWithProgress(showProgress = false) + } + }, + ) { + val currency = createdCurrency + if (currency == null) { + Timber.e("Trying to add currency without validation") + showErrorDialog() + return@resource + } + + derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse { + Timber.e(it, "Failed to derive public keys") + showErrorDialog() + return@resource + } + + addCryptoCurrenciesUseCase(params.userWalletId, currency).getOrElse { + Timber.e(it, "Failed to add currency") + showErrorDialog() + return@resource + } + + params.onCurrencyAdded() + } + private fun selectNetwork() { params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 87e6636653..b5a235ee3a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -9,6 +9,8 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage @@ -67,6 +69,12 @@ internal class ManageTokensModel @Inject constructor( } } + fun reloadList() { + modelScope.launch { + manageTokensListManager.reload(params.userWalletId) + } + } + private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { return if (userWalletId == null) { createReadContentModel() @@ -166,9 +174,18 @@ internal class ManageTokensModel @Inject constructor( (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> Timber.e(fetchError.throwable) } + state.copySealed( isInitialBatchLoading = false, isNextBatchLoading = false, + scrollToTop = if (state.isInitialBatchLoading && state.items.isNotEmpty()) { + triggeredEvent( + data = Unit, + onConsume = ::consumeScrollToTopEvent, + ) + } else { + state.scrollToTop + }, ) } is PaginationStatus.EndOfPagination -> state.copySealed( @@ -179,6 +196,14 @@ internal class ManageTokensModel @Inject constructor( } } + private fun consumeScrollToTopEvent() { + this.state.update { state -> + state.copySealed( + scrollToTop = consumedEvent(), + ) + } + } + private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { state.update { state -> state.copySealed( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 9769590b55..1cff9a28cb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.CircularProgressIndicator @@ -45,6 +46,7 @@ import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalSnackbarHostState @@ -166,9 +168,12 @@ private fun SaveChangesButton( @Composable private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + Box(modifier = modifier) { Currencies( modifier = Modifier.fillMaxSize(), + listState = listState, items = state.items, showLoadingItem = state.isNextBatchLoading, onLoadMore = state.loadMore, @@ -185,10 +190,15 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { ) } } + + EventEffect(event = state.scrollToTop) { + listState.animateScrollToItem(index = 0) + } } @Composable private fun Currencies( + listState: LazyListState, items: ImmutableList, showLoadingItem: Boolean, isEditable: Boolean, @@ -198,7 +208,6 @@ private fun Currencies( val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(density = this).toDp() } - val listState = rememberLazyListState() LazyColumn( modifier = modifier, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 78e3d1011b..1cc07f7286 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -13,6 +13,7 @@ import com.tangem.features.managetokens.utils.ui.update import com.tangem.pagination.Batch import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -49,8 +50,9 @@ internal class ManageTokensUiManager( newCurrencyBatches.forEach { (key, data) -> val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key } + val currencyBatch = state.value.currencyBatches.getOrNull(indexToUpdate) - if (indexToUpdate == -1) { + if (indexToUpdate == -1 || currencyBatch == null) { val newBatch = Batch( key = key, data = data.map { item -> @@ -62,7 +64,7 @@ internal class ManageTokensUiManager( }, ) - batches.add(newBatch) + batches.addOrReplace(newBatch) { it.key == key } } else { val uiBatchToUpdate = currentUiBatches[indexToUpdate] @@ -70,8 +72,6 @@ internal class ManageTokensUiManager( return@forEach } - val currentCurrencyBatches = state.value.currencyBatches - val currencyBatch = currentCurrencyBatches[indexToUpdate] val updatedBatch = uiBatchToUpdate.copy( data = data.mapIndexed { index, item -> if (item == currencyBatch.data[index]) { From 248dab7303634ccd7c66c1f40df8da2ec83a290d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 14:21:17 +0500 Subject: [PATCH 076/197] Updated on 2026-08-14 --- .../managetokens/utils/list/ManageTokensListManager.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 12c248b792..565bb8de58 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -92,6 +92,7 @@ internal class ManageTokensListManager @Inject constructor( } suspend fun reload(userWalletId: UserWalletId?) { + state.value = ManageTokensListState() actionsFlow.emit( BatchAction.Reload( requestParams = ManageTokensListConfig(userWalletId, searchText = null), @@ -166,6 +167,8 @@ internal class ManageTokensListManager @Inject constructor( override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) { scope.launch { removeCustomCurrencyUseCase.invoke(userWalletId, currency) + .onRight { reload(userWalletId) } + .onLeft { Timber.e(it) } } } From 681fe664c632c7eb3592525e0fe6c71a6bb09c05 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:23:16 +0500 Subject: [PATCH 077/197] Updated on 2026-08-14 --- .../transformers/SetConfirmationStateCompletedTransformer.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 27e6593bd0..d2e2435917 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -4,11 +4,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateCompletedTransformer( private val appCurrencyProvider: Provider, @@ -39,6 +37,7 @@ internal class SetConfirmationStateCompletedTransformer( validatorState = validatorState.copySealed( isClickable = false, ), + notifications = persistentListOf(), transactionDoneState = TransactionDoneState.Content( timestamp = System.currentTimeMillis(), txUrl = txUrl, From 01b655feb7a6012a0c1ed8f08d5350ba5cf57bd2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:23:48 +0500 Subject: [PATCH 078/197] Updated on 2026-08-14 --- .../presentation/state/StakingStateRouter.kt | 4 +--- .../transformers/SetButtonsStateTransformer.kt | 2 +- .../ui/StakingConfirmationContent.kt | 11 ++++++++--- .../presentation/viewmodel/StakingViewModel.kt | 16 ++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 03cc906908..5dd1f3de27 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -26,9 +26,7 @@ internal class StakingStateRouter( StakingStep.Validators, StakingStep.Amount, -> showConfirmation() - StakingStep.Confirmation -> { - // TODO staking handle - } + StakingStep.Confirmation -> showInitial() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 6bd2c902fa..b18c96f147 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -152,7 +152,7 @@ internal class SetButtonsStateTransformer : Transformer { private fun StakingUiState.onConfirmationClick() { if (confirmationState is StakingStates.ConfirmationState.Data) { if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - clickIntents.onBackClick() + clickIntents.onNextClick() } else { val isEnterAction = actionType == StakingActionCommonType.ENTER val isApproveNeeded = confirmationState.isApprovalNeeded diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index ae5079b938..d7a9431621 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -3,11 +3,15 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -45,11 +49,12 @@ internal fun StakingConfirmationContent( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { + val doneState = state.transactionDoneState AnimatedVisibility( - visible = state.transactionDoneState is TransactionDoneState.Content, + visible = doneState is TransactionDoneState.Content, modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), ) { - val transactionDoneStateContent = state.transactionDoneState as TransactionDoneState.Content + val transactionDoneStateContent = remember(this) { doneState as TransactionDoneState.Content } TransactionDoneTitle( titleRes = R.string.sent_transaction_sent_title, date = transactionDoneStateContent.timestamp, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 1c18173837..4eba6fabde 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -164,23 +164,19 @@ internal class StakingViewModel @Inject constructor( stateController.update { it.copy(actionType = actionType) } } stakingStateRouter.onNextClick() - if (isAssentState()) { - getFee(pendingAction) + when { + isInitState() -> stateController.update(SetConfirmationStateLoadingTransformer(yield)) + isAssentState() -> getFee(pendingAction) } } override fun onActionClick() { handleOnNextConfirmationClick() - stakingStateRouter.onNextClick() } override fun getFee(pendingAction: PendingAction?) { viewModelScope.launch { - stateController.update( - SetConfirmationStateLoadingTransformer( - yield = yield, - ), - ) + stateController.update(SetConfirmationStateLoadingTransformer(yield)) val cryptoCurrencyValue = cryptoCurrencyStatus.value val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") @@ -871,6 +867,10 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } + private fun isInitState(): Boolean { + return value.currentStep == StakingStep.InitialInfo + } + private suspend fun checkIfSubtractAvailable() { isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrencyStatus.currency) .getOrElse { false } From a809fa07b6a4a10d4a14742e1d1630fab7085a88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:24:18 +0500 Subject: [PATCH 079/197] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../staking/model/stakekit/YieldBalance.kt | 25 ++++++++++++++--- .../state/InnerYieldBalanceState.kt | 1 + .../RewardsValidatorStateConverter.kt | 27 ++++++++++++------- .../converters/YieldBalancesConverter.kt | 21 ++++----------- .../previewdata/InitialStakingStatePreview.kt | 6 ++++- .../ui/StakingInitialInfoContent.kt | 14 +++++++++- 7 files changed, 63 insertions(+), 32 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 314a9a07d4..7b30804e92 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -671,6 +671,7 @@ Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. + Preparing Ready to withdraw Rebond Restake diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index adf85b9751..1c7c27ac54 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -78,11 +78,28 @@ data class PendingAction( enum class BalanceType { AVAILABLE, STAKED, - UNSTAKING, - UNSTAKED, PREPARING, - REWARDS, - LOCKED, + UNSTAKING, UNLOCKING, + UNSTAKED, + LOCKED, + REWARDS, UNKNOWN, + ; + + companion object { + fun BalanceType.isClickable() = when (this) { + STAKED, + UNSTAKED, + -> true + AVAILABLE, + UNSTAKING, + PREPARING, + REWARDS, + LOCKED, + UNLOCKING, + UNKNOWN, + -> false + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index ef7e4e8fec..c2ed6f2d62 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -39,5 +39,6 @@ data class BalanceState( val fiatAmount: TextReference, val rawCurrencyId: String?, val unbondingPeriod: TextReference, + val warmupPeriod: TextReference, val pendingActions: ImmutableList, ) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index a1c35482de..b27bdf6c41 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -1,6 +1,9 @@ package com.tangem.features.staking.impl.presentation.state.converters -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -46,16 +49,11 @@ internal class RewardsValidatorStateConverter( } val cryptoValue = balance.amount val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) - val unbondingPeriod = yield.metadata.cooldownPeriod.days + validator?.toBalanceState( cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoValue = cryptoValue, fiatValue = fiatValue, - unbondingPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(unbondingPeriod), - ), pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -64,12 +62,12 @@ internal class RewardsValidatorStateConverter( cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoValue: BigDecimal, fiatValue: BigDecimal?, - unbondingPeriod: TextReference, pendingActions: ImmutableList, ): BalanceState { val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency - + val unbondingPeriod = yield.metadata.cooldownPeriod.days + val warmupPeriod = yield.metadata.warmupPeriod.days val cryptoAmount = stringReference( BigDecimalFormatter.formatCryptoAmount( cryptoAmount = cryptoValue, @@ -94,7 +92,16 @@ internal class RewardsValidatorStateConverter( cryptoAmount = cryptoAmount, fiatAmount = fiatAmount, rawCurrencyId = cryptoCurrency.id.rawCurrencyId, - unbondingPeriod = unbondingPeriod, + unbondingPeriod = pluralReference( + id = R.plurals.common_days, + count = unbondingPeriod, + formatArgs = wrappedList(unbondingPeriod), + ), + warmupPeriod = pluralReference( + id = R.plurals.common_days, + count = unbondingPeriod, + formatArgs = wrappedList(warmupPeriod), + ), pendingActions = pendingActions, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 9be3086028..17c6e659d7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState @@ -61,7 +62,7 @@ internal class YieldBalancesConverter( .groupBy { it.type.toGroup() } .mapNotNull { item -> val (title, footer) = getGroupTitle(item.key) - val isClickable = getClickableType(item.key) + val isClickable = item.key.isClickable() title?.let { BalanceGroupedState( items = item.value.mapBalances().toPersistentList(), @@ -88,6 +89,7 @@ internal class YieldBalancesConverter( val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) val unbonding = getUnbondingDate(balance.date) + val warmupPeriod = yield.metadata.warmupPeriod.days validator?.let { BalanceState( validator = validator, @@ -108,6 +110,7 @@ internal class YieldBalancesConverter( ), rawCurrencyId = balance.rawCurrencyId, unbondingPeriod = unbonding, + warmupPeriod = pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)), pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -128,27 +131,13 @@ internal class YieldBalancesConverter( resourceReference(R.string.staking_unstaked_footer) BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null BalanceType.AVAILABLE -> null to null - BalanceType.PREPARING -> null to null + BalanceType.PREPARING -> resourceReference(R.string.staking_preparing) to null BalanceType.REWARDS -> null to null BalanceType.LOCKED -> null to null BalanceType.UNLOCKING -> null to null BalanceType.UNKNOWN -> null to null } - private fun getClickableType(type: BalanceType) = when (type) { - BalanceType.STAKED, - BalanceType.UNSTAKED, - -> true - BalanceType.AVAILABLE, - BalanceType.UNSTAKING, - BalanceType.PREPARING, - BalanceType.REWARDS, - BalanceType.LOCKED, - BalanceType.UNLOCKING, - BalanceType.UNKNOWN, - -> false - } - private fun getUnbondingDate(date: DateTime?): TextReference { val now = DateTime.now().millis val nowCalendar = Calendar.getInstance() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 66d92d8f89..15efd12337 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -6,7 +6,10 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { @@ -87,6 +90,7 @@ internal object InitialStakingStatePreview { ), unbondingPeriod = stringReference("3 days"), pendingActions = persistentListOf(), + warmupPeriod = stringReference("3 days"), ), ), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 8bd4543778..c84e8247e3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -209,7 +209,7 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli InputRowImageInfo( subtitle = stringReference(balance.validator.name), caption = getCaption(group.type, balance), - isGrayscaleImage = group.type == BalanceType.UNSTAKING, + isGrayscaleImage = !group.isClickable, infoTitle = balance.fiatAmount, infoSubtitle = balance.cryptoAmount, imageUrl = balance.validator.image.orEmpty(), @@ -248,6 +248,18 @@ private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextRef BalanceType.UNSTAKED -> { resourceReference(R.string.staking_ready_to_withdraw) } + BalanceType.PREPARING -> { + combinedReference( + resourceReference(R.string.staking_details_warmup_period), + annotatedReference { + appendSpace() + appendColored( + text = balance.warmupPeriod.resolveReference(), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } else -> { combinedReference( resourceReference(R.string.staking_details_apr), From 653cd7e72cb0009f513a618dbf20b7789ddf15cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 15:15:07 +0300 Subject: [PATCH 080/197] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 5 - .../com/tangem/common/routing/AppRoute.kt | 4 +- .../domain/tokens/legacy/TradeCryptoAction.kt | 2 - .../feature/swap/domain/SwapInteractor.kt | 8 +- .../feature/swap/domain/SwapInteractorImpl.kt | 129 +++++++----------- .../swap/domain/di/SwapDomainModule.kt | 124 ++--------------- .../feature/swap/viewmodels/SwapViewModel.kt | 58 ++++---- .../viewmodels/TokenDetailsViewModel.kt | 4 +- .../MultiWalletCurrencyActionsConverter.kt | 8 +- .../WalletCurrencyActionsClickIntents.kt | 17 ++- 10 files changed, 121 insertions(+), 238 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index cfb77e6180..9f5d8aecb2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -48,7 +48,6 @@ object TradeCryptoMiddleware { is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.Buy -> proceedBuyAction(state, action) is TradeCryptoAction.Sell -> proceedSellAction(action) - is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency) is TradeCryptoAction.Stake -> openStaking( userWalletId = action.userWalletId, cryptoCurrencyId = action.cryptoCurrencyId, @@ -141,10 +140,6 @@ object TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openSwap(currency: CryptoCurrency) { - store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) } - } - private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) { store.dispatchNavigationAction { push( diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index bae031de6d..d8e383a18e 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -219,12 +219,14 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Swap( val currency: CryptoCurrency, - ) : AppRoute(path = "/swap/${currency.id.value}"), RouteBundleParams { + val userWalletId: UserWalletId, + ) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) companion object { const val CURRENCY_BUNDLE_KEY = "currency" + const val USER_WALLET_ID_KEY = "userWalletId" } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 0b04bf321b..706493a6ea 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -40,8 +40,6 @@ sealed class TradeCryptoAction : Action { val transactionInfo: TransactionInfo? = null, ) : TradeCryptoAction() - data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() - data class Stake( val userWalletId: UserWalletId, val cryptoCurrencyId: CryptoCurrency.ID, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index dafaff9a51..8c6f6a8de7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount import com.tangem.feature.swap.domain.models.domain.PermissionOptions @@ -79,12 +79,14 @@ interface SwapInteractor { */ fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount - fun getSelectedWallet(): UserWallet? - suspend fun selectInitialCurrencyToSwap( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, ): CryptoCurrencyStatus? fun getNativeToken(networkId: String): CryptoCurrency + + interface Factory { + fun create(selectedWalletId: UserWalletId): SwapInteractor + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index de0468d9ff..72b9def69e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -14,7 +14,6 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.* @@ -29,7 +28,7 @@ import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.DataError @@ -43,21 +42,22 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFee import com.tangem.lib.crypto.models.ProxyFees +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode -import javax.inject.Inject @Suppress("LargeClass", "LongParameterList") -internal class SwapInteractorImpl @Inject constructor( +internal class SwapInteractorImpl @AssistedInject constructor( private val transactionManager: TransactionManager, private val userWalletManager: UserWalletManager, private val repository: SwapRepository, private val allowPermissionsHandler: AllowPermissionsHandler, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, @@ -69,9 +69,10 @@ internal class SwapInteractorImpl @Inject constructor( private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, - private val demoConfig: DemoConfig, private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) { @@ -82,15 +83,13 @@ internal class SwapInteractorImpl @Inject constructor( private val amountFormatter = AmountFormatter() private val hundredPercent = BigInteger("100") + private val userWallet + get() = getUserWalletUseCase(userWalletId).getOrElse { + error("Failed to get user wallet") + } + override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { - val selectedWallet = getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { it }, - ) - - requireNotNull(selectedWallet) { "No selected wallet" } - - val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) + val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(userWalletId) .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses @@ -133,10 +132,6 @@ internal class SwapInteractorImpl @Inject constructor( ) } - override fun getSelectedWallet(): UserWallet? { - return getSelectedWalletSyncUseCase().getOrNull() - } - private fun getToCurrenciesGroup( currency: CryptoCurrency, leastPairs: List, @@ -224,7 +219,7 @@ internal class SwapInteractorImpl @Inject constructor( memo = null, destination = getTokenAddress(permissionOptions.fromToken), network = permissionOptions.fromToken.network, - userWalletId = requireNotNull(getSelectedWallet()).walletId, + userWalletId = userWalletId, txExtras = createDexTxExtras( dataToSign, permissionOptions.fromToken.network, @@ -237,7 +232,7 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( txData = approveTransaction, - userWallet = requireNotNull(getSelectedWallet()), + userWallet = userWallet, network = permissionOptions.fromToken.network, ) return result.fold( @@ -403,7 +398,6 @@ internal class SwapInteractorImpl @Inject constructor( minAdaValue: BigDecimal?, ): List { val fromToken = fromTokenStatus.currency - val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() val warnings = mutableListOf() manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken, feeState) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) @@ -597,7 +591,8 @@ internal class SwapInteractorImpl @Inject constructor( """.trimIndent(), ) - val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return SwapTransactionState.UnknownError + val cardId = userWallet.scanResponse.card.cardId if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode return when (swapProvider.type) { @@ -615,7 +610,6 @@ internal class SwapInteractorImpl @Inject constructor( amount = amountToSwapWithFee, txFee = fee, swapProvider = swapProvider, - userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { @@ -627,7 +621,6 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGetStatus = currencyToGet, fee = fee, amountToSwap = amountToSwap, - userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } } @@ -679,7 +672,6 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGetStatus: CryptoCurrencyStatus, amountToSwap: String, fee: TxFee, - userWalletId: UserWalletId, ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) @@ -706,7 +698,7 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( txData = txData, - userWallet = requireNotNull(getSelectedWallet()), + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return SwapTransactionState.UnknownError }, network = currencyToSendStatus.currency.network, ) return result.fold( @@ -766,7 +758,6 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, txFee: TxFee, swapProvider: SwapProvider, - userWalletId: UserWalletId, ): SwapTransactionState { val exchangeData = repository.getExchangeData( fromContractAddress = currencyToSend.currency.getContractAddress(), @@ -787,8 +778,9 @@ internal class SwapInteractorImpl @Inject constructor( val exchangeDataCex = exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError - val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError - if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.UnknownError + val cardId = userWallet.scanResponse.card.cardId + + if (isDemoCardUseCase(cardId)) return SwapTransactionState.UnknownError val txData = createTransactionUseCase( amount = amount.value.convertToSdkAmount(currencyToSend.currency), @@ -811,7 +803,7 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( txData = txData, - userWallet = requireNotNull(getSelectedWallet()), + userWallet = userWallet, network = currencyToSend.currency.network, ) @@ -941,9 +933,8 @@ internal class SwapInteractorImpl @Inject constructor( txExternalUrl: String? = null, txExternalId: String? = null, ) { - val selectedWallet = getSelectedWallet() ?: return swapTransactionRepository.storeTransaction( - userWalletId = selectedWallet.walletId, + userWalletId = userWalletId, fromCryptoCurrency = currencyToSend.currency, toCryptoCurrency = currencyToGet.currency, transaction = SavedSwapTransactionModel( @@ -994,23 +985,16 @@ internal class SwapInteractorImpl @Inject constructor( spenderAddress: String, ): Boolean { if (fromToken is CryptoCurrency.Coin) return true - return getSelectedWalletSyncUseCase().fold( - ifRight = { userWallet -> - val allowance = repository.getAllowance( - userWalletId = userWallet.walletId, - networkId = networkId, - derivationPath = fromToken.network.derivationPath.value, - tokenDecimalCount = fromToken.decimals, - tokenAddress = getTokenAddress(fromToken), - spenderAddress = spenderAddress, - ) - allowance >= amount.value - }, - ifLeft = { - Timber.e("Swap Error on isAllowedToSpend") - false - }, + + val allowance = repository.getAllowance( + userWalletId = userWallet.walletId, + networkId = networkId, + derivationPath = fromToken.network.derivationPath.value, + tokenDecimalCount = fromToken.decimals, + tokenAddress = getTokenAddress(fromToken), + spenderAddress = spenderAddress, ) + return allowance >= amount.value } private suspend fun createEmptyAmountState(): SwapState { @@ -1040,13 +1024,11 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { - val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.let { userWallet -> - getUnhandledFee( - amount = amount.value, - userWallet = userWallet, - cryptoCurrency = fromToken, - ) - } + val txFeeResult = getUnhandledFee( + amount = amount.value, + userWallet = userWallet, + cryptoCurrency = fromToken, + ) val txFee = if (provider.type == ExchangeProviderType.CEX) { getFeeForCex(txFeeResult, fromTokenStatus) @@ -1196,7 +1178,6 @@ internal class SwapInteractorImpl @Inject constructor( is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee } val feePaidCurrency = getFeePaidCurrency( - userWalletId = requireNotNull(getSelectedWallet()).walletId, currency = fromToken, ) @@ -1256,7 +1237,6 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getFormattedFiatFees(fromToken: CryptoCurrency, vararg fees: BigDecimal): List { val appCurrency = getSelectedAppCurrencyUseCase.unwrap() val feePaidCurrency = getFeePaidCurrency( - userWalletId = requireNotNull(getSelectedWallet()).walletId, currency = fromToken, ) val feeCurrencyId: CryptoCurrency.ID = when (feePaidCurrency) { @@ -1308,7 +1288,6 @@ internal class SwapInteractorImpl @Inject constructor( val otherNativeFee = transaction.otherNativeFeeWei ?.movePointLeft(nativeCoinDecimals) ?: BigDecimal.ZERO - val userWallet = getSelectedWallet() val txFeeState = when ( val feeData = getFeeDataForDexSwap( networkId = networkId, @@ -1509,7 +1488,6 @@ internal class SwapInteractorImpl @Inject constructor( swapAmount = swapAmount, spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, ) - val userWallet = getSelectedWallet() val cardId = userWallet?.scanResponse?.card?.cardId val feeData = if (cardId != null && isDemoCardUseCase(cardId)) { getDemoFees(fromTokenStatus.currency) @@ -1846,7 +1824,6 @@ internal class SwapInteractorImpl @Inject constructor( ): Boolean { val tokenBalance = getTokenBalance(fromToken).value val feePaidCurrency = getFeePaidCurrency( - userWalletId = requireNotNull(getSelectedWallet()).walletId, currency = fromToken.currency, ) return when (feePaidCurrency) { @@ -1861,7 +1838,7 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency { + private suspend fun getFeePaidCurrency(currency: CryptoCurrency): FeePaidCurrency { return currenciesRepository.getFeePaidCurrency( userWalletId = userWalletId, currency = currency, @@ -1894,13 +1871,12 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromTokenStatus: CryptoCurrencyStatus, ): SwapFeeState { - val userWalletId = requireNotNull(getSelectedWallet()).walletId if (fee == null) { return SwapFeeState.NotEnough() } val percentsToFeeIncrease = BigDecimal.ONE - return when (val feePaidCurrency = getFeePaidCurrency(userWalletId, fromTokenStatus.currency)) { + return when (val feePaidCurrency = getFeePaidCurrency(fromTokenStatus.currency)) { FeePaidCurrency.Coin -> { val nativeTokenBalance = userWalletManager.getNativeTokenBalance( networkId, @@ -1994,21 +1970,13 @@ internal class SwapInteractorImpl @Inject constructor( swapAmount: SwapAmount? = null, spenderAddress: String, ): String { - return getSelectedWalletSyncUseCase().fold( - ifRight = { userWallet -> - repository.getApproveData( - userWalletId = userWallet.walletId, - networkId = networkId, - derivationPath = derivationPath, - currency = fromToken, - amount = swapAmount?.value, - spenderAddress = spenderAddress, - ) - }, - ifLeft = { - Timber.e("Swap Error on getApproveData") - error("Swap Error on getApproveData") - }, + return repository.getApproveData( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + currency = fromToken, + amount = swapAmount?.value, + spenderAddress = spenderAddress, ) } @@ -2059,4 +2027,9 @@ internal class SwapInteractorImpl @Inject constructor( private val normalDemoFee = "0.0002".toBigDecimal() private val priorityDemoFee = "0.0003".toBigDecimal() } + + @AssistedFactory + interface Factory : SwapInteractor.Factory { + override fun create(selectedWalletId: UserWalletId): SwapInteractorImpl + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 3f20bc09bc..9f95b3e0c6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,79 +1,34 @@ package com.tangem.feature.swap.domain.di -import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* -import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.lib.crypto.TransactionManager -import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Qualifier import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class SwapDomainModule { +internal class SwapDomainModule { + + @Provides + fun provideAllowPermissionsHandler(): AllowPermissionsHandler { + return AllowPermissionsHandlerImpl() + } @Provides @Singleton - fun provideSwapInteractor( - swapRepository: SwapRepository, - userWalletManager: UserWalletManager, - transactionManager: TransactionManager, - @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, - @SwapScope sendTransactionUseCase: SendTransactionUseCase, - @SwapScope createTransactionUseCase: CreateTransactionUseCase, - createTransactionDataExtrasUseCase: CreateTransactionDataExtrasUseCase, - isDemoCardUseCase: IsDemoCardUseCase, - quotesRepository: QuotesRepository, - swapTransactionRepository: SwapTransactionRepository, - appCurrencyRepository: AppCurrencyRepository, - currencyChecksRepository: CurrencyChecksRepository, - initialToCurrencyResolver: InitialToCurrencyResolver, - currenciesRepository: CurrenciesRepository, - validateTransactionUseCase: ValidateTransactionUseCase, - estimateFeeUseCase: EstimateFeeUseCase, - ): SwapInteractor { - return SwapInteractorImpl( - transactionManager = transactionManager, - userWalletManager = userWalletManager, - repository = swapRepository, - allowPermissionsHandler = AllowPermissionsHandlerImpl(), - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, - sendTransactionUseCase = sendTransactionUseCase, - createTransactionUseCase = createTransactionUseCase, - createTransactionExtrasUseCase = createTransactionDataExtrasUseCase, - isDemoCardUseCase = isDemoCardUseCase, - quotesRepository = quotesRepository, - swapTransactionRepository = swapTransactionRepository, - appCurrencyRepository = appCurrencyRepository, - currencyChecksRepository = currencyChecksRepository, - currenciesRepository = currenciesRepository, - initialToCurrencyResolver = initialToCurrencyResolver, - demoConfig = DemoConfig(), - validateTransactionUseCase = validateTransactionUseCase, - estimateFeeUseCase = estimateFeeUseCase, - ) + fun provideSwapInteractorFactory(factory: SwapInteractorImpl.Factory): SwapInteractor.Factory { + return factory } @Provides @@ -84,13 +39,6 @@ class SwapDomainModule { ) } - @SwapScope - @Provides - @Singleton - fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) - } - @Provides @Singleton fun providesGetCryptoCurrencyStatusUseCase( @@ -109,58 +57,10 @@ class SwapDomainModule { ) } - @SwapScope - @Provides - @Singleton - fun providesGetCardTokensListUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, - ): GetCardTokensListUseCase { - return GetCardTokensListUseCase( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } - - @SwapScope - @Provides - fun provideDemoCardUseCase(): IsDemoCardUseCase { - return IsDemoCardUseCase(config = DemoConfig()) - } - - @SwapScope - @Provides - @Singleton - fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { - return CreateTransactionUseCase( - transactionRepository = transactionRepository, - ) - } - - @SwapScope - @Provides - @Singleton - fun provideSendTransactionUseCase( - cardSdkConfigRepository: CardSdkConfigRepository, - transactionRepository: TransactionRepository, - walletManagersFacade: WalletManagersFacade, - ): SendTransactionUseCase { - return SendTransactionUseCase( - demoConfig = DemoConfig(), - cardSdkConfigRepository = cardSdkConfigRepository, - transactionRepository = transactionRepository, - walletManagersFacade = walletManagersFacade, - ) - } - @Provides @Singleton fun provideInitialToCurrencyResolver( - @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, swapTransactionRepository: SwapTransactionRepository, ): InitialToCurrencyResolver { return DefaultInitialToCurrencyResolver( @@ -168,8 +68,4 @@ class SwapDomainModule { swapTransactionRepository = swapTransactionRepository, ) } -} - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class SwapScope \ No newline at end of file +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 8361ef85c9..1958445de2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -25,7 +25,6 @@ import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor @@ -65,7 +64,7 @@ typealias SuccessLoadedSwapData = Map @Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class SwapViewModel @Inject constructor( - private val swapInteractor: SwapInteractor, + private val swapInteractorFactory: SwapInteractor.Factory, private val blockchainInteractor: BlockchainInteractor, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -79,7 +78,13 @@ internal class SwapViewModel @Inject constructor( private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Swap.CURRENCY_BUNDLE_KEY) ?.unbundle(CryptoCurrency.serializer()) - ?: error("no expected parameter CryptoCurrency found`") + ?: error("no expected parameter CryptoCurrency found") + + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Swap.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) + ?: error("no expected parameter UserWalletId found") + + private val swapInteractor = swapInteractorFactory.create(userWalletId) private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus @@ -126,15 +131,13 @@ internal class SwapViewModel @Inject constructor( init { viewModelScope.launch(dispatchers.io) { - swapInteractor.getSelectedWallet()?.let { - val cryptoCurrencyStatus = - getCryptoCurrencyStatusUseCase(it.walletId, initialCryptoCurrency.id).getOrNull() - if (cryptoCurrencyStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back) - } else { - initialCryptoCurrencyStatus = cryptoCurrencyStatus - initTokens() - } + val cryptoCurrencyStatus = + getCryptoCurrencyStatusUseCase(userWalletId, initialCryptoCurrency.id).getOrNull() + if (cryptoCurrencyStatus == null) { + uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back) + } else { + initialCryptoCurrencyStatus = cryptoCurrencyStatus + initTokens() } } } @@ -191,7 +194,6 @@ internal class SwapViewModel @Inject constructor( ), ) - val userWalletId = swapInteractor.getSelectedWallet()?.walletId ?: return@launch (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { subscribeToCoinBalanceUpdates( userWalletId = userWalletId, @@ -707,8 +709,6 @@ internal class SwapViewModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it)) } - val userWalletId = swapInteractor.getSelectedWallet()?.walletId - if (foundToken != null) { val fromToken: CryptoCurrencyStatus val toToken: CryptoCurrencyStatus @@ -717,7 +717,7 @@ internal class SwapViewModel @Inject constructor( toToken = initialCryptoCurrencyStatus val newToken = fromToken.currency as? CryptoCurrency.Coin - if (userWalletId != null && newToken != null) { + if (newToken != null) { subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = newToken, @@ -729,7 +729,7 @@ internal class SwapViewModel @Inject constructor( toToken = foundToken.currencyStatus val newToken = toToken.currency as? CryptoCurrency.Coin - if (userWalletId != null && newToken != null) { + if (newToken != null) { subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = newToken, @@ -985,12 +985,10 @@ internal class SwapViewModel @Inject constructor( } }, onBuyClick = { currency -> - swapInteractor.getSelectedWallet()?.let { userWallet -> - swapRouter.openTokenDetails( - userWalletId = userWallet.walletId, - currency = currency, - ) - } + swapRouter.openTokenDetails( + userWalletId = userWalletId, + currency = currency, + ) }, onRetryClick = { startLoadingQuotesFromLastState() @@ -1162,20 +1160,18 @@ internal class SwapViewModel @Inject constructor( } private fun updateWalletBalance() { - swapInteractor.getSelectedWallet()?.let { userWallet -> - dataState.fromCryptoCurrency?.currency?.network?.let { network -> - viewModelScope.launch { - withContext(NonCancellable) { - updateForBalance(userWallet, network) - } + dataState.fromCryptoCurrency?.currency?.network?.let { network -> + viewModelScope.launch { + withContext(NonCancellable) { + updateForBalance(userWalletId, network) } } } } - private suspend fun updateForBalance(userWallet: UserWallet, network: Network) { + private suspend fun updateForBalance(userWalletId: UserWalletId, network: Network) { updateDelayedCurrencyStatusUseCase( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, network = network, delayMillis = UPDATE_BALANCE_DELAY_MILLIS, refresh = true, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index d6c7da21d3..f959f9632a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -6,6 +6,7 @@ import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -126,6 +127,7 @@ internal class TokenDetailsViewModel @Inject constructor( tokenDetailsFeatureToggles: TokenDetailsFeatureToggles, deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, + private val appRouter: AppRouter, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.CurrencyDetails.USER_WALLET_ID_KEY) @@ -616,7 +618,7 @@ internal class TokenDetailsViewModel @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason)) return - reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrency)) + appRouter.push(AppRoute.Swap(currency = cryptoCurrency, userWalletId = userWalletId)) } override fun onDismissDialog() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index d924dabe10..95f595916e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -77,7 +77,13 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Swap -> { title = resourceReference(R.string.swapping_swap_action) icon = R.drawable.ic_exchange_horizontal_24 - action = { clickIntents.onSwapClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) } + action = { + clickIntents.onSwapClick( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = userWallet.walletId, + unavailabilityReason = ScenarioUnavailabilityReason.None, + ) + } } is TokenActionsState.ActionState.CopyAddress -> { title = resourceReference(R.string.common_copy_address) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index cb24122b08..f00e79aa55 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -57,7 +59,11 @@ interface WalletCurrencyActionsClickIntents { fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) - fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) + fun onSwapClick( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWalletId: UserWalletId, + unavailabilityReason: ScenarioUnavailabilityReason, + ) fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) @@ -95,6 +101,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val getYieldUseCase: GetYieldUseCase, + private val appRouter: AppRouter, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -352,6 +359,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, + userWalletId: UserWalletId, unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( @@ -360,7 +368,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason)) return - reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrencyStatus.currency)) + appRouter.push( + AppRoute.Swap( + currency = cryptoCurrencyStatus.currency, + userWalletId = userWalletId, + ), + ) } override fun onExploreClick() { From 5cd0d0b878f23a06c498f9523a4c7c1442ad17c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 12:17:42 +0400 Subject: [PATCH 081/197] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 18 +++++++++-- .../DefaultCustomTokensRepository.kt | 30 +++++++++++++++++++ .../GetSupportedNetworksUseCase.kt | 27 +++++++++++++++++ .../ValidateDerivationPathUseCase.kt | 24 +++++++++++++++ .../DerivationPathValidationException.kt | 8 +++++ .../exceptoin/SupportedBlockchainException.kt | 8 +++++ .../repository/CustomTokensRepository.kt | 4 +++ 7 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 576da07163..02bb0f9562 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,8 +1,6 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.domain.managetokens.SaveManagedTokensUseCase -import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -67,4 +65,20 @@ internal object ManageTokensDomainModule { derivationsRepository = derivationsRepository, ) } + + @Provides + @Singleton + fun provideGetSupportedNetworksUseCase( + customTokensRepository: CustomTokensRepository, + ): GetSupportedNetworksUseCase { + return GetSupportedNetworksUseCase(customTokensRepository) + } + + @Provides + @Singleton + fun provideValidateDerivationPathUseCase( + customTokensRepository: CustomTokensRepository, + ): ValidateDerivationPathUseCase { + return ValidateDerivationPathUseCase(customTokensRepository) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 0ec95b565f..752cef4833 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.managetokens import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.UserTokensResponseFactory @@ -16,8 +17,10 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.canHandleBlockchain import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository @@ -178,6 +181,33 @@ internal class DefaultCustomTokensRepository( } } + override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + ?: error("User wallet not found") + val scanResponse = userWallet.scanResponse + + Blockchain.entries + .mapNotNull { blockchain -> + if (scanResponse.card.canHandleBlockchain(blockchain, scanResponse.cardTypesResolver)) { + getNetwork( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = scanResponse.derivationStyleProvider, + ) + } else { + null + } + } + } + + override fun createDerivationPath(rawPath: String): Network.DerivationPath { + val sdkPath = DerivationPath(rawPath) + + return Network.DerivationPath.Custom( + value = sdkPath.rawPath, + ) + } + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) appPreferencesStore.storeObject( diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt new file mode 100644 index 0000000000..1074a21b02 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetSupportedNetworksUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.tangem.domain.managetokens.model.exceptoin.SupportedBlockchainException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class GetSupportedNetworksUseCase( + private val repository: CustomTokensRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either> { + return either { + val networks = catch({ repository.getSupportedNetworks(userWalletId) }) { + raise(SupportedBlockchainException.DataError(it)) + } + + ensureNotNull(networks.takeIf { it.isNotEmpty() }) { + SupportedBlockchainException.EmptyList + } + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt new file mode 100644 index 0000000000..deaa42edf9 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateDerivationPathUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.Network + +class ValidateDerivationPathUseCase( + private val repository: CustomTokensRepository, +) { + + operator fun invoke(rawValue: String): Either { + return either { + ensure(rawValue.isNotEmpty()) { DerivationPathValidationException.Empty } + + catch({ repository.createDerivationPath(rawValue) }) { + raise(DerivationPathValidationException.Invalid) + } + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt new file mode 100644 index 0000000000..fdda23776c --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/DerivationPathValidationException.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class DerivationPathValidationException { + + data object Empty : DerivationPathValidationException() + + data object Invalid : DerivationPathValidationException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt new file mode 100644 index 0000000000..946e3cd9b4 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/exceptoin/SupportedBlockchainException.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model.exceptoin + +sealed class SupportedBlockchainException { + + data object EmptyList : SupportedBlockchainException() + + data class DataError(val cause: Throwable) : SupportedBlockchainException() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index 5f84209200..c0f898207e 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -33,4 +33,8 @@ interface CustomTokensRepository { ): CryptoCurrency.Token suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) + + suspend fun getSupportedNetworks(userWalletId: UserWalletId): List + + fun createDerivationPath(rawPath: String): Network.DerivationPath } \ No newline at end of file From 3cf222aa32e516f80a7d0c2d366be2e9e83be267 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 12:53:14 +0400 Subject: [PATCH 082/197] Updated on 2026-08-14 --- .../CustomTokenDerivationInputComponent.kt | 3 +- ...aultCustomTokenDerivationInputComponent.kt | 127 ++++++++++++++++++ .../impl/DefaultManageTokensComponent.kt | 8 +- .../managetokens/di/ComponentModule.kt | 16 +-- .../CustomTokenSelectorDialogConfig.kt | 13 ++ ...ig.kt => ManageTokensBottomSheetConfig.kt} | 4 +- .../managetokens/model/ManageTokensModel.kt | 6 +- 7 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt rename features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/{BottomSheetConfig.kt => ManageTokensBottomSheetConfig.kt} (73%) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt index 1530c3d205..383d94f073 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenDerivationInputComponent.kt @@ -3,12 +3,13 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent { data class Params( val userWalletId: UserWalletId, - val onConfirm: (String) -> Unit, + val onConfirm: (SelectedDerivationPath) -> Unit, val onDismiss: () -> Unit, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt new file mode 100644 index 0000000000..0695290569 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt @@ -0,0 +1,127 @@ +package com.tangem.features.managetokens.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import arrow.core.getOrElse +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.managetokens.ValidateDerivationPathUseCase +import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent +import com.tangem.features.managetokens.entity.customtoken.CustomDerivationInputUM +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.dialog.CustomDerivationInputDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* + +internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: CustomTokenDerivationInputComponent.Params, + private val validateDerivationPathUseCase: ValidateDerivationPathUseCase, +) : CustomTokenDerivationInputComponent, AppComponentContext by context { + + private val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + init { + observeValueUpdates() + } + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun Dialog() { + val state by state.collectAsStateWithLifecycle() + + CustomDerivationInputDialog( + model = state, + onDismiss = ::dismiss, + ) + } + + @OptIn(FlowPreview::class) + private fun observeValueUpdates() { + state + .map { it.value.text } + .distinctUntilChanged() + .sample(periodMillis = 1_000) + .onEach(::validateValue) + .launchIn(componentScope) + } + + private fun getInitialState(): CustomDerivationInputUM = CustomDerivationInputUM( + value = TextFieldValue(), + error = null, + updateValue = ::updateValue, + isConfirmEnabled = false, + onConfirm = ::confirm, + ) + + private fun validateValue(value: String) { + validateDerivationPathUseCase(value).getOrElse { e -> + updateWithValidationError(e) + return + } + + state.update { state -> + state.copy( + error = null, + isConfirmEnabled = true, + ) + } + } + + private fun updateWithValidationError(e: DerivationPathValidationException) { + state.update { state -> + state.copy( + error = when (e) { + DerivationPathValidationException.Empty -> null + DerivationPathValidationException.Invalid -> { + resourceReference(R.string.custom_token_invalid_derivation_path) + } + }, + isConfirmEnabled = false, + ) + } + } + + private fun updateValue(value: TextFieldValue) { + state.update { state -> + state.copy(value = value) + } + } + + private fun confirm() { + if (state.value.error != null && !state.value.isConfirmEnabled) { + return + } + + val value = state.value.value.text + val model = SelectedDerivationPath( + id = null, + value = Network.DerivationPath.Custom(value), + networkName = stringReference(value = value), + ) + + params.onConfirm(model) + } + + @AssistedFactory + interface Factory : CustomTokenDerivationInputComponent.Factory { + override fun create( + context: AppComponentContext, + params: CustomTokenDerivationInputComponent.Params, + ): DefaultCustomTokenDerivationInputComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index 98df4a4346..2b513d23cb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -15,7 +15,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig +import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.model.ManageTokensModel import com.tangem.features.managetokens.ui.ManageTokensScreen import dagger.assisted.Assisted @@ -32,7 +32,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, - serializer = BottomSheetConfig.serializer(), + serializer = ManageTokensBottomSheetConfig.serializer(), handleBackButton = false, childFactory = ::bottomSheetChild, ) @@ -53,10 +53,10 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( } private fun bottomSheetChild( - config: BottomSheetConfig, + config: ManageTokensBottomSheetConfig, componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { - is BottomSheetConfig.AddCustomToken -> { + is ManageTokensBottomSheetConfig.AddCustomToken -> { addCustomTokenComponentFactory.create( context = childByContext(componentContext), params = AddCustomTokenComponent.Params( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt index 9de5afa38c..ea21e92af1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt @@ -1,13 +1,7 @@ package com.tangem.features.managetokens.di -import com.tangem.features.managetokens.component.AddCustomTokenComponent -import com.tangem.features.managetokens.component.CustomTokenFormComponent -import com.tangem.features.managetokens.component.CustomTokenSelectorComponent -import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.component.impl.DefaultAddCustomTokenComponent -import com.tangem.features.managetokens.component.impl.DefaultCustomTokenFormComponent -import com.tangem.features.managetokens.component.impl.DefaultCustomTokenSelectorComponent -import com.tangem.features.managetokens.component.impl.DefaultManageTokensComponent +import com.tangem.features.managetokens.component.* +import com.tangem.features.managetokens.component.impl.* import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -39,4 +33,10 @@ internal interface ComponentModule { fun bindCustomTokenFormComponentFactory( factory: DefaultCustomTokenFormComponent.Factory, ): CustomTokenFormComponent.Factory + + @Binds + @Singleton + fun bindCustomTokenDerivationInputComponentFactory( + factory: DefaultCustomTokenDerivationInputComponent.Factory, + ): CustomTokenDerivationInputComponent.Factory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt new file mode 100644 index 0000000000..19b090720b --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorDialogConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.features.managetokens.entity.customtoken + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class CustomTokenSelectorDialogConfig { + + @Serializable + data class CustomDerivationInput( + val userWalletId: UserWalletId, + ) : CustomTokenSelectorDialogConfig() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt similarity index 73% rename from features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt rename to features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt index 102140a57c..96f7553288 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt @@ -4,10 +4,10 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @Serializable -internal sealed class BottomSheetConfig { +internal sealed class ManageTokensBottomSheetConfig { @Serializable data class AddCustomToken( val userWalletId: UserWalletId, - ) : BottomSheetConfig() + ) : ManageTokensBottomSheetConfig() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index b5a235ee3a..581f40fc1b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -18,7 +18,7 @@ import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM -import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig +import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R @@ -47,7 +47,7 @@ internal class ManageTokensModel @Inject constructor( private val params: ManageTokensComponent.Params = paramsContainer.require() val state: MutableStateFlow = MutableStateFlow(getInitialState(params.userWalletId)) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { manageTokensListManager.uiItems @@ -228,7 +228,7 @@ internal class ManageTokensModel @Inject constructor( private fun navigateToAddCustomToken() { params.userWalletId?.let { - bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it)) + bottomSheetNavigation.activate(ManageTokensBottomSheetConfig.AddCustomToken(it)) } } From a99b3372700504a4e2dfc35f6ccd22c4f0e28043 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 13:07:15 +0400 Subject: [PATCH 083/197] Updated on 2026-08-14 --- .../component/CustomTokenFormComponent.kt | 2 +- .../component/CustomTokenSelectorComponent.kt | 1 + .../impl/DefaultAddCustomTokenComponent.kt | 15 +- .../DefaultCustomTokenSelectorComponent.kt | 40 +++- .../preview/PreviewAddCustomTokenComponent.kt | 3 +- .../PreviewCustomTokenSelectorComponent.kt | 7 +- .../features/managetokens/di/ModelModule.kt | 12 ++ .../customtoken/AddCustomTokenConfig.kt | 2 +- .../customtoken/CustomTokenSelectorUM.kt | 1 + .../model/CustomTokenFormModel.kt | 11 +- .../model/CustomTokenSelectorModel.kt | 172 ++++++++++++++++++ .../utils/mapper/CurrencyNetworksMapper.kt | 20 +- .../utils/mapper/DerivationPathMapper.kt | 34 ++++ 13 files changed, 296 insertions(+), 24 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index e9fc3c448f..b7c219e560 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -12,7 +12,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val network: SelectedNetwork, - val derivationPath: SelectedDerivationPath, + val derivationPath: SelectedDerivationPath?, val formValues: CustomTokenFormValues, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt index b3e4a159a0..d1f5e8aaf6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt @@ -18,6 +18,7 @@ internal interface CustomTokenSelectorComponent : ComposableContentComponent { data class DerivationPathSelector( val userWalletId: UserWalletId, + val selectedNetwork: SelectedNetwork, val selectedDerivationPath: SelectedDerivationPath?, val onDerivationPathSelected: (SelectedDerivationPath) -> Unit, ) : Params() diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 3679e6f929..bde22e12c6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -12,7 +12,6 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.component.CustomTokenSelectorComponent @@ -20,7 +19,6 @@ import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork -import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -110,6 +108,9 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( context = childByContext(componentContext), params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = config.userWalletId, + selectedNetwork = requireNotNull(config.selectedNetwork) { + "Network is not selected" + }, selectedDerivationPath = config.selectedDerivationPath, onDerivationPathSelected = { derivationPath -> showForm(derivationPath = derivationPath) @@ -122,12 +123,10 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( context = childByContext(componentContext), params = CustomTokenFormComponent.Params( userWalletId = config.userWalletId, - network = config.selectedNetwork ?: error("Network is not selected"), - derivationPath = config.selectedDerivationPath ?: SelectedDerivationPath( - id = config.selectedNetwork.id, - value = config.selectedNetwork.derivationPath, - networkName = resourceReference(R.string.custom_token_derivation_path_default), - ), + network = requireNotNull(config.selectedNetwork) { + "Network is not selected" + }, + derivationPath = config.selectedDerivationPath, formValues = config.formValues, onSelectNetworkClick = ::showNetworkSelector, onSelectDerivationPathClick = ::showDerivationPathSelector, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt index c748d7363f..2d690a862c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt @@ -4,33 +4,61 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent import com.tangem.features.managetokens.component.CustomTokenSelectorComponent -import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent -import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig +import com.tangem.features.managetokens.model.CustomTokenSelectorModel import com.tangem.features.managetokens.ui.CustomTokenSelectorContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.MutableStateFlow internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted params: CustomTokenSelectorComponent.Params, + private val customTokenDerivationInputComponentFactory: CustomTokenDerivationInputComponent.Factory, ) : CustomTokenSelectorComponent, AppComponentContext by context { - private val state: MutableStateFlow = MutableStateFlow( - value = PreviewCustomTokenSelectorComponent(params = params).previewState, + private val model: CustomTokenSelectorModel = getOrCreateModel(params) + private val dialogSlot = childSlot( + source = model.dialogNavigation, + serializer = CustomTokenSelectorDialogConfig.serializer(), + childFactory = ::createDialog, ) + private fun createDialog( + config: CustomTokenSelectorDialogConfig, + context: ComponentContext, + ): ComposableDialogComponent = when (config) { + is CustomTokenSelectorDialogConfig.CustomDerivationInput -> customTokenDerivationInputComponentFactory.create( + context = childByContext(context), + params = CustomTokenDerivationInputComponent.Params( + userWalletId = config.userWalletId, + onConfirm = model::selectCustomDerivationPath, + onDismiss = model.dialogNavigation::dismiss, + ), + ) + } + @Composable override fun Content(modifier: Modifier) { - val state by state.collectAsStateWithLifecycle() + val state by model.state.collectAsStateWithLifecycle() + val dialog by dialogSlot.subscribeAsState() CustomTokenSelectorContent( modifier = modifier, model = state, ) + + dialog.child?.instance?.Dialog() } @AssistedFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index a09d7b2b93..79d2f3bdcd 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -60,7 +60,8 @@ internal class PreviewAddCustomTokenComponent( PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = state.userWalletId, - selectedDerivationPath = state.selectedDerivationPath, + selectedNetwork = state.selectedNetwork!!, + selectedDerivationPath = state.selectedDerivationPath!!, onDerivationPathSelected = {}, ), ).Content(modifier) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 1ebcff8b5d..0a37116d7d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -35,7 +35,7 @@ internal class PreviewCustomTokenSelectorComponent( ) DerivationPathUM( - id = d.id.value, + id = d.id?.value ?: "", value = d.value.value.orEmpty(), networkName = d.networkName, isSelected = d.value == params.selectedDerivationPath?.value, @@ -75,7 +75,10 @@ internal class PreviewCustomTokenSelectorComponent( val previewState = CustomTokenSelectorUM( header = when (params) { - is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton({}) + is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton( + value = null, + onClick = {}, + ) is Params.NetworkSelector -> if (params.selectedNetwork == null) { CustomTokenSelectorUM.HeaderUM.Description } else { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt index 572c2624cb..0f33d5994f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt @@ -2,6 +2,8 @@ package com.tangem.features.managetokens.di import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.managetokens.model.CustomTokenFormModel +import com.tangem.features.managetokens.model.CustomTokenSelectorModel import com.tangem.features.managetokens.model.ManageTokensModel import dagger.Binds import dagger.Module @@ -17,4 +19,14 @@ internal interface ModelModule { @IntoMap @ClassKey(ManageTokensModel::class) fun provideManageTokensModel(model: ManageTokensModel): Model + + @Binds + @IntoMap + @ClassKey(CustomTokenFormModel::class) + fun provideCustomTokenFormModel(model: CustomTokenFormModel): Model + + @Binds + @IntoMap + @ClassKey(CustomTokenSelectorModel::class) + fun provideCustomTokenSelectorModel(model: CustomTokenSelectorModel): Model } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt index 6a43cdc40d..1c5de5c702 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt @@ -34,7 +34,7 @@ internal data class SelectedNetwork( @Serializable internal data class SelectedDerivationPath( - val id: Network.ID, + val id: Network.ID?, val value: Network.DerivationPath, val networkName: TextReference, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt index 25c08fac10..d607a8b959 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt @@ -17,6 +17,7 @@ internal data class CustomTokenSelectorUM( data object Description : HeaderUM() data class CustomDerivationButton( + val value: String?, val onClick: () -> Unit, ) : HeaderUM() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 25fbf11d48..2fa77b8e92 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -14,6 +14,7 @@ import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM @@ -57,7 +58,7 @@ internal class CustomTokenFormModel @Inject constructor( customCurrencyValidator.createCoin( userWalletId = params.userWalletId, networkId = params.network.id, - derivationPath = params.derivationPath.value, + derivationPath = getDerivationPath(), ) } } @@ -77,7 +78,7 @@ internal class CustomTokenFormModel @Inject constructor( }, derivationPath = ClickableFieldUM( label = resourceReference(R.string.custom_token_derivation_path), - value = if (params.derivationPath.id == params.network.id) { + value = if (params.derivationPath == null || params.derivationPath.id == params.network.id) { resourceReference(R.string.custom_token_derivation_path_default) } else { params.derivationPath.networkName @@ -104,7 +105,7 @@ internal class CustomTokenFormModel @Inject constructor( customCurrencyValidator.validateForm( userWalletId = params.userWalletId, networkId = params.network.id, - derivationPath = params.derivationPath.value, + derivationPath = getDerivationPath(), formValues = formValues, ) } @@ -266,6 +267,10 @@ internal class CustomTokenFormModel @Inject constructor( return formValues.fillValues(form) } + private fun getDerivationPath(): Network.DerivationPath { + return params.derivationPath?.value ?: params.network.derivationPath + } + private fun updateContractAddress(value: String) { state.update { state -> state.updateTokenForm { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt new file mode 100644 index 0000000000..611c6a1fe9 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -0,0 +1,172 @@ +package com.tangem.features.managetokens.model + +import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.managetokens.GetSupportedNetworksUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.DerivationPathSelector +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.NetworkSelector +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM.HeaderUM +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork +import com.tangem.features.managetokens.entity.item.DerivationPathUM +import com.tangem.features.managetokens.entity.item.SelectableItemUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel +import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class CustomTokenSelectorModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase, + private val messageSender: UiMessageSender, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: CustomTokenSelectorComponent.Params = paramsContainer.require() + + val dialogNavigation: SlotNavigation = SlotNavigation() + + val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + init { + loadItems() + } + + private fun getInitialState(): CustomTokenSelectorUM = when (params) { + is NetworkSelector -> CustomTokenSelectorUM( + header = if (params.selectedNetwork == null) { + HeaderUM.Description + } else { + HeaderUM.None + }, + items = persistentListOf(), + ) + is DerivationPathSelector -> CustomTokenSelectorUM( + header = HeaderUM.CustomDerivationButton( + value = (params.selectedDerivationPath?.value as? Network.DerivationPath.Custom)?.value, + onClick = ::showCustomDerivationInput, + ), + items = persistentListOf(), + ) + } + + private fun loadItems() = modelScope.launch { + val items = when (params) { + is NetworkSelector -> loadNetworks(params).toImmutableList() + is DerivationPathSelector -> loadDerivationPaths(params).toImmutableList() + } + + state.update { state -> + state.copy(items = items) + } + } + + private suspend fun loadNetworks(selector: NetworkSelector): List { + return getSupportedNetworks(selector.userWalletId).map { network -> + network.toCurrencyNetworkModel( + isSelected = network.id == selector.selectedNetwork?.id, + onSelectedStateChange = { + val model = SelectedNetwork( + id = network.id, + name = stringReference(network.name), + derivationPath = network.derivationPath, + canHandleTokens = network.canHandleTokens, + ) + + selector.onNetworkSelected(model) + }, + ) + } + } + + private suspend fun loadDerivationPaths(selector: DerivationPathSelector): List { + val derivationPaths = mutableListOf() + val defaultPath = selector.selectedNetwork.let { network -> + network.toDerivationPathModel( + isSelected = network.id == selector.selectedDerivationPath?.id, + onSelectedStateChange = { + val model = SelectedDerivationPath( + id = network.id, + networkName = resourceReference(R.string.custom_token_derivation_path_default), + value = network.derivationPath, + ) + + selector.onDerivationPathSelected(model) + }, + ) + } + + if (defaultPath != null) { + derivationPaths.add(defaultPath) + } + + getSupportedNetworks(selector.userWalletId) + .mapNotNullTo(derivationPaths) { network -> + if (network.id == selector.selectedNetwork.id) { + return@mapNotNullTo null // Skip default path + } + + network.toDerivationPathModel( + isSelected = network.id == selector.selectedDerivationPath?.id, + onSelectedStateChange = { + val model = SelectedDerivationPath( + id = network.id, + networkName = stringReference(network.name), + value = network.derivationPath, + ) + + selector.onDerivationPathSelected(model) + }, + ) + } + + return derivationPaths + } + + private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List { + return getSupportedNetworksUseCase(userWalletId).getOrElse { e -> + val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)) + messageSender.send(message) + + emptyList() + } + } + + private fun showCustomDerivationInput() { + val config = when (params) { + is NetworkSelector -> return + is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.userWalletId) + } + + dialogNavigation.activate(config) + } + + fun selectCustomDerivationPath(value: SelectedDerivationPath) { + when (params) { + is NetworkSelector -> return + is DerivationPathSelector -> params.onDerivationPathSelected(value) + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt index 6fdacc516a..2e88d82956 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.utils.mapper import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.utils.ui.getIconRes @@ -15,7 +16,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( return if (isExpanded) { NetworksUM.Expanded( networks = availableNetworks.map { - it.toUiModel( + it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, @@ -27,7 +28,22 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( } } -private fun SourceNetwork.toUiModel( +internal fun Network.toCurrencyNetworkModel( + isSelected: Boolean, + onSelectedStateChange: (Boolean) -> Unit, +): CurrencyNetworkUM { + return CurrencyNetworkUM( + network = this, + name = name, + iconResId = id.getIconRes(isColored = true), + isSelected = isSelected, + type = standardType.name, + isMainNetwork = false, + onSelectedStateChange = onSelectedStateChange, + ) +} + +private fun SourceNetwork.toCurrencyNetworkModel( isSelected: Boolean, isEditable: Boolean, onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt new file mode 100644 index 0000000000..2e843ec716 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/DerivationPathMapper.kt @@ -0,0 +1,34 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork +import com.tangem.features.managetokens.entity.item.DerivationPathUM +import com.tangem.features.managetokens.impl.R + +internal fun Network.toDerivationPathModel( + isSelected: Boolean, + onSelectedStateChange: (Boolean) -> Unit, +): DerivationPathUM? { + return DerivationPathUM( + id = id.value, + value = derivationPath.value ?: return null, + networkName = stringReference(name), + isSelected = isSelected, + onSelectedStateChange = onSelectedStateChange, + ) +} + +internal fun SelectedNetwork.toDerivationPathModel( + isSelected: Boolean, + onSelectedStateChange: (Boolean) -> Unit, +): DerivationPathUM? { + return DerivationPathUM( + id = id.value, + value = derivationPath.value ?: return null, + networkName = resourceReference(R.string.custom_token_derivation_path_default), + isSelected = isSelected, + onSelectedStateChange = onSelectedStateChange, + ) +} \ No newline at end of file From f78d948799d5a241446cbd49ac626e87f89fc1d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 18:06:37 +0400 Subject: [PATCH 084/197] Updated on 2026-08-14 --- .../ui/CustomTokenSelectorContent.kt | 104 ++++++++++++------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 5984b55cf3..476433fb7d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -35,6 +35,7 @@ import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.entity.item.DerivationPathUM import com.tangem.features.managetokens.impl.R @@ -110,6 +111,8 @@ private fun Header(header: CustomTokenSelectorUM.HeaderUM, modifier: Modifier = is CustomTokenSelectorUM.HeaderUM.CustomDerivationButton -> { CustomDerivationButton( modifier = modifier.padding(vertical = TangemTheme.dimens.spacing16), + enteredDerivationPath = header.value, + isSelected = header.value != null, onClick = header.onClick, ) } @@ -164,38 +167,41 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) ) }, action = { - AnimatedVisibility( - modifier = Modifier.size(TangemTheme.dimens.size24), - visible = model.isSelected, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } + SelectedIcon(isVisible = model.isSelected) }, ) } @Composable -private fun CustomDerivationButton(onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun CustomDerivationButton( + enteredDerivationPath: String?, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { InformationBlock( - modifier = modifier.clickable(onClick = onClick), + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = onClick), title = { - Text( - text = stringResource(id = R.string.custom_token_custom_derivation), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = stringResource(id = R.string.custom_token_custom_derivation), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = enteredDerivationPath ?: stringResource(id = R.string.custom_token_custom_derivation_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } }, - content = { - Text( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = stringResource(id = R.string.custom_token_custom_derivation_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + action = { + SelectedIcon(isVisible = isSelected) }, ) } @@ -206,23 +212,43 @@ private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Mod modifier = modifier, shape = RectangleShape, title = { - Text( - text = model.networkName.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = model.networkName.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = model.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } }, - content = { - Text( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = model.value, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + action = { + SelectedIcon(isVisible = model.isSelected) }, ) } +@Composable +private fun SelectedIcon(isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier.size(TangemTheme.dimens.size24), + visible = isVisible, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) @@ -243,6 +269,12 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = SelectedNetwork( + id = Network.ID(value = "0"), + name = stringReference("Ethereum"), + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + canHandleTokens = true, + ), selectedDerivationPath = SelectedDerivationPath( id = Network.ID(value = "0"), value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), From b37227d5331393f94ef9b59fdea3323712be338e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 19:04:31 +0400 Subject: [PATCH 085/197] Updated on 2026-08-14 --- .../tangem/tap/di/domain/CardDomainModule.kt | 8 ++++ .../card/DefaultDerivationsRepository.kt | 24 ++++++++++ .../card/HasMissedDerivationsUseCase.kt | 23 ++++++++++ .../card/repository/DerivationsRepository.kt | 6 +++ features/markets/impl/build.gradle.kts | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 18 ++++---- .../impl/model/MarketsPortfolioModel.kt | 38 +++++++++++++-- .../impl/model/MyPortfolioUMMFactory.kt | 46 ++++++++----------- .../portfolio/impl/model/PortfolioUIData.kt | 20 ++++++++ 9 files changed, 144 insertions(+), 40 deletions(-) create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index c74a09521c..ce9ed6e593 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -92,4 +92,12 @@ internal object CardDomainModule { fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase { return NetworkHasDerivationUseCase() } + + @Provides + @Singleton + fun provideIsRequiredDerivePublicKeysUseCase( + derivationsRepository: DerivationsRepository, + ): HasMissedDerivationsUseCase { + return HasMissedDerivationsUseCase(derivationsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index a5d6f883b7..4390c3ca9f 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -1,5 +1,7 @@ package com.tangem.tap.domain.card +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.core.TangemSdkError @@ -8,8 +10,10 @@ import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -52,6 +56,26 @@ internal class DefaultDerivationsRepository( derivePublicKeys(userWalletId = userWalletId, derivations = derivations) } + override suspend fun hasMissedDerivations( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") + + val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse) + .findByNetworks( + networksWithDerivationPath.mapNotNull { (networkId, extraDerivationPath) -> + getNetwork( + blockchain = Blockchain.fromNetworkId(networkId.value) ?: return@mapNotNull null, + extraDerivationPath = extraDerivationPath, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) + }, + ) + + return derivations.isNotEmpty() + } + override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys { tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) .doOnSuccess { response -> diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt new file mode 100644 index 0000000000..5d481fe1ca --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case to check if user has missed derivations + * +[REDACTED_AUTHOR] + */ +class HasMissedDerivationsUseCase( + private val derivationsRepository: DerivationsRepository, +) { + + /** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend operator fun invoke( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean { + return derivationsRepository.hasMissedDerivations(userWalletId, networksWithDerivationPath) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt index 5c6512a557..c4abbb8284 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt @@ -20,4 +20,10 @@ interface DerivationsRepository { userWalletId: UserWalletId, derivations: Map>, ): Map + + /** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend fun hasMissedDerivations( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 410e6dbd11..3fc641b0da 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.card) implementation(projects.domain.markets) implementation(projects.domain.staking.models) implementation(projects.domain.tokens) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 9983cd1bb2..6b8140850f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -36,21 +36,21 @@ internal class AddToPortfolioBSContentUMFactory( /** * Create [TangemBottomSheetConfig] * - * @param portfolioData portfolio data - * @param portfolioBSVisibilityModel portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param networksWithToggle networks with toggle - * @param isUserChangedNetworks flag indicates if user changed networks + * @param portfolioData portfolio data + * @param portfolioUIData portfolio bottom sheet visibility model + * @param selectedWallet selected wallet + * @param networksWithToggle networks with toggle + * @param isUserChangedNetworks flag indicates if user changed networks */ fun create( portfolioData: PortfolioData, - portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + portfolioUIData: PortfolioUIData, selectedWallet: UserWallet, networksWithToggle: Map, isUserChangedNetworks: Boolean, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( - isShow = portfolioBSVisibilityModel.addToPortfolioBSVisibility, + isShow = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, content = AddToPortfolioBSContentUM( selectedWallet = selectedWallet.toSelectedUserWalletItemUM(), @@ -58,11 +58,11 @@ internal class AddToPortfolioBSContentUMFactory( networksWithToggle = networksWithToggle, onNetworkSwitchClick = onNetworkSwitchClick, ).convert(value = token), - isScanCardNotificationVisible = false, // TODO [REDACTED_JIRA] + isScanCardNotificationVisible = portfolioUIData.hasMissedDerivations, continueButtonEnabled = isUserChangedNetworks, onContinueButtonClick = onContinueClick, walletSelectorConfig = crateWalletSelectorBSConfig( - isShow = portfolioBSVisibilityModel.walletSelectorBSVisibility, + isShow = portfolioUIData.portfolioBSVisibilityModel.walletSelectorBSVisibility, portfolioData = portfolioData, selectedWalletId = selectedWallet.walletId, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 219da20190..5d647ae942 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -5,8 +5,10 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent @@ -24,6 +26,7 @@ internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val portfolioDataLoader: PortfolioDataLoader, + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -85,16 +88,45 @@ internal class MarketsPortfolioModel @Inject constructor( private fun subscribeOnStateUpdates() { combine( flow = portfolioDataLoader.load(params.token.id), - flow2 = portfolioBSVisibilityModelFlow, + flow2 = getPortfolioUIDataFlow(), flow3 = availableNetworksFlow, - flow4 = selectedMultiWalletIdFlow, - flow5 = walletsWithChangedNetworksFlow, transform = factory::create, ) .onEach { _state.value = it } .launchIn(modelScope) } + private fun getPortfolioUIDataFlow(): Flow { + return combine( + flow = portfolioBSVisibilityModelFlow, + flow2 = selectedMultiWalletIdFlow, + flow3 = walletsWithChangedNetworksFlow, + transform = { portfolioBSVisibilityModel, selectedWalletId, walletsWithChangedNetworks -> + PortfolioUIData( + portfolioBSVisibilityModel = portfolioBSVisibilityModel, + selectedWalletId = selectedWalletId, + walletsWithChangedNetworks = walletsWithChangedNetworks, + hasMissedDerivations = hasMissedDerivations(selectedWalletId, walletsWithChangedNetworks), + ) + }, + ) + } + + private suspend fun hasMissedDerivations( + selectedWalletId: UserWalletId?, + walletsWithChangedNetworks: Map>, + ): Boolean { + return if (selectedWalletId != null) { + hasMissedDerivationsUseCase.invoke( + userWalletId = selectedWalletId, + networksWithDerivationPath = walletsWithChangedNetworks[selectedWalletId].orEmpty() + .associate { Network.ID(it) to null }, + ) + } else { + false + } + } + private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) { _state.update { val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt index 91072c3906..109e54ad83 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt @@ -5,7 +5,6 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @@ -24,17 +23,15 @@ internal class MyPortfolioUMMFactory( ) { fun create( - portfolioModel: PortfolioData, - portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + portfolioData: PortfolioData, + portfolioUIData: PortfolioUIData, availableNetworks: List?, - selectedWalletId: UserWalletId?, - walletsWithChangedNetworks: Map>, ): MyPortfolioUM { if (availableNetworks == null) return MyPortfolioUM.Loading if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable - val walletsWithStatuses = portfolioModel.walletsWithCurrencyStatuses + val walletsWithStatuses = portfolioData.walletsWithCurrencyStatuses .filterAvailableNetworks(networks = availableNetworks) val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty() @@ -44,11 +41,9 @@ internal class MyPortfolioUMMFactory( return if (hasMultiWallets) { MyPortfolioUM.AddFirstToken( bsConfig = createAddToPortfolioBSConfig( - portfolioModel = portfolioModel, - selectedWalletId = selectedWalletId, + portfolioData = portfolioData, + portfolioUIData = portfolioUIData, availableNetworks = availableNetworks, - walletsWithChangedNetworks = walletsWithChangedNetworks, - portfolioBSVisibilityModel = portfolioBSVisibilityModel, ), onAddClick = onAddClick, ) @@ -58,15 +53,13 @@ internal class MyPortfolioUMMFactory( } return TokensPortfolioUMConverter( - appCurrency = portfolioModel.appCurrency, - isBalanceHidden = portfolioModel.isBalanceHidden, + appCurrency = portfolioData.appCurrency, + isBalanceHidden = portfolioData.isBalanceHidden, isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks), bsConfig = createAddToPortfolioBSConfig( - portfolioModel = portfolioModel, - selectedWalletId = selectedWalletId, + portfolioData = portfolioData, + portfolioUIData = portfolioUIData, availableNetworks = availableNetworks, - portfolioBSVisibilityModel = portfolioBSVisibilityModel, - walletsWithChangedNetworks = walletsWithChangedNetworks, ), onAddClick = onAddClick, onTokenItemClick = onTokenItemClick, @@ -75,23 +68,20 @@ internal class MyPortfolioUMMFactory( } private fun createAddToPortfolioBSConfig( - portfolioModel: PortfolioData, - selectedWalletId: UserWalletId?, + portfolioData: PortfolioData, + portfolioUIData: PortfolioUIData, availableNetworks: List, - portfolioBSVisibilityModel: PortfolioBSVisibilityModel, - walletsWithChangedNetworks: Map>, ): TangemBottomSheetConfig { - val walletId = requireNotNull(selectedWalletId) { - "Selected wallet must be not null in state with AddToPortfolio bottom sheet" - } + val walletId = portfolioUIData.selectedWalletId + ?: portfolioData.walletsWithCurrencyStatuses.keys.firstOrNull { it.isMultiCurrency }?.walletId - val selectedWallet = portfolioModel.walletsWithCurrencyStatuses.keys + val selectedWallet = portfolioData.walletsWithCurrencyStatuses.keys .firstOrNull { it.walletId == walletId } ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") - val changedNetworks = walletsWithChangedNetworks[selectedWalletId] + val changedNetworks = portfolioUIData.walletsWithChangedNetworks[portfolioUIData.selectedWalletId] val alreadyAddedNetworks = requireNotNull( - value = portfolioModel.walletsWithCurrencyStatuses[selectedWallet], + value = portfolioData.walletsWithCurrencyStatuses[selectedWallet], lazyMessage = { "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" }, @@ -100,8 +90,8 @@ internal class MyPortfolioUMMFactory( .map { it.currency.network.backendId } return addToPortfolioBSContentUMFactory.create( - portfolioData = portfolioModel, - portfolioBSVisibilityModel = portfolioBSVisibilityModel, + portfolioData = portfolioData, + portfolioUIData = portfolioUIData, selectedWallet = selectedWallet, networksWithToggle = availableNetworks.associateWithToggle( changedNetworks = changedNetworks, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt new file mode 100644 index 0000000000..fcb0dd86c0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Portfolio UI data. Combined data from all UI flows that required to setup portfolio + * + * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model + * @property selectedWalletId selected wallet id + * @property walletsWithChangedNetworks wallets with changed networks + * @property hasMissedDerivations flag that indicates if user has missed derivations + * +[REDACTED_AUTHOR] + */ +internal data class PortfolioUIData( + val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, + val selectedWalletId: UserWalletId?, + val walletsWithChangedNetworks: Map>, + val hasMissedDerivations: Boolean, +) \ No newline at end of file From 1976f57045efd24185631ceff15a04cff2881670 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 19:50:17 +0500 Subject: [PATCH 086/197] Updated on 2026-08-14 --- app/src/main/assets/testnet_tokens.json | 10 ++++++++++ .../core/ui/extensions/BlockchainIcons.kt | 5 +++++ core/ui/src/main/res/drawable/ic_sei_22.xml | 10 ++++++++++ core/ui/src/main/res/drawable/img_sei_22.xml | 20 +++++++++++++++++++ gradle/dependencies.toml | 2 +- .../tangem/blockchainsdk/utils/Blockchain.kt | 5 +++++ 6 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/res/drawable/ic_sei_22.xml create mode 100644 core/ui/src/main/res/drawable/img_sei_22.xml diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 409a6b6067..cb960e3694 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -708,6 +708,16 @@ "networkId": "cyber/test" } ] + }, + { + "id": "sei-network", + "name": "Sei Network", + "symbol": "SEI", + "networks": [ + { + "networkId": "sei-network/test" + } + ] } ] } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index f59561c295..97adf7cbe4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -73,6 +73,7 @@ fun getActiveIconRes(blockchainId: String): Int { "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 + "sei", "sei/test" -> R.drawable.img_sei_22 else -> R.drawable.ic_alert_24 } } @@ -147,6 +148,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 + "sei", "sei/test" -> R.drawable.img_sei_22 else -> R.drawable.ic_alert_24 } } @@ -218,6 +220,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 + "sei", "sei/test" -> R.drawable.img_sei_22 else -> R.drawable.ic_alert_24 } } @@ -292,6 +295,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "blast", "blast/test" -> R.drawable.ic_blast_22 "filecoin" -> R.drawable.ic_filecoin_22 "cyber", "cyber/test" -> R.drawable.ic_cyber_22 + "sei", "sei/test" -> R.drawable.ic_sei_22 else -> R.drawable.ic_alert_24 } } @@ -366,6 +370,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "blast", "blast/test" -> R.drawable.ic_blast_22 "filecoin" -> R.drawable.ic_filecoin_22 "cyber", "cyber/test" -> R.drawable.ic_cyber_22 + "sei", "sei/test" -> R.drawable.ic_sei_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_sei_22.xml b/core/ui/src/main/res/drawable/ic_sei_22.xml new file mode 100644 index 0000000000..576382fca3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_sei_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_sei_22.xml b/core/ui/src/main/res/drawable/img_sei_22.xml new file mode 100644 index 0000000000..39ecb53ae8 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_sei_22.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index fc08cd3ad3..0d71031a5e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-750" +tangemBlockchainSdk = "develop-755" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 8d53ec6fb6..22942b8e18 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -123,6 +123,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "blast/test" -> Blockchain.BlastTestnet "cyber" -> Blockchain.Cyber "cyber/test" -> Blockchain.CyberTestnet + "sei-network" -> Blockchain.Sei + "sei-network/test" -> Blockchain.SeiTestnet else -> null } } @@ -247,6 +249,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.BlastTestnet -> "blast/test" Blockchain.Cyber -> "cyber" Blockchain.CyberTestnet -> "cyber/test" + Blockchain.Sei -> "sei-network" + Blockchain.SeiTestnet -> "sei-network/test" } } @@ -329,6 +333,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Filecoin -> "filecoin" Blockchain.Blast, Blockchain.BlastTestnet -> "blast-ethereum" Blockchain.Cyber, Blockchain.CyberTestnet -> "cyberconnect" + Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network" } } From 050823f7a180d94ca96f2b8757fc4492a5c53f92 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Sep 2024 12:02:40 +0400 Subject: [PATCH 087/197] Updated on 2026-08-14 --- .../managetokens/model/ManageTokensModel.kt | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) 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 581f40fc1b..030e583e14 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 @@ -29,6 +29,7 @@ import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -64,6 +65,8 @@ internal class ManageTokensModel @Inject constructor( ::updateChangedItems, ).launchIn(modelScope) + observeSearchQueryChanges() + modelScope.launch { manageTokensListManager.launchPagination(params.userWalletId) } @@ -132,6 +135,31 @@ internal class ManageTokensModel @Inject constructor( ) } + @OptIn(FlowPreview::class) + private fun observeSearchQueryChanges() { + state + .distinctUntilChanged { old, new -> + // It's also used to skip search activation to avoid searching an empty query + old.search.query == new.search.query && + (old.search.isActive == new.search.isActive || new.search.isActive) + } + .transform { state -> + val query = state.search.query + + if (state.search.isActive) { + emit(query) + } + } + .sample(periodMillis = 1_000) + .onEach { query -> + manageTokensListManager.search( + userWalletId = params.userWalletId, + query = query, + ) + } + .launchIn(modelScope) + } + private fun updateItems(items: ImmutableList) { state.update { state -> state.copySealed( @@ -256,26 +284,15 @@ internal class ManageTokensModel @Inject constructor( ), ) } - - modelScope.launch { - manageTokensListManager.search(params.userWalletId, query) - } } private fun toggleSearchBar(isActive: Boolean) { state.update { state -> state.copySealed( search = state.search.copy( - query = if (isActive) state.search.query else "", isActive = isActive, ), ) } - - modelScope.launch { - if (!isActive) { - manageTokensListManager.reload(params.userWalletId) - } - } } } \ No newline at end of file From 0387fb8a4f3137e3a05a74c4553c58bdd3714691 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Sep 2024 13:28:13 +0500 Subject: [PATCH 088/197] Updated on 2026-08-14 --- .../response/model/YieldBalanceWrapperDTO.kt | 3 +++ .../local/token/DefaultStakingBalanceStore.kt | 14 ++++++------- .../local/token/StakingBalanceStore.kt | 5 ++--- .../data/staking/DefaultStakingRepository.kt | 19 ++++------------- .../converters/YieldBalanceConverter.kt | 21 +++++++------------ .../converters/YieldBalanceListConverter.kt | 9 +------- .../staking/model/stakekit/YieldBalance.kt | 1 + .../operations/CurrencyStatusOperations.kt | 10 +++++---- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 3 +-- .../domain/tokens/mock/MockTokensStates.kt | 5 ++--- 10 files changed, 33 insertions(+), 57 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index 1cd438732e..964bde08a7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -2,12 +2,15 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.request.Address import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import org.joda.time.DateTime import java.math.BigDecimal @JsonClass(generateAdapter = true) data class YieldBalanceWrapperDTO( + @Json(name = "addresses") + val addresses: Address, @Json(name = "balances") val balances: List, @Json(name = "integrationId") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index a80f1b8e05..008cdd1c0f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -1,12 +1,11 @@ package com.tangem.datasource.local.token -import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -30,17 +29,16 @@ internal class DefaultStakingBalanceStore( } } - override fun get(userWalletId: UserWalletId, integrationId: String): Flow> { + override fun get(userWalletId: UserWalletId, integrationId: String): Flow { return dataStore.get(userWalletId.stringValue) - .map { balances -> - balances.filter { it.integrationId == integrationId } - .flatMap { it.balances } + .mapNotNull { balances -> + balances.firstOrNull { it.integrationId == integrationId } } } - override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? { + override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO? { return dataStore.getSyncOrNull(userWalletId.stringValue) - ?.firstOrNull { it.integrationId == integrationId }?.balances + ?.firstOrNull { it.integrationId == integrationId } } override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index 1dc58ba38d..421f03dae1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -1,6 +1,5 @@ package com.tangem.datasource.local.token -import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -13,9 +12,9 @@ interface StakingBalanceStore { suspend fun store(userWalletId: UserWalletId, items: Set) - fun get(userWalletId: UserWalletId, integrationId: String): Flow> + fun get(userWalletId: UserWalletId, integrationId: String): Flow - suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO? suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 24039a6bb3..7635f8e0f4 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -299,6 +299,7 @@ internal class DefaultStakingRepository( YieldBalanceWrapperDTO( balances = result, integrationId = requestBody.integrationId, + addresses = requestBody.addresses, ), ) }, @@ -317,14 +318,7 @@ internal class DefaultStakingRepository( ?: error("Could not get integrationId") stakingBalanceStore.get(userWalletId, integrationId) .collectLatest { - send( - yieldBalanceConverter.convert( - YieldBalanceConverter.Data( - balance = it, - integrationId = integrationId, - ), - ), - ) + send(yieldBalanceConverter.convert(it)) } } @@ -351,12 +345,7 @@ internal class DefaultStakingRepository( val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) ?: return@withContext YieldBalance.Error - yieldBalanceConverter.convert( - YieldBalanceConverter.Data( - balance = result, - integrationId = integrationId, - ), - ) + yieldBalanceConverter.convert(result) } } @@ -385,7 +374,7 @@ internal class DefaultStakingRepository( null } } - .distinct() + .distinctBy { it.second } .map { getBalanceRequestData(it.first, it.second) } .ifEmpty { return@invokeOnExpire } val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index ec86b52e73..66e1e735d1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -1,24 +1,22 @@ package com.tangem.data.staking.converters import com.tangem.data.staking.converters.action.PendingActionConverter -import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceItem +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.stakekit.* import com.tangem.utils.converter.Converter -internal class YieldBalanceConverter : Converter { +internal class YieldBalanceConverter : Converter { private val pendingActionConverter by lazy(LazyThreadSafetyMode.NONE) { PendingActionConverter() } - override fun convert(value: Data): YieldBalance { - return if (value.balance.isEmpty()) { + override fun convert(value: YieldBalanceWrapperDTO): YieldBalance { + return if (value.balances.isEmpty()) { YieldBalance.Empty } else { YieldBalance.Data( + address = value.addresses.address, balance = YieldBalanceItem( - items = value.balance.map { item -> + items = value.balances.map { item -> BalanceItem( type = BalanceType.valueOf(item.type.name), amount = item.amount, @@ -35,9 +33,4 @@ internal class YieldBalanceConverter : Converter, - val integrationId: String?, - ) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt index 007c73bfe1..6a3f3c793a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -15,14 +15,7 @@ internal class YieldBalanceListConverter : Converter YieldBalanceList.Empty } else { YieldBalanceList.Data( - balances = value.map { - converter.convert( - YieldBalanceConverter.Data( - balance = it.balances, - integrationId = it.integrationId, - ), - ) - }, + balances = value.map(converter::convert), ) } } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 1c7c27ac54..c759687602 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -8,6 +8,7 @@ sealed class YieldBalance { data class Data( val balance: YieldBalanceItem, + val address: String, ) : YieldBalance() { fun getTotalStakingBalance(): BigDecimal { return balance.items diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 380f77dc4f..abdf3f94fd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -57,14 +57,16 @@ internal class CurrencyStatusOperations( val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) - + val isCurrentAddressStaking = + (yieldBalance as? YieldBalance.Data)?.address == status.address.defaultAddress.value + val currentYieldBalance = yieldBalance.takeIf { isCurrentAddressStaking } return when { ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, - yieldBalance = yieldBalance, + yieldBalance = currentYieldBalance, ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, @@ -74,7 +76,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, - yieldBalance = yieldBalance, + yieldBalance = currentYieldBalance, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -85,7 +87,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, - yieldBalance = yieldBalance, + yieldBalance = currentYieldBalance, ) } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 824a8e89c2..b96776f92b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -4,7 +4,6 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.error.DataError -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes @@ -123,7 +122,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), - yieldBalance = YieldBalance.Error, + yieldBalance = null, ), ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 78858d98c1..45c2f537d4 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -152,7 +151,7 @@ internal object MockTokensStates { pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, - yieldBalance = YieldBalance.Error, + yieldBalance = null, ), ) } @@ -168,7 +167,7 @@ internal object MockTokensStates { .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, - yieldBalance = YieldBalance.Error, + yieldBalance = null, ), ) } From a7fbf9b3ee2abcc7fb8c288a266d0d1e8ab29abe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Sep 2024 13:21:06 +0300 Subject: [PATCH 089/197] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 5 ++-- .../converters/YieldBalanceConverter.kt | 3 ++- .../domain/staking/model/stakekit/Yield.kt | 12 +++++----- .../staking/model/stakekit/YieldBalance.kt | 2 +- .../converters/YieldBalancesConverter.kt | 11 +++++++-- .../state/stub/StakingClickIntentsStub.kt | 2 +- .../ActionTypeActiveStakeTransformer.kt | 24 +++++++++++++++++++ .../SetButtonsStateTransformer.kt | 3 +-- .../viewmodel/StakingClickIntents.kt | 4 ++-- .../viewmodel/StakingViewModel.kt | 18 ++++++-------- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 ++++ 11 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 24039a6bb3..779640e32c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.raise.catch -import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData @@ -108,8 +107,7 @@ internal class DefaultStakingRepository( value = emptyMap(), ) - private val tronStakeKitTransactionAdapter: JsonAdapter = - moshi.adapter(TronStakeKitTransaction::class.java) + private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) } override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { rawNetworkId.plus(rawCurrencyId) @@ -537,6 +535,7 @@ internal class DefaultStakingRepository( args = ActionRequestBodyArgs( amount = params.amount.toPlainString(), validatorAddress = params.validatorAddress, + validatorAddresses = listOf(params.validatorAddress), ), ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index ec86b52e73..c171ba5acd 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -25,7 +25,8 @@ internal class YieldBalanceConverter : Converter true AVAILABLE, UNSTAKING, PREPARING, REWARDS, - LOCKED, UNLOCKING, UNKNOWN, -> false diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 17c6e659d7..098483c942 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -85,7 +85,7 @@ internal class YieldBalancesConverter( .mapNotNull { balance -> val validator = yield.validators.firstOrNull { balance.validatorAddress?.contains(it.address, ignoreCase = true) == true - } + } ?: mockedValidator val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) val unbonding = getUnbondingDate(balance.date) @@ -130,10 +130,10 @@ internal class YieldBalancesConverter( BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer) BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null + BalanceType.LOCKED -> resourceReference(R.string.staking_locked) to null BalanceType.AVAILABLE -> null to null BalanceType.PREPARING -> resourceReference(R.string.staking_preparing) to null BalanceType.REWARDS -> null to null - BalanceType.LOCKED -> null to null BalanceType.UNLOCKING -> null to null BalanceType.UNKNOWN -> null to null } @@ -164,5 +164,12 @@ internal class YieldBalancesConverter( private companion object { const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000 + + private val mockedValidator = Yield.Validator( + address = "", + status = "", + name = "Mocked validator", + preferred = true, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index b2edad1892..4697e7fe88 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -15,7 +15,7 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) {} + override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) {} override fun onActionClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt new file mode 100644 index 0000000000..1e451a27c2 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.lib.crypto.BlockchainUtils.isTron +import com.tangem.utils.transformer.Transformer + +internal class ActionTypeActiveStakeTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val activeStake: BalanceState, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) + val actionType = if (activeStake.pendingActions.isEmpty() || isTron) { + StakingActionCommonType.EXIT + } else { + StakingActionCommonType.PENDING_OTHER + } + + return prevState.copy(actionType = actionType) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index b18c96f147..5ad39ad9be 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -137,9 +137,8 @@ internal class SetButtonsStateTransformer : Transformer { private fun StakingUiState.onPrimaryClick() { when (currentStep) { StakingStep.InitialInfo -> { - val actionType = StakingActionCommonType.ENTER clickIntents.onAmountValueChange("") // reset amount state - clickIntents.onNextClick(actionType) + clickIntents.onNextClick(StakingActionCommonType.ENTER) } StakingStep.Validators, StakingStep.Amount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 11910f51df..076d61b47d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -15,7 +15,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick(actionType: StakingActionCommonType? = null, pendingAction: PendingAction? = null) + fun onNextClick(actionTypeToOverwrite: StakingActionCommonType? = null, pendingAction: PendingAction? = null) fun onActionClick() @@ -27,7 +27,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun getFee(pendingAction: PendingAction?) - override fun onAmountNext() = onNextClick(actionType = null) + override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null) fun openValidators() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 4eba6fabde..a79ffd5875 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -159,9 +159,9 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick(actionType: StakingActionCommonType?, pendingAction: PendingAction?) { - if (actionType != null) { - stateController.update { it.copy(actionType = actionType) } + override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) { + if (actionTypeToOverwrite != null) { + stateController.update { it.copy(actionType = actionTypeToOverwrite) } } stakingStateRouter.onNextClick() when { @@ -395,7 +395,7 @@ internal class StakingViewModel @Inject constructor( if (rewards != null && rewards.isSingleItem()) { onActiveStake(rewards.first()) } else { - onNextClick(actionType = StakingActionCommonType.PENDING_REWARDS) + onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS) } } @@ -405,7 +405,7 @@ internal class StakingViewModel @Inject constructor( ShowActionSelectorBottomSheetTransformer( pendingActions = activeStake.pendingActions, onActionSelect = { - onNextClick(actionType = StakingActionCommonType.PENDING_OTHER, pendingAction = it) + onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, pendingAction = it) stateController.update(DismissBottomSheetStateTransformer()) }, onDismiss = { @@ -414,14 +414,10 @@ internal class StakingViewModel @Inject constructor( ), ) } else { - val actionType = if (activeStake.pendingActions.isEmpty()) { - StakingActionCommonType.EXIT - } else { - StakingActionCommonType.PENDING_OTHER - } + stateController.update(ActionTypeActiveStakeTransformer(cryptoCurrencyStatus, activeStake)) stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - onNextClick(actionType, activeStake.pendingActions.firstOrNull()) + onNextClick(null, activeStake.pendingActions.firstOrNull()) } } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 89f796a6b7..bc53484728 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -52,6 +52,11 @@ object BlockchainUtils { return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet } + fun isTron(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet + } + fun isSupportedNetworkId(networkId: String): Boolean { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } From d7374fb83f68bbae42bfe3216b24930f8ccdcf7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 10:12:05 +0300 Subject: [PATCH 090/197] Updated on 2026-08-14 --- .../impl/presentation/state/StakingStateController.kt | 2 ++ .../staking/impl/presentation/state/StakingUiState.kt | 2 ++ .../transformers/SetInitialDataStateTransformer.kt | 1 + .../state/transformers/SetTitleTransformer.kt | 11 ++++++++++- .../staking/impl/presentation/ui/StakingScreen.kt | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 8bb0aef58a..6f007f76cb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -61,7 +61,9 @@ internal class StakingStateController @Inject constructor() { private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, + subtitle = null, clickIntents = StakingClickIntentsStub, + walletName = "", cryptoCurrencyName = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index d06f0e1507..637d40f910 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -22,7 +22,9 @@ import java.math.BigDecimal @Immutable internal data class StakingUiState( val title: TextReference, + val subtitle: TextReference?, val clickIntents: StakingClickIntents, + val walletName: String, val cryptoCurrencyName: String, val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 7d873f5da1..cd6d2f6166 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -65,6 +65,7 @@ internal class SetInitialDataStateTransformer( R.string.staking_title_stake, wrappedList(cryptoCurrencyStatusProvider().currency.name), ), + walletName = userWalletProvider.invoke().name, cryptoCurrencyName = cryptoCurrencyStatusProvider.invoke().currency.name, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index ffb66a5ee1..7ad24a9041 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R @@ -36,6 +37,14 @@ internal object SetTitleTransformer : Transformer { } } - return prevState.copy(title = title) + val subtitle = when (currentStep) { + StakingStep.Confirmation -> stringReference(prevState.walletName) + else -> null + } + + return prevState.copy( + title = title, + subtitle = subtitle, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 9fad6829f4..568d824f9e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -97,6 +97,7 @@ private fun SendAppBar(uiState: StakingUiState) { } AppBarWithBackButtonAndIcon( text = uiState.title.resolveReference(), + subtitle = uiState.subtitle?.resolveReference(), backIconRes = backIcon, onBackClick = uiState.clickIntents::onBackClick, backgroundColor = TangemTheme.colors.background.secondary, From 9de7841edda0859b0b83eb257829d7163137d724 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 11:05:02 +0300 Subject: [PATCH 091/197] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 18 -- .../common/ui/tokens/TokenActionsUtils.kt | 69 ++++++ .../bottomsheets/TangemBottomSheetConfig.kt | 7 +- .../core/ui/components/token/TokenItem.kt | 48 +++-- .../model/ScenarioUnavailabilityReason.kt | 0 .../tokens/GetCryptoCurrencyActionsUseCase.kt | 17 +- .../domain/tokens/legacy/TradeCryptoAction.kt | 10 +- .../domain/tokens/model/TokenActionsState.kt | 6 +- features/markets/impl/build.gradle.kts | 7 + .../impl/model/MarketsTokenDetailsModel.kt | 15 +- .../impl/model/formatter/Formatters.kt | 11 - .../portfolio/impl/loader/PortfolioData.kt | 13 +- .../impl/loader/PortfolioDataLoader.kt | 40 +++- .../model/AddToPortfolioBSContentUMFactory.kt | 4 +- .../impl/model/MarketsPortfolioModel.kt | 42 +++- ...oUMMFactory.kt => MyPortfolioUMFactory.kt} | 38 ++-- .../impl/model/PortfolioTokenUMConverter.kt | 72 ++++++- .../impl/model/TokenActionsHandler.kt | 199 ++++++++++++++++++ .../impl/model/TokensPortfolioUMConverter.kt | 20 +- .../impl/ui/AddToPortfolioBottomSheet.kt | 19 +- .../markets/portfolio/impl/ui/MyPortfolio.kt | 10 +- .../portfolio/impl/ui/PortfolioItem.kt | 11 +- .../impl/ui/PortfolioQuickActions.kt | 73 ++++--- .../impl/ui/TokenActionsBottomSheet.kt | 18 +- .../PreviewAddToPortfolioBSContentProvider.kt | 15 +- .../preview/PreviewMyPortfolioUMProvider.kt | 39 ++-- .../ui/state/AddToPortfolioBSContentUM.kt | 1 + .../portfolio/impl/ui/state/MyPortfolioUM.kt | 6 +- .../impl/ui/state/PortfolioTokenUM.kt | 12 +- .../portfolio/impl/ui/state/QuickActionUM.kt | 2 + .../impl/ui/state/TokenActionsBSContentUM.kt | 40 +++- .../converters/MarketsTokenItemConverter.kt | 12 +- features/tokendetails/impl/build.gradle.kts | 1 + .../state/factory/TokenDetailsStateFactory.kt | 66 +----- features/wallet/impl/build.gradle.kts | 2 +- .../wallet/presentation/WalletFragment.kt | 6 + .../MultiWalletCurrencyActionsConverter.kt | 2 +- .../WalletCurrencyActionsClickIntents.kt | 88 +------- 38 files changed, 687 insertions(+), 372 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt rename domain/tokens/{src/main/kotlin => models/src/main/java}/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt (100%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/{MyPortfolioUMMFactory.kt => MyPortfolioUMFactory.kt} (76%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 9f5d8aecb2..1abdf861b9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkAddress @@ -48,11 +47,6 @@ object TradeCryptoMiddleware { is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.Buy -> proceedBuyAction(state, action) is TradeCryptoAction.Sell -> proceedSellAction(action) - is TradeCryptoAction.Stake -> openStaking( - userWalletId = action.userWalletId, - cryptoCurrencyId = action.cryptoCurrencyId, - yield = action.yield, - ) is TradeCryptoAction.SendToken -> handleNewSendToken(action = action) is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action) } @@ -140,18 +134,6 @@ object TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) { - store.dispatchNavigationAction { - push( - AppRoute.Staking( - userWalletId = userWalletId, - cryptoCurrencyId = cryptoCurrencyId, - yield = yield, - ), - ) - } - } - private fun handleNewSendToken(action: TradeCryptoAction.SendToken) { handleNewSend( userWalletId = action.userWallet.walletId, diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt new file mode 100644 index 0000000000..d33e9330f1 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt @@ -0,0 +1,69 @@ +package com.tangem.common.ui.tokens + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason + +fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { + return when (val unavailabilityReason = this) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.PendingTransaction -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_send, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + } + } + is ScenarioUnavailabilityReason.EmptyBalance -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_send, + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_sell, + ) + } + } + is ScenarioUnavailabilityReason.BuyUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_buy_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotExchangeable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_not_exchangeable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotSupportedBySellService -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.Unreachable -> { + resourceReference( + id = R.string.token_button_unavailability_generic_description, + ) + } + ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( + id = R.string.warning_receive_blocked_hedera_token_association_required_message, + ) + ScenarioUnavailabilityReason.None -> { + throw IllegalArgumentException("The unavailability reason must be other than None") + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt index 0bb0162ccc..d2eb1f9935 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt @@ -11,4 +11,9 @@ data class TangemBottomSheetConfig( val isShow: Boolean, val onDismissRequest: () -> Unit, val content: TangemBottomSheetConfigContent, -) \ No newline at end of file +) { + + companion object { + val Empty = TangemBottomSheetConfig(false, {}, TangemBottomSheetConfigContent.Empty) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 436f507958..0a519a13e1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -47,12 +48,18 @@ private enum class LayoutId { * >Figma Component */ @Composable -fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { +fun TokenItem( + state: TokenItemState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, + itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), +) { TokenItem( state = state, isBalanceHidden = isBalanceHidden, modifier = modifier, reorderableTokenListState = null, + itemPaddingValues = itemPaddingValues, ) } @@ -63,6 +70,7 @@ fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifie * @param isBalanceHidden flag that shows/hides balance * @param reorderableTokenListState reorderable token list state * @param modifier modifier + * @param itemPaddingValues padding values * * @see Figma Component @@ -73,12 +81,15 @@ fun TokenItem( isBalanceHidden: Boolean, reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier, + itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12), ) { val betweenRowsMargin = TangemTheme.dimens.spacing2 CustomContainer( state = state, - modifier = modifier.tokenClickable(state = state), + modifier = modifier + .tokenClickable(state = state) + .padding(itemPaddingValues), ) { CurrencyIcon( state = state.iconState, @@ -163,10 +174,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier Layout(content = content, modifier = modifier) { measurables, constraints -> val layoutWidth = constraints.maxWidth - val horizontalPadding = with(density) { dimens.size12.roundToPx() } val verticalPadding = with(density) { dimens.size15.roundToPx() } - val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding - val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() @@ -202,35 +210,35 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier -> { fiatAmount = measurables.measureFiatAmount( state = state, - maxWidth = layoutWidthWithoutPaddings - icon.width - titleMinWidth, + maxWidth = layoutWidth - icon.width - titleMinWidth, defaultConstraints = constraints, ) cryptoAmount = measurables.measureCryptoAmount( state = state, - maxWidth = layoutWidthWithoutPaddings - icon.width - priceChangeMinWidth, + maxWidth = layoutWidth - icon.width - priceChangeMinWidth, defaultConstraints = constraints, ) - firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - fiatAmount.width - secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - cryptoAmount.width + firstRowRemainingFreeSpace = layoutWidth - icon.width - fiatAmount.width + secondRowRemainingFreeSpace = layoutWidth - icon.width - cryptoAmount.width } is TokenItemState.Draggable -> { cryptoAmount = measurables.measureCryptoAmount( state = state, - maxWidth = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width, + maxWidth = layoutWidth - icon.width - nonFiatContent.width, defaultConstraints = constraints, ) - firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width } is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { - firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width if (state.subtitleState != null) { - secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + secondRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width } } } @@ -262,10 +270,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) layout(width = constraints.maxWidth, height = layoutHeight) { - icon.placeRelative(x = horizontalPadding, y = (layoutHeight - icon.height).div(other = 2)) + icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2)) title.placeRelative( - x = horizontalPadding + icon.width, + x = icon.width, y = when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, @@ -280,23 +288,23 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier }, ) - fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - horizontalPadding, y = verticalPadding) + fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width, y = verticalPadding) priceChange?.placeRelative( - x = horizontalPadding + icon.width, + x = icon.width, y = layoutHeight - priceChange.height - verticalPadding, ) cryptoAmount?.placeRelative( x = when (state) { - is TokenItemState.Draggable -> horizontalPadding + icon.width - else -> layoutWidth - cryptoAmount.width - horizontalPadding + is TokenItemState.Draggable -> icon.width + else -> layoutWidth - cryptoAmount.width }, y = layoutHeight - cryptoAmount.height - verticalPadding, ) nonFiatContent.placeRelative( - x = layoutWidth - nonFiatContent.width - horizontalPadding, + x = layoutWidth - nonFiatContent.width, y = (layoutHeight - nonFiatContent.height).div(other = 2), ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt similarity index 100% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index c83af879ba..99ec404379 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -130,11 +130,24 @@ class GetCryptoCurrencyActionsUseCase( // staking if (stakingFeatureToggles.isStakingEnabled) { if (isStakingAvailable(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None)) + val yield = kotlin.runCatching { + stakingRepository.getYield( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) + }.getOrNull() + + activeList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.None, + yield = yield, + ), + ) } else { disabledList.add( TokenActionsState.ActionState.Stake( unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + yield = null, ), ) } @@ -254,7 +267,7 @@ class GetCryptoCurrencyActionsUseCase( actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } if (stakingFeatureToggles.isStakingEnabled) { - actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable)) + actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) } actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 706493a6ea..b32c7a091f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -1,10 +1,8 @@ package com.tangem.domain.tokens.legacy -import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import org.rekotlin.Action import java.math.BigDecimal @@ -24,6 +22,7 @@ sealed class TradeCryptoAction : Action { val appCurrencyCode: String, ) : TradeCryptoAction() + @Deprecated("Use AppRoute instead") data class SendToken( val userWallet: UserWallet, val tokenCurrency: CryptoCurrency.Token, @@ -33,6 +32,7 @@ sealed class TradeCryptoAction : Action { val transactionInfo: TransactionInfo? = null, ) : TradeCryptoAction() + @Deprecated("Use AppRoute instead") data class SendCoin( val userWallet: UserWallet, val coinStatus: CryptoCurrencyStatus, @@ -40,12 +40,6 @@ sealed class TradeCryptoAction : Action { val transactionInfo: TransactionInfo? = null, ) : TradeCryptoAction() - data class Stake( - val userWalletId: UserWalletId, - val cryptoCurrencyId: CryptoCurrency.ID, - val yield: Yield, - ) : TradeCryptoAction() - data class TransactionInfo( val transactionId: String, val destinationAddress: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index a3dbea0959..c6db8e28e2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.wallets.models.UserWalletId data class TokenActionsState( @@ -20,7 +21,10 @@ data class TokenActionsState( data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Stake(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Stake( + override val unavailabilityReason: ScenarioUnavailabilityReason, + val yield: Yield?, + ) : ActionState() data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 3fc641b0da..8cccec2563 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -29,6 +29,13 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + // FIXME [REDACTED_TASK_KEY] + // Remove the "Buy" and "Sell" actions from the redux middleware. + // Instead, create some kind of interface for such cases. + /* Redux -_- */ + implementation(projects.domain.legacy) + implementation(deps.reKotlin) + /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index e899cfeb23..83b17519eb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -346,9 +346,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartDataProducer.runTransaction { updateLook { - it.copy( - type = getChartTypeByPercent(percent), - ) + it.copy(type = percent.percentChangeType().toChartType()) } } } @@ -356,13 +354,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( private suspend fun updateQuotes(newQuotes: TokenQuotes) { quotesStateUpdater.updateQuotes(newQuotes) - val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) + val percent = newQuotes + .getPercentByInterval(interval = state.value.selectedInterval) chartDataProducer.runTransaction { updateLook { - it.copy( - type = getChartTypeByPercent(percent), - ) + it.copy(type = percent.percentChangeType().toChartType()) } } } @@ -430,7 +427,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartDataProducer.runTransaction { updateLook { it.copy( - type = getChartTypeByPercent(percent), + type = percent.percentChangeType().toChartType(), ) } } @@ -477,7 +474,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( launch { while (true) { delay(timeMillis) - // Update quotes only when content visible on the screen + // Update quotes only when content is visible on the screen isVisibleOnScreen.first { it } loadQuotes() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt index c4cf1f6e3f..d2f2869872 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt @@ -87,15 +87,4 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type { PriceChangeType.DOWN -> MarketChartLook.Type.Falling PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral } -} - -@Suppress("MagicNumber") -internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type { - val scaled = percent?.setScale(4, RoundingMode.HALF_UP) - return when { - scaled == null -> return MarketChartLook.Type.Neutral - scaled > BigDecimal.ZERO -> MarketChartLook.Type.Growing - scaled < BigDecimal.ZERO -> MarketChartLook.Type.Falling - else -> MarketChartLook.Type.Neutral - } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt index 8ac05a4e90..ece4a0a217 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt @@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -11,7 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId /** * Portfolio data. Combined data from all flows that required to setup portfolio * - * @property walletsWithCurrencyStatuses wallets with crypto currency statuses + * @property walletsWithCurrencies wallets with crypto currency statuses * @property appCurrency app currency * @property isBalanceHidden flag that indicates if balance should be hidden * @property walletsWithBalance wallets with total balance @@ -19,8 +20,14 @@ import com.tangem.domain.wallets.models.UserWalletId [REDACTED_AUTHOR] */ internal data class PortfolioData( - val walletsWithCurrencyStatuses: Map>, + val walletsWithCurrencies: Map>, val appCurrency: AppCurrency, val isBalanceHidden: Boolean, val walletsWithBalance: Map>, -) \ No newline at end of file +) { + data class CryptoCurrencyData( + val userWallet: UserWallet, + val status: CryptoCurrencyStatus, + val actions: List, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index e2e08f4b70..0685abffb7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -6,9 +6,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -32,18 +32,19 @@ internal class PortfolioDataLoader @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, ) { /** Load data by [currencyRawId] */ @OptIn(ExperimentalCoroutinesApi::class) fun load(currencyRawId: String): Flow { return combine( - flow = getAllWalletsCryptoCurrencyStatusesFlow(currencyRawId = currencyRawId), + flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), flow2 = getSelectedAppCurrencyFlow(), flow3 = getBalanceHidingSettingsFlow(), - ) { walletsWithCurrencyStatuses, appCurrency, isBalanceHidden -> + ) { walletsWithCurrencies, appCurrency, isBalanceHidden -> PortfolioData( - walletsWithCurrencyStatuses = walletsWithCurrencyStatuses, + walletsWithCurrencies = walletsWithCurrencies, appCurrency = appCurrency, isBalanceHidden = isBalanceHidden, walletsWithBalance = emptyMap(), @@ -52,15 +53,16 @@ internal class PortfolioDataLoader @Inject constructor( // setup balances for wallets from walletsWithCurrencyStatuses .flatMapLatest { portfolioData -> getWalletsWithTotalBalanceFlow( - ids = portfolioData.walletsWithCurrencyStatuses.keys.map(UserWallet::walletId), + ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), ) .map { portfolioData.copy(walletsWithBalance = it) } } } - private fun getAllWalletsCryptoCurrencyStatusesFlow( + @OptIn(ExperimentalCoroutinesApi::class) + private fun getAllWalletsCryptoCurrenciesData( currencyRawId: String, - ): Flow>> { + ): Flow>> { return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) .distinctUntilChanged() .map { walletsWithMaybeStatuses -> @@ -68,6 +70,30 @@ internal class PortfolioDataLoader @Inject constructor( entry.value.mapNotNull { it.getOrNull() } } } + .flatMapLatest { walletsWithStatuses -> + val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> + statuses.map { status -> + getCryptoCurrencyActionsUseCase(wallet, status) + .map { + PortfolioData.CryptoCurrencyData( + userWallet = wallet, + status = status, + actions = it.states, + ) + } + } + } + + combine(actionsFlows) { actions -> + walletsWithStatuses.mapValues { entry -> + entry.value.mapNotNull { status -> + actions.firstOrNull { + it.userWallet == entry.key && it.status == status + } + } + } + } + } } private fun getSelectedAppCurrencyFlow(): Flow { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 6b8140850f..e88ef7d966 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -66,6 +66,8 @@ internal class AddToPortfolioBSContentUMFactory( portfolioData = portfolioData, selectedWalletId = selectedWallet.walletId, ), + // TODO maybe do not show wallets with already added token + isWalletBlockVisible = portfolioData.walletsWithBalance.size > 1, ), ) } @@ -86,7 +88,7 @@ internal class AddToPortfolioBSContentUMFactory( isShow = isShow, onDismissRequest = { onWalletSelectorVisibilityChange(false) }, content = WalletSelectorBSContentUM( - userWallets = portfolioData.walletsWithCurrencyStatuses + userWallets = portfolioData.walletsWithCurrencies .filterKeys(UserWallet::isMultiCurrency) .map { it.key } .map { userWallet -> diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 5d647ae942..891fc3b8aa 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -6,6 +6,8 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.card.HasMissedDerivationsUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -14,20 +16,24 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ComponentScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, - override val dispatchers: CoroutineDispatcherProvider, + private val tokenActionsIntentsFactory: TokenActionsHandler.Factory, ) : Model() { val state: StateFlow get() = _state @@ -45,7 +51,17 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) - private val factory = MyPortfolioUMMFactory( + private val currentAppCurrency = getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + + private val factory = MyPortfolioUMFactory( onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) }, onTokenItemClick = ::onTokenItemClick, addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( @@ -56,6 +72,15 @@ internal class MarketsPortfolioModel @Inject constructor( onWalletSelect = ::onWalletSelect, onContinueClick = ::onContinueClick, ), + tokenActionsHandler = tokenActionsIntentsFactory.create( + currentAppCurrency = Provider { currentAppCurrency.value }, + updateTokenActionsBSConfig = { updateBlock -> + updateTokensState { it.copy(tokenActionsBSConfig = updateBlock(it.tokenActionsBSConfig)) } + }, + updateTokenReceiveBSConfig = { updateBlock -> + updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } + }, + ), ) init { @@ -128,9 +153,7 @@ internal class MarketsPortfolioModel @Inject constructor( } private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) { - _state.update { - val state = _state.value as? MyPortfolioUM.Tokens ?: return@update it - + updateTokensState { state -> state.copy( tokens = state.tokens .mapIndexed { i, tokenUM -> @@ -138,7 +161,7 @@ internal class MarketsPortfolioModel @Inject constructor( if (index == i && id.value == tokenUM.tokenItemState.id) { tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown) } else { - tokenUM + tokenUM.copy(isQuickActionsShown = false) } } .toImmutableList(), @@ -194,4 +217,11 @@ internal class MarketsPortfolioModel @Inject constructor( it.copy(addToPortfolioBSVisibility = true, walletSelectorBSVisibility = isShow) } } + + private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) { + _state.update { stateToUpdate -> + val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate + block(tokensState) + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt similarity index 76% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 109e54ad83..fb632b9acc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -16,10 +16,11 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM * [REDACTED_AUTHOR] */ -internal class MyPortfolioUMMFactory( +internal class MyPortfolioUMFactory( private val onAddClick: () -> Unit, private val onTokenItemClick: (Int, CryptoCurrency.ID) -> Unit, private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, + private val tokenActionsHandler: TokenActionsHandler, ) { fun create( @@ -31,16 +32,16 @@ internal class MyPortfolioUMMFactory( if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable - val walletsWithStatuses = portfolioData.walletsWithCurrencyStatuses + val walletsWithCurrencies = portfolioData.walletsWithCurrencies .filterAvailableNetworks(networks = availableNetworks) - val isPortfolioEmpty = walletsWithStatuses.flatMap { it.value }.isEmpty() + val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() if (isPortfolioEmpty) { - val hasMultiWallets = walletsWithStatuses.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() + val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() return if (hasMultiWallets) { MyPortfolioUM.AddFirstToken( - bsConfig = createAddToPortfolioBSConfig( + addToPortfolioBSConfig = createAddToPortfolioBSConfig( portfolioData = portfolioData, portfolioUIData = portfolioUIData, availableNetworks = availableNetworks, @@ -55,7 +56,7 @@ internal class MyPortfolioUMMFactory( return TokensPortfolioUMConverter( appCurrency = portfolioData.appCurrency, isBalanceHidden = portfolioData.isBalanceHidden, - isAllAvailableNetworksAdded = walletsWithStatuses.isAllAvailableNetworksAdded(availableNetworks), + isAllAvailableNetworksAdded = walletsWithCurrencies.isAllAvailableNetworksAdded(availableNetworks), bsConfig = createAddToPortfolioBSConfig( portfolioData = portfolioData, portfolioUIData = portfolioUIData, @@ -63,8 +64,9 @@ internal class MyPortfolioUMMFactory( ), onAddClick = onAddClick, onTokenItemClick = onTokenItemClick, + quickActionsIntents = tokenActionsHandler, ) - .convert(walletsWithStatuses) + .convert(walletsWithCurrencies) } private fun createAddToPortfolioBSConfig( @@ -73,21 +75,21 @@ internal class MyPortfolioUMMFactory( availableNetworks: List, ): TangemBottomSheetConfig { val walletId = portfolioUIData.selectedWalletId - ?: portfolioData.walletsWithCurrencyStatuses.keys.firstOrNull { it.isMultiCurrency }?.walletId + ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }?.walletId - val selectedWallet = portfolioData.walletsWithCurrencyStatuses.keys + val selectedWallet = portfolioData.walletsWithCurrencies.keys .firstOrNull { it.walletId == walletId } ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") val changedNetworks = portfolioUIData.walletsWithChangedNetworks[portfolioUIData.selectedWalletId] val alreadyAddedNetworks = requireNotNull( - value = portfolioData.walletsWithCurrencyStatuses[selectedWallet], + value = portfolioData.walletsWithCurrencies[selectedWallet], lazyMessage = { "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" }, ) .filterAvailableNetworks(availableNetworks) - .map { it.currency.network.backendId } + .map { it.status.currency.network.backendId } return addToPortfolioBSContentUMFactory.create( portfolioData = portfolioData, @@ -117,7 +119,7 @@ internal class MyPortfolioUMMFactory( } } - private fun Map>.isAllAvailableNetworksAdded( + private fun Map>.isAllAvailableNetworksAdded( availableNetworks: List, ): Boolean { val networkIds = availableNetworks.map { it.networkId } @@ -125,26 +127,26 @@ internal class MyPortfolioUMMFactory( return this // User can add currencies only in multi-currency wallets .filterKeys(UserWallet::isMultiCurrency) - .mapValues { it.value.map { it.currency.network.backendId } } + .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } // Each wallets contains all available networks? .all { it.value.containsAll(networkIds) } } /** Filter map values by available networks [networks] */ - private fun Map>.filterAvailableNetworks( + private fun Map>.filterAvailableNetworks( networks: List, - ): Map> { + ): Map> { return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } } /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ - private fun List.filterAvailableNetworks( + private fun List.filterAvailableNetworks( networks: List, - ): List { + ): List { val networkIds = networks.map(TokenMarketInfo.Network::networkId) return mapNotNull { - it.takeIf { networkIds.contains(it.currency.network.backendId) } + it.takeIf { networkIds.contains(it.status.currency.network.backendId) } } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index 5e05440a85..cf437c80c1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -4,9 +4,15 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList /** * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] @@ -17,27 +23,75 @@ internal class PortfolioTokenUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, -) : Converter, PortfolioTokenUM> { - - override fun convert(value: Pair): PortfolioTokenUM { - val (userWallet, cryptoCurrencyStatus) = value + private val tokenActionsHandler: TokenActionsHandler, +) : Converter { + override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM { val tokenItemStateConverter = TokenItemStateConverter( appCurrency = appCurrency, - titleStateProvider = { TokenItemState.TitleState.Content(text = userWallet.name) }, + titleStateProvider = { TokenItemState.TitleState.Content(text = value.userWallet.name) }, subtitleStateProvider = { - TokenItemState.SubtitleState.TextContent(value = cryptoCurrencyStatus.currency.name) + TokenItemState.SubtitleState.TextContent(value = value.status.currency.name) }, onItemClick = onTokenItemClick, - onItemLongClick = null, + onItemLongClick = { + tokenActionsHandler.onTokenLongClick(cryptoCurrencyData = value) + }, ) return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = cryptoCurrencyStatus), + tokenItemState = tokenItemStateConverter.convert(value = value.status), isBalanceHidden = isBalanceHidden, isQuickActionsShown = false, + quickActions = quickActions(cryptoData = value), + ) + } + + private fun quickActions(cryptoData: PortfolioData.CryptoCurrencyData): PortfolioTokenUM.QuickActions { + return PortfolioTokenUM.QuickActions( + actions = listOfNotNull( + QuickActionUM.Buy.takeIf { + cryptoData.actions.any { + it is TokenActionsState.ActionState.Buy && + it.unavailabilityReason == ScenarioUnavailabilityReason.None + } + }, + QuickActionUM.Exchange.takeIf { + cryptoData.actions.any { + it is TokenActionsState.ActionState.Swap && + it.unavailabilityReason == ScenarioUnavailabilityReason.None + } + }, + QuickActionUM.Receive.takeIf { + cryptoData.actions.any { + it is TokenActionsState.ActionState.Receive && + it.unavailabilityReason == ScenarioUnavailabilityReason.None + } + }, + ).toImmutableList(), onQuickActionClick = { - // TODO: [REDACTED_JIRA] + when (it) { + QuickActionUM.Buy -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.Buy, + cryptoCurrencyData = cryptoData, + ) + QuickActionUM.Exchange -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.Exchange, + cryptoCurrencyData = cryptoData, + ) + QuickActionUM.Receive -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.Receive, + cryptoCurrencyData = cryptoData, + ) + } + }, + onQuickActionLongClick = { + if (it == QuickActionUM.Receive) { + tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.CopyAddress, + cryptoCurrencyData = cryptoData, + ) + } }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt new file mode 100644 index 0000000000..8dea5f71b6 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -0,0 +1,199 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.toImmutableList + +@Suppress("LongParameterList") +@ComponentScoped +internal class TokenActionsHandler @AssistedInject constructor( + private val router: Router, + private val clipboardManager: ClipboardManager, + private val uiMessageSender: UiMessageSender, + private val reduxStateHolder: ReduxStateHolder, + @Assisted + private val currentAppCurrency: Provider, + @Assisted("updateTokenReceiveBSConfig") + private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + @Assisted("updateTokenActionsBSConfig") + private val updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, +) { + + fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + when (action) { + TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Receive -> onReceiveClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData) + } + } + + fun onTokenLongClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + updateTokenActionsBSConfig { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = { + updateTokenActionsBSConfig { + it.copy(isShow = false) + } + }, + content = TokenActionsBSContentUM( + title = cryptoCurrencyData.userWallet.name, + actions = cryptoCurrencyData.actions + .mapNotNull { it.toAction(cryptoCurrencyData) } + .sortedBy { it.order } + .toImmutableList(), + onActionClick = { action -> + handle(action, cryptoCurrencyData) + updateTokenActionsBSConfig { + it.copy(isShow = false) + } + }, + ), + ) + } + } + + private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val cryptoCurrencyStatus = cryptoCurrencyData.status + val currency = cryptoCurrencyStatus.currency + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return + + updateTokenReceiveBSConfig { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = { + updateTokenReceiveBSConfig { + it.copy(isShow = false) + } + }, + content = TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(currency) + .toImmutableList(), + onCopyClick = {}, + onShareClick = {}, + ), + ) + } + } + + private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val cryptoCurrencyStatus = cryptoCurrencyData.status + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return + val addresses = networkAddress.availableAddresses + .mapToAddressModels(cryptoCurrencyStatus.currency) + .toImmutableList() + val defaultAddress = addresses.firstOrNull()?.value ?: return + + clipboardManager.setText(text = defaultAddress) + uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) + } + + private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + reduxStateHolder.dispatch( + TradeCryptoAction.Buy( + userWallet = cryptoCurrencyData.userWallet, + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ), + ) + } + + private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + reduxStateHolder.dispatch( + TradeCryptoAction.Sell( + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ), + ) + } + + private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + router.push( + AppRoute.Swap( + currency = cryptoCurrencyData.status.currency, + userWalletId = cryptoCurrencyData.userWallet.walletId, + ), + ) + } + + private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + router.push( + AppRoute.Send( + userWalletId = cryptoCurrencyData.userWallet.walletId, + currency = cryptoCurrencyData.status.currency, + ), + ) + } + + private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val yield = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake } + ?.let { it as TokenActionsState.ActionState.Stake } + ?.yield ?: return + + router.push( + AppRoute.Staking( + userWalletId = cryptoCurrencyData.userWallet.walletId, + cryptoCurrencyId = cryptoCurrencyData.status.currency.id, + yield = yield, + ), + ) + } + + private fun TokenActionsState.ActionState.toAction( + cryptoCurrencyData: PortfolioData.CryptoCurrencyData, + ): TokenActionsBSContentUM.Action? = when (this) { + is TokenActionsState.ActionState.Buy -> TokenActionsBSContentUM.Action.Buy + is TokenActionsState.ActionState.CopyAddress -> TokenActionsBSContentUM.Action.CopyAddress + is TokenActionsState.ActionState.HideToken -> null + is TokenActionsState.ActionState.Receive -> TokenActionsBSContentUM.Action.Receive + is TokenActionsState.ActionState.Sell -> TokenActionsBSContentUM.Action.Sell + is TokenActionsState.ActionState.Send -> { + TokenActionsBSContentUM.Action.Send.takeIf { + cryptoCurrencyData.status.value.amount.isNullOrZero().not() + } + } + is TokenActionsState.ActionState.Stake -> TokenActionsBSContentUM.Action.Stake + is TokenActionsState.ActionState.Swap -> TokenActionsBSContentUM.Action.Exchange + } + .takeIf { this.unavailabilityReason == ScenarioUnavailabilityReason.None } + + @AssistedFactory + interface Factory { + fun create( + currentAppCurrency: Provider, + @Assisted("updateTokenReceiveBSConfig") + updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + @Assisted("updateTokenActionsBSConfig") + updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + ): TokenActionsHandler + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index e9e61f5618..1614ad4ebb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -5,6 +5,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState import com.tangem.utils.converter.Converter @@ -15,6 +16,7 @@ import kotlinx.collections.immutable.toImmutableList * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class TokensPortfolioUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, @@ -22,25 +24,27 @@ internal class TokensPortfolioUMConverter( private val bsConfig: TangemBottomSheetConfig, private val onAddClick: () -> Unit, private val onTokenItemClick: (index: Int, id: CryptoCurrency.ID) -> Unit, -) : Converter>, MyPortfolioUM.Tokens> { + private val quickActionsIntents: TokenActionsHandler, +) : Converter>, MyPortfolioUM.Tokens> { - override fun convert(value: Map>): MyPortfolioUM.Tokens { + override fun convert(value: Map>): MyPortfolioUM.Tokens { return MyPortfolioUM.Tokens( tokens = value - .flatMap { entry -> - entry.value.map { entry.key to it } - } - .mapIndexed { index, status -> + .flatMap { entry -> entry.value } + .mapIndexed { index, cryptoData -> PortfolioTokenUMConverter( appCurrency = appCurrency, isBalanceHidden = isBalanceHidden, onTokenItemClick = { onTokenItemClick(index, it.currency.id) }, - ).convert(status) + tokenActionsHandler = quickActionsIntents, + ).convert(cryptoData) } .toImmutableList(), buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available, - bsConfig = bsConfig, + addToPortfolioBSConfig = bsConfig, onAddClick = onAddClick, + tokenReceiveBSConfig = TangemBottomSheetConfig.Empty, + tokenActionsBSConfig = TangemBottomSheetConfig.Empty, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 315718d001..f04e4206dc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -73,15 +73,16 @@ private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modif .verticalScroll(state = scrollState) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - UserWalletItem( - state = state.selectedWallet, - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) - - SpacerH12() + if (state.isWalletBlockVisible) { + UserWalletItem( + state = state.selectedWallet, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) + SpacerH12() + } NetworkSelection( modifier = Modifier.fillMaxWidth(), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 8c3b6118fd..dda1777df3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SmallButtonShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -24,8 +25,8 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @Composable internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { @@ -54,8 +55,8 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { } when (state) { - is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.bsConfig) - is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.bsConfig) + is MyPortfolioUM.AddFirstToken -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig) + is MyPortfolioUM.Tokens -> AddToPortfolioBottomSheet(config = state.addToPortfolioBSConfig) else -> Unit } } @@ -105,6 +106,9 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier } } } + + TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig) + TokenActionsBottomSheet(config = state.tokenActionsBSConfig) } @Composable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt index 2b781bd2a7..3b3ed01682 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -25,11 +26,15 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie state = state.tokenItemState, isBalanceHidden = state.isBalanceHidden, modifier = Modifier.background(color = TangemTheme.colors.background.action), + itemPaddingValues = PaddingValues( + start = TangemTheme.dimens.spacing10, + end = TangemTheme.dimens.spacing12, + ), ) PortfolioQuickActions( modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing12) + .background(color = TangemTheme.colors.background.action) .padding( bottom = if (lastInList) { TangemTheme.dimens.spacing12 @@ -37,8 +42,10 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie TangemTheme.dimens.spacing24 }, ), + actions = state.quickActions.actions, isVisible = state.isQuickActionsShown, - onActionClick = state.onQuickActionClick, + onActionClick = state.quickActions.onQuickActionClick, + onActionLongClick = state.quickActions.onQuickActionLongClick, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt index f498942443..ba5ff99531 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt @@ -4,9 +4,7 @@ import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button @@ -15,7 +13,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalLayoutDirection @@ -23,6 +20,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.haptic.TangemHapticEffect @@ -30,34 +28,33 @@ import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList @Composable internal fun PortfolioQuickActions( + actions: ImmutableList, isVisible: Boolean, onActionClick: (QuickActionUM) -> Unit, + onActionLongClick: (QuickActionUM) -> Unit, modifier: Modifier = Modifier, ) { + if (actions.isEmpty()) return + AnimatedVisibility( visible = isVisible, enter = expandVertically(expandFrom = Alignment.Top), exit = shrinkVertically(shrinkTowards = Alignment.Top), ) { Column(modifier = modifier) { - LineSeparator() - QuickActionItem( - state = QuickActionUM.Buy, - onClick = { onActionClick(QuickActionUM.Buy) }, - ) - LineSeparator() - QuickActionItem( - state = QuickActionUM.Exchange, - onClick = { onActionClick(QuickActionUM.Exchange) }, - ) - LineSeparator() - QuickActionItem( - state = QuickActionUM.Receive, - onClick = { onActionClick(QuickActionUM.Receive) }, - ) + actions.fastForEach { action -> + LineSeparator() + QuickActionItem( + state = action, + onClick = { onActionClick(action) }, + onLongClick = { onActionLongClick(action) }.takeIf { action.longClickAvailable }, + ) + } } } } @@ -68,10 +65,9 @@ private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) val lineColor = TangemTheme.colors.stroke.primary val strokeWidth = TangemTheme.dimens.size1 val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val verticalPadding = TangemTheme.dimens.spacing2 - val startPadding = TangemTheme.dimens.spacing18 + val startPadding = TangemTheme.dimens.spacing30 - val height = TangemTheme.dimens.size16 + verticalPadding * 2 + val height = TangemTheme.dimens.size16 Canvas( modifier = modifier @@ -96,31 +92,42 @@ private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) drawLine( color = lineColor, - start = Offset(x, verticalPadding.toPx()), - end = Offset(x, size.height - verticalPadding.toPx()), + start = Offset(x, 0f), + end = Offset(x, size.height), strokeWidth = strokeWidth.toPx(), ) } } -@OptIn(ExperimentalAnimationApi::class) +@OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class) @Composable private fun AnimatedVisibilityScope.QuickActionItem( state: QuickActionUM, onClick: () -> Unit, + onLongClick: (() -> Unit)?, modifier: Modifier = Modifier, ) { val hapticManager = LocalHapticManager.current + val onLongClickInternal: (() -> Unit)? = if (onLongClick != null) { + { + hapticManager.perform(TangemHapticEffect.View.LongPress) + onLongClick() + } + } else { + null + } Row( modifier = modifier .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersMedium) - .clickable { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - } - .padding(horizontal = TangemTheme.dimens.spacing2, vertical = TangemTheme.dimens.spacing3), + .combinedClickable( + onLongClick = onLongClickInternal, + onClick = { + hapticManager.perform(TangemHapticEffect.View.SegmentTick) + onClick() + }, + ) + .padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), ) { @@ -190,8 +197,10 @@ private fun Preview() { modifier = Modifier.background(color = TangemTheme.colors.background.action), ) { PortfolioQuickActions( + actions = QuickActionUM.entries.toImmutableList(), isVisible = isVisible, onActionClick = {}, + onActionLongClick = {}, ) } } @@ -205,8 +214,10 @@ private fun PreviewRtl() { TangemThemePreview(rtl = true) { Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { PortfolioQuickActions( + actions = QuickActionUM.entries.toImmutableList(), isVisible = true, onActionClick = {}, + onActionLongClick = {}, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt index bc725d98f3..57a8133772 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -2,7 +2,6 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -10,12 +9,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SimpleSettingsRow import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle -import com.tangem.core.ui.components.inputrow.InputRowChecked -import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -50,16 +49,15 @@ private fun Content(content: TokenActionsBSContentUM) { else -> CornersToRound.ZERO } - DividerContainer( + Box( modifier = Modifier .clip(cornersToRound.getShape()) - .background(TangemTheme.colors.background.action) - .clickable { content.onActionClick(action) }, - showDivider = index != content.actions.lastIndex, + .background(TangemTheme.colors.background.action), ) { - InputRowChecked( - text = action.text, - checked = false, + SimpleSettingsRow( + title = action.text.resolveReference(), + icon = action.iconRes, + onItemsClick = { content.onActionClick(action) }, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index 725f9518be..3b0e811163 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -3,7 +3,6 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId @@ -54,13 +53,10 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider iconUrl = null, ), isScanCardNotificationVisible = true, + isWalletBlockVisible = true, continueButtonEnabled = true, onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig( - isShow = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), + walletSelectorConfig = TangemBottomSheetConfig.Empty, ), AddToPortfolioBSContentUM( selectedWallet = userWallet, @@ -81,13 +77,10 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider iconUrl = null, ), isScanCardNotificationVisible = true, + isWalletBlockVisible = false, continueButtonEnabled = false, onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig( - isShow = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), + walletSelectorConfig = TangemBottomSheetConfig.Empty, ), ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 02339b5257..247b98ce7d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -2,12 +2,13 @@ package com.tangem.features.markets.portfolio.impl.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { @@ -16,39 +17,29 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider Unit, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt index 4f08610f4d..cee5e178a7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -10,7 +10,9 @@ internal sealed class MyPortfolioUM { data class Tokens( val tokens: ImmutableList, val buttonState: AddButtonState, - val bsConfig: TangemBottomSheetConfig, + val addToPortfolioBSConfig: TangemBottomSheetConfig, + val tokenReceiveBSConfig: TangemBottomSheetConfig, + val tokenActionsBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() { @@ -22,7 +24,7 @@ internal sealed class MyPortfolioUM { } data class AddFirstToken( - val bsConfig: TangemBottomSheetConfig, + val addToPortfolioBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt index 59dc297746..b4e01c2896 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt @@ -1,10 +1,18 @@ package com.tangem.features.markets.portfolio.impl.ui.state import com.tangem.core.ui.components.token.state.TokenItemState +import kotlinx.collections.immutable.ImmutableList internal data class PortfolioTokenUM( val tokenItemState: TokenItemState, val isBalanceHidden: Boolean, val isQuickActionsShown: Boolean, - val onQuickActionClick: (QuickActionUM) -> Unit, -) \ No newline at end of file + val quickActions: QuickActions, +) { + + data class QuickActions( + val actions: ImmutableList, + val onQuickActionClick: (QuickActionUM) -> Unit, + val onQuickActionLongClick: (QuickActionUM) -> Unit, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt index b079b5ce50..dba8458d97 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -11,6 +11,7 @@ internal enum class QuickActionUM( val title: TextReference, val description: TextReference, @DrawableRes val icon: Int, + val longClickAvailable: Boolean = false, ) { Buy( title = resourceReference(R.string.common_buy), @@ -26,5 +27,6 @@ internal enum class QuickActionUM( title = resourceReference(R.string.common_receive), description = resourceReference(R.string.receive_token_description), icon = R.drawable.ic_arrow_down_24, + longClickAvailable = true, ), } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt index 7b2a115f54..83450b1ebc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.impl.ui.state +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference @@ -16,13 +17,38 @@ internal data class TokenActionsBSContentUM( @Immutable enum class Action( val text: TextReference, + @DrawableRes val iconRes: Int, ) { - CopyAddress(text = resourceReference(R.string.common_copy_address)), - Receive(text = resourceReference(R.string.common_receive)), - Sell(text = resourceReference(R.string.common_sell)), - Buy(text = resourceReference(R.string.common_buy)), - Send(text = resourceReference(R.string.common_send)), - Exchange(text = resourceReference(R.string.common_exchange)), - Stake(text = resourceReference(R.string.common_stake)), + CopyAddress( + text = resourceReference(R.string.common_copy_address), + iconRes = R.drawable.ic_copy_24, + ), + Send( + text = resourceReference(R.string.common_send), + iconRes = R.drawable.ic_arrow_up_24, + ), + Receive( + text = resourceReference(R.string.common_receive), + iconRes = R.drawable.ic_arrow_down_24, + ), + Buy( + text = resourceReference(R.string.common_buy), + iconRes = R.drawable.ic_plus_24, + ), + Sell( + text = resourceReference(R.string.common_sell), + iconRes = R.drawable.ic_currency_24, + ), + Exchange( + text = resourceReference(R.string.common_exchange), + iconRes = R.drawable.ic_exchange_horizontal_24, + ), + Stake( + text = resourceReference(R.string.common_stake), + iconRes = R.drawable.ic_staking_24, + ), + ; + + val order: Int = ordinal } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index 295cebedcf..0a482edd91 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -121,16 +121,18 @@ internal class MarketsTokenItemConverter( } } + @Suppress("MagicNumber") private fun TokenMarket.getTrendType(): PriceChangeType { val percent = when (currentTrendInterval) { TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent TrendInterval.D7 -> tokenQuotesShort.weekChangePercent TrendInterval.M1 -> tokenQuotesShort.monthChangePercent - }.setScale(2, RoundingMode.UP) - - return when (percent.compareTo(BigDecimal.ZERO)) { - 1 -> PriceChangeType.UP - -1 -> PriceChangeType.DOWN + } + val scaled = percent.setScale(4, RoundingMode.HALF_UP) + return when { + scaled == null -> PriceChangeType.NEUTRAL + scaled > BigDecimal.ZERO -> PriceChangeType.UP + scaled < BigDecimal.ZERO -> PriceChangeType.DOWN else -> PriceChangeType.NEUTRAL } } diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index de5b9043b9..32efd6bb49 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.core.deepLinks.global) implementation(projects.core.featuretoggles) implementation(projects.core.decompose) + implementation(projects.common.ui) implementation(projects.libs.crypto) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index a13a7b2a77..934b75a0d1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import androidx.paging.PagingData import arrow.core.Either +import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig @@ -12,7 +13,6 @@ import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.NetworkHasDerivationUseCase @@ -216,7 +216,7 @@ internal class TokenDetailsStateFactory( isShow = true, onDismissRequest = clickIntents::onDismissDialog, content = TokenDetailsDialogConfig.DialogContentConfig.DisabledButtonReasonDialogConfig( - text = getUnavailabilityReasonText(unavailabilityReason), + text = unavailabilityReason.getUnavailabilityReasonText(), onConfirmClick = clickIntents::onDismissDialog, ), ), @@ -408,66 +408,4 @@ internal class TokenDetailsStateFactory( }.toImmutableList(), ) } - - private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { - return when (unavailabilityReason) { - is ScenarioUnavailabilityReason.StakingUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_staking_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.PendingTransaction -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_send, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_sell, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - } - } - is ScenarioUnavailabilityReason.EmptyBalance -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_send, - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_sell, - ) - } - } - is ScenarioUnavailabilityReason.BuyUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_buy_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotExchangeable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_not_exchangeable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotSupportedBySellService -> { - resourceReference( - id = R.string.token_button_unavailability_reason_sell_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - ScenarioUnavailabilityReason.Unreachable -> { - resourceReference( - id = R.string.token_button_unavailability_generic_description, - ) - } - ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( - id = R.string.warning_receive_blocked_hedera_token_association_required_message, - ) - ScenarioUnavailabilityReason.None -> { - throw IllegalArgumentException("The unavailability reason must be other than None") - } - } - } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 75b2af5fb3..5aa98a53a1 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -78,7 +78,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.analytics) implementation(projects.domain.visa) - implementation(projects.domain.staking) + implementation(projects.domain.staking.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 9d9206c566..74b81ff310 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.arkivanov.decompose.defaultComponentContext +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.utils.asRouter import com.tangem.core.decompose.context.DefaultAppComponentContext import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.ui.UiDependencies @@ -44,6 +46,9 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var marketsFeatureToggles: MarketsFeatureToggles + @Inject + internal lateinit var appRouter: AppRouter + private var marketsEntryComponent: MarketsEntryComponent? = null private val _walletRouter: InnerWalletRouter @@ -60,6 +65,7 @@ internal class WalletFragment : ComposeFragment() { messageHandler = uiDependencies.eventMessageHandler, dispatchers = coroutineDispatcherProvider, hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), ) marketsEntryComponent = marketsEntryComponentFactory.create(appContext) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 95f595916e..f4009e9c73 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -62,7 +62,7 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Stake -> { title = resourceReference(R.string.common_stake) icon = R.drawable.ic_staking_24 - action = { clickIntents.onStakeClick(cryptoCurrencyStatus) } + action = { clickIntents.onStakeClick(cryptoCurrencyStatus, actionsState.yield) } } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index f00e79aa55..e66128d2c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents -import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -14,7 +14,6 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModel import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -22,7 +21,7 @@ import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.staking.GetYieldUseCase +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency @@ -67,7 +66,7 @@ interface WalletCurrencyActionsClickIntents { fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? @@ -100,7 +99,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, - private val getYieldUseCase: GetYieldUseCase, private val appRouter: AppRouter, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { @@ -380,25 +378,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } - override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) viewModelScope.launch { val userWalletId = stateHolder.getSelectedWalletId() val cryptoCurrency = cryptoCurrencyStatus.currency - val yield = getYieldUseCase.invoke( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, - ).getOrElse { - error("Staking is unavailable for ${cryptoCurrency.name}") - } - reduxStateHolder.dispatch( - TradeCryptoAction.Stake( + appRouter.push( + AppRoute.Staking( userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrency.id, - yield = yield, + yield = yield ?: return@launch, ), ) } @@ -487,14 +479,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false - val unavailabilityReasonText = getUnavailabilityReasonText(unavailabilityReason) - viewModelScope.launch(dispatchers.main) { walletEventSender.send( event = WalletEvent.ShowAlert( state = WalletAlertState.DefaultAlert( title = null, - message = unavailabilityReasonText, + message = unavailabilityReason.getUnavailabilityReasonText(), onConfirmClick = null, ), ), @@ -503,66 +493,4 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( return true } - - private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { - return when (unavailabilityReason) { - is ScenarioUnavailabilityReason.StakingUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_staking_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.PendingTransaction -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_send, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_sell, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - } - } - is ScenarioUnavailabilityReason.EmptyBalance -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_send, - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_sell, - ) - } - } - is ScenarioUnavailabilityReason.BuyUnavailable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_buy_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotExchangeable -> { - resourceReference( - id = R.string.token_button_unavailability_reason_not_exchangeable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - is ScenarioUnavailabilityReason.NotSupportedBySellService -> { - resourceReference( - id = R.string.token_button_unavailability_reason_sell_unavailable, - formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), - ) - } - ScenarioUnavailabilityReason.Unreachable -> { - resourceReference( - id = R.string.token_button_unavailability_generic_description, - ) - } - ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( - id = R.string.warning_receive_blocked_hedera_token_association_required_message, - ) - ScenarioUnavailabilityReason.None -> { - throw IllegalArgumentException("The unavailability reason must be other than None") - } - } - } } \ No newline at end of file From 88021b8169864ddae45720b1f9c35321604c62fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 12:22:21 +0300 Subject: [PATCH 092/197] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 8 +++++- core/res/src/main/res/values-ru/strings.xml | 6 +++-- .../src/main/res/values-uk-rUA/strings.xml | 7 +++--- core/res/src/main/res/values/strings.xml | 5 +++- .../components/inputrow/InputRowImageBase.kt | 20 ++++++++------- .../components/inputrow/InputRowImageInfo.kt | 2 +- .../state/InnerYieldBalanceState.kt | 3 ++- .../RewardsValidatorStateConverter.kt | 1 + .../converters/YieldBalancesConverter.kt | 25 +++++++++++-------- .../previewdata/InitialStakingStatePreview.kt | 1 + .../ValidatorSelectChangeTransformer.kt | 4 ++- .../ui/StakingClaimRewardsValidatorContent.kt | 8 +++--- .../ui/StakingInitialInfoContent.kt | 10 ++++---- 13 files changed, 61 insertions(+), 39 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 54492f5825..b0e97efb9f 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -70,6 +70,7 @@ Zugang verweigert Alle Erlauben + Analysen Anwenden Genehmigung Genehmigen @@ -82,6 +83,7 @@ Gehe zu %1$s D hast keinen Zugang zur Kamera erteilt, bitte passe deine Datenschutzeinstellungen an Abbrechen + Aktion wählen Stakingbelohnungen beanstpruchen Schließen Weiter @@ -354,7 +356,7 @@ Keine Daten Schnelle Aktionen Ergebnis - Token unter 100k Marktkapitalisierung anzeigen + Token unter 100k USD Marktkapitalisierung anzeigen Token anzeigen Kein Ergebnis Netzwerk auswählen @@ -646,6 +648,7 @@ Jährliche prozentuale Rendite Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Effektiver Jahreszins + Belohnungen sammeln sich täglich automatisch in deinem Staking-Konto an. Verfügbar Durchschnittliche Belohnungsquote Was sit Staking? @@ -664,6 +667,7 @@ Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. Aufwärmphase Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. + Gesperrt Migrieren Natives Staking Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag. @@ -672,6 +676,8 @@ Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. + Dein Guthaben wurde freigegeben. Du kannst es jetzt zusammen mit deinen verdienten Prämien auszahlen. + Vorbereitung Bereit zum Abheben Erneut binden Erneut staken diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 50e75be7a4..2f4348ef70 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -357,7 +357,7 @@ Нет данных Быстрые действия Результат - Токены с капитализацией меньше 100к + Токены с капитализацией меньше 100к USD Показать токены Нет результата Выберите сеть @@ -666,7 +666,7 @@ Это период, определяющий, когда участники стейкинга получат свои вознаграждения. Вознаграждение для получения: %s Стейкинг %s - Период вывода + Период отзыва Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. Период прогрева Время, необходимое для начала процесса стейкинга и активации процесса начисления наград @@ -698,10 +698,12 @@ Застейкать еще Застейкать %s Вывести %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 767c26f4fd..83d2675b15 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -351,7 +351,7 @@ Не вдалося завантажити дані... Швидкі дії Результат - Переглянути токени до 100к ринкової капіталізації + Переглянути токени до 100к USD ринкової капіталізації Показати токени Жодного результату Виберіть мережу @@ -674,7 +674,7 @@ Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щомісяця. Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щотиждня. Отримуйте винагороду за стейкінг - Винагороди припиняють нараховуватися одразу після того, як ви знімаєте ставку. Процес зняття займає %s. + Винагорода припиняє нараховуватися одразу після того, як ви почнете процес завершення стейкінгу. Процес завершення триває %s. Зʼєднати ще раз Повторно застейкати Застейкати винагороди @@ -695,7 +695,7 @@ Застейкати %s Зняти зі стейкінгу %s Розблокувати - Скасовано зі стейкінгу + Вивід зі стейкінгу Перевірити незастейкані, щоб отримати свої активи Зняти зі стейкінгу Валідатор @@ -871,6 +871,7 @@ На цій картці залишається лише %s підписів. Ви повинні вивести всі свої кошти. Низька кількість підписів Токени в різних мережах можуть мати різні адреси. Переконайтеся, що ваша адреса відповідає мережі, коли переказуєте кошти. + Токен MATIC мігрує до POL. Однак кінцевого терміну не встановлено і MATIC ще не застарілий. Ви можете безпечно продовжувати використовувати токен MATIC або скористатися біржами, щоб обміняти його на POL. Використовуйте вашу картку, щоб отримати адресу для %d мережі Використовуйте вашу картку, щоб отримати адреси для %d мереж diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 7b30804e92..566ba9fae5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -356,7 +356,7 @@ No data Quick actions Result - See tokens under 100k market cap + See tokens under 100k USD market cap Show tokens No result Select network @@ -644,6 +644,7 @@ Annual percentage rate The annual percentage return you can earn from participating in staking. APR + Rewards automatically accumulate in your staking balance daily. Available Average Reward Rate What is Staking? @@ -671,6 +672,7 @@ Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. + Your funds have been unstaked. You can now withdraw them along with your earned rewards. Preparing Ready to withdraw Rebond @@ -698,6 +700,7 @@ Unstaked Check unstaked to claim your assets Unstaking + Unstaking assets %s Validator Validators Vote diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 0442dc06ce..fd4913770c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -42,7 +42,7 @@ import com.tangem.core.ui.res.TangemThemePreview @Composable internal fun InputRowImageBase( subtitle: TextReference, - imageUrl: String, + imageUrl: String?, modifier: Modifier = Modifier, caption: TextReference? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, @@ -55,14 +55,16 @@ internal fun InputRowImageBase( verticalAlignment = Alignment.CenterVertically, modifier = modifier, ) { - InputRowAsyncImage( - imageUrl = imageUrl, - isGrayscale = isGrayscaleImage, - modifier = Modifier - .size(TangemTheme.dimens.spacing36) - .clip(TangemTheme.shapes.roundedCornersXLarge), - ) - SpacerW12() + if (imageUrl != null) { + InputRowAsyncImage( + imageUrl = imageUrl, + isGrayscale = isGrayscaleImage, + modifier = Modifier + .size(TangemTheme.dimens.spacing36) + .clip(TangemTheme.shapes.roundedCornersXLarge), + ) + SpacerW12() + } Column { Text( text = subtitle.resolveReference(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index 967547e085..16ea439603 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -40,7 +40,7 @@ import com.tangem.core.ui.res.TangemThemePreview fun InputRowImageInfo( subtitle: TextReference, infoTitle: TextReference, - imageUrl: String, + imageUrl: String?, modifier: Modifier = Modifier, title: TextReference? = null, caption: TextReference? = null, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index c2ed6f2d62..3056c85688 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -32,7 +32,8 @@ data class BalanceGroupedState( @Immutable data class BalanceState( - val validator: Yield.Validator, + val validator: Yield.Validator?, + val title: TextReference, val cryptoValue: String, val cryptoDecimal: BigDecimal, val cryptoAmount: TextReference, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index b27bdf6c41..673e81499b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -87,6 +87,7 @@ internal class RewardsValidatorStateConverter( return BalanceState( validator = this, + title = stringReference(this.name), cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), cryptoDecimal = cryptoValue, cryptoAmount = cryptoAmount, 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 098483c942..1524154584 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 @@ -65,7 +65,7 @@ internal class YieldBalancesConverter( val isClickable = item.key.isClickable() title?.let { BalanceGroupedState( - items = item.value.mapBalances().toPersistentList(), + items = item.value.mapBalances(item.key).toPersistentList(), footer = footer, title = it, type = item.key, @@ -76,23 +76,33 @@ internal class YieldBalancesConverter( .filterNot { it.items.isEmpty() } .toPersistentList() - private fun List.mapBalances(): List { + private fun List.mapBalances(balanceType: BalanceType): List { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency + var incrementingIndex = 1 return this .filterNot { it.amount.isZero() } .mapNotNull { balance -> val validator = yield.validators.firstOrNull { balance.validatorAddress?.contains(it.address, ignoreCase = true) == true - } ?: mockedValidator + } + val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) val unbonding = getUnbondingDate(balance.date) val warmupPeriod = yield.metadata.warmupPeriod.days - validator?.let { + val title = when { + validator != null -> stringReference(validator.name) + balanceType == BalanceType.UNSTAKING -> { + resourceReference(R.string.staking_unstaking_item_name, wrappedList(incrementingIndex++)) + } + else -> null + } + title?.let { BalanceState( validator = validator, + title = title, cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), cryptoDecimal = cryptoAmount, cryptoAmount = stringReference( @@ -164,12 +174,5 @@ internal class YieldBalancesConverter( private companion object { const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000 - - private val mockedValidator = Yield.Validator( - address = "", - status = "", - name = "Mocked validator", - preferred = true, - ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 15efd12337..091afc897a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -71,6 +71,7 @@ internal object InitialStakingStatePreview { isClickable = true, items = persistentListOf( BalanceState( + title = stringReference("Binance"), cryptoValue = "100", cryptoAmount = stringReference("100 SOL"), cryptoDecimal = "100".toBigDecimal(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index eba4aae409..e6ba4e32db 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -7,10 +7,12 @@ import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.utils.transformer.Transformer internal class ValidatorSelectChangeTransformer( - private val selectedValidator: Yield.Validator, + private val selectedValidator: Yield.Validator?, ) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + selectedValidator ?: return prevState val validatorState = (confirmationState.validatorState as? ValidatorState.Content)?.copy( chosenValidator = selectedValidator, ) ?: ValidatorState.Content( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt index 3e6723c8b2..cbcb806d97 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -33,16 +33,16 @@ internal fun StakingClaimRewardsValidatorContent( .verticalScroll(rememberScrollState()), ) { state.rewards.forEachIndexed { index, item -> - key(item.validator.address) { + key(item.title.resolveReference() + index) { InputRowImageInfo( - subtitle = stringReference(item.validator.name), + subtitle = item.title, caption = combinedReference( resourceReference(R.string.staking_details_apr), annotatedReference { appendSpace() appendColored( text = BigDecimalFormatter.formatPercent( - percent = item.validator.apr.orZero(), + percent = item.validator?.apr.orZero(), useAbsoluteValue = true, ), color = TangemTheme.colors.text.accent, @@ -51,7 +51,7 @@ internal fun StakingClaimRewardsValidatorContent( ), infoTitle = item.fiatAmount, infoSubtitle = item.cryptoAmount, - imageUrl = item.validator.image.orEmpty(), + imageUrl = item.validator?.image.orEmpty(), modifier = modifier .roundedShapeItemDecoration(index, state.rewards.lastIndex, false) .background(TangemTheme.colors.background.action) 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 c84e8247e3..ec508e38a9 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 @@ -204,15 +204,15 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli end = TangemTheme.dimens.spacing12, ), ) - group.items.forEach { balance -> - key(balance.validator.address) { + group.items.forEachIndexed { index, balance -> + key(balance.title.resolveReference() + index) { InputRowImageInfo( - subtitle = stringReference(balance.validator.name), + subtitle = balance.title, caption = getCaption(group.type, balance), isGrayscaleImage = !group.isClickable, infoTitle = balance.fiatAmount, infoSubtitle = balance.cryptoAmount, - imageUrl = balance.validator.image.orEmpty(), + imageUrl = balance.validator?.image, iconEndRes = R.drawable.ic_chevron_right_24.takeIf { group.isClickable }, modifier = Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, @@ -267,7 +267,7 @@ private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextRef appendSpace() appendColored( text = BigDecimalFormatter.formatPercent( - percent = balance.validator.apr.orZero(), + percent = balance.validator?.apr.orZero(), useAbsoluteValue = true, ), color = TangemTheme.colors.text.accent, From 428b2554fd35388b33f50636d742ed4913234e73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 13:36:09 +0300 Subject: [PATCH 093/197] Updated on 2026-08-14 --- .../tangem/core/ui/components/SettingsRow.kt | 15 +++- .../core/ui/components/buttons/chip/Chip.kt | 73 ++++++++++++++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 9 ++- .../details/impl/ui/components/LinksBlock.kt | 14 ++-- .../impl/loader/PortfolioDataLoader.kt | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 1 - .../impl/ui/AddToPortfolioBottomSheet.kt | 10 ++- .../impl/ui/TokenActionsBottomSheet.kt | 1 + .../markets/tokenlist/impl/ui/MarketsList.kt | 76 ++++++++++++++++--- 9 files changed, 171 insertions(+), 29 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/buttons/chip/Chip.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt index 6b497b4af0..cb5266dce9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.res.TangemTheme /** * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=281-248&mode=design&t=bXqehWPHyATKcZEW-4) - * */ + **/ @Composable fun SimpleSettingsRow( title: String, @@ -26,10 +26,11 @@ fun SimpleSettingsRow( rowColors: RowColors = getDefaultRowColors(), enabled: Boolean = true, subtitle: String? = null, + redesign: Boolean = false, ) { Row( modifier = modifier - .height(TangemTheme.dimens.size56) + .height(if (redesign) TangemTheme.dimens.size48 else TangemTheme.dimens.size56) .fillMaxWidth() .clickable( onClick = { @@ -44,10 +45,16 @@ fun SimpleSettingsRow( Icon( painter = painterResource(id = icon), contentDescription = null, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20), + modifier = Modifier + .padding(horizontal = if (redesign) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing20), tint = rowColors.iconColor(enabled = enabled).value, ) - Column(modifier = Modifier.padding(end = TangemTheme.dimens.spacing20)) { + Column( + modifier = Modifier + .padding( + end = if (redesign) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing20, + ), + ) { Text( text = title, style = TangemTheme.typography.subtitle1, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/chip/Chip.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/chip/Chip.kt new file mode 100644 index 0000000000..3826991070 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/chip/Chip.kt @@ -0,0 +1,73 @@ +package com.tangem.core.ui.components.buttons.chip + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +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.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun Chip(text: TextReference, @DrawableRes iconResId: Int, onClick: () -> Unit, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .defaultMinSize( + minWidth = TangemTheme.dimens.size46, + minHeight = TangemTheme.dimens.size28, + ) + .clip(RoundedCornerShape(size = TangemTheme.dimens.size100)) + .clickable(onClick = onClick) + .background(color = TangemTheme.colors.background.tertiary) + .padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing6, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size16), + tint = TangemTheme.colors.icon.secondary, + ) + + Text( + text = text.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Box(Modifier.background(TangemTheme.colors.background.action)) { + Chip( + text = stringReference("Chip Chip Chip"), + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ) + } + } +} \ 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 23e21f4840..9bbdfb559c 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 @@ -122,6 +122,7 @@ private fun Content( IntervalSelector( trendInterval = state.selectedInterval, onIntervalClick = state.onSelectedIntervalChange, + isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing, modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), @@ -219,6 +220,7 @@ private fun TokenPriceText( @Composable private fun IntervalSelector( trendInterval: PriceChangeInterval, + isEnabled: Boolean, onIntervalClick: (PriceChangeInterval) -> Unit, modifier: Modifier = Modifier, ) { @@ -235,6 +237,7 @@ private fun IntervalSelector( color = TangemTheme.colors.button.secondary, initialSelectedItem = trendInterval, onClick = onIntervalClick, + isEnabled = isEnabled, modifier = modifier, ) { Box( @@ -249,7 +252,11 @@ private fun IntervalSelector( modifier = Modifier.align(Alignment.Center), text = it.getText().resolveReference(), style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, + color = if (isEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.disabled + }, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index 92d905ff2e..cfbb51ec74 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -13,9 +13,7 @@ import androidx.compose.ui.util.fastForEach 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.components.buttons.chip.Chip import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -98,12 +96,10 @@ private fun SubBlock( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { links.fastForEach { - SecondarySmallButton( - config = SmallButtonConfig( - text = it.title, - onClick = { onLinkClick(it) }, - icon = TangemButtonIconPosition.Start(iconResId = it.iconRes), - ), + Chip( + text = it.title, + iconResId = it.iconRes, + onClick = { onLinkClick(it) }, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index 0685abffb7..db10d655ab 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -94,6 +94,7 @@ internal class PortfolioDataLoader @Inject constructor( } } } + .distinctUntilChanged() } private fun getSelectedAppCurrencyFlow(): Flow { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index e88ef7d966..763a431844 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -66,7 +66,6 @@ internal class AddToPortfolioBSContentUMFactory( portfolioData = portfolioData, selectedWalletId = selectedWallet.walletId, ), - // TODO maybe do not show wallets with already added token isWalletBlockVisible = portfolioData.walletsWithBalance.size > 1, ), ) 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 f04e4206dc..19080cb275 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 @@ -128,13 +128,19 @@ private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modif continueButtonAreaHeight = it.size.height }, enabled = state.continueButtonEnabled, + isTangemIconVisible = state.isScanCardNotificationVisible, onClick = state.onContinueButtonClick, ) } } @Composable -private fun ContinueButton(enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun ContinueButton( + enabled: Boolean, + isTangemIconVisible: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { TangemButton( enabled = enabled, modifier = modifier @@ -146,7 +152,7 @@ private fun ContinueButton(enabled: Boolean, onClick: () -> Unit, modifier: Modi .navigationBarsPadding() .fillMaxWidth(), text = stringResource(R.string.common_continue), - icon = if (enabled) { + icon = if (enabled && isTangemIconVisible) { TangemButtonIconPosition.End(R.drawable.ic_tangem_24) } else { TangemButtonIconPosition.None diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt index 57a8133772..7dc8517cd8 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -57,6 +57,7 @@ private fun Content(content: TokenActionsBSContentUM) { SimpleSettingsRow( title = action.text.resolveReference(), icon = action.iconRes, + redesign = true, onItemsClick = { content.onActionClick(action) }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index fa2964129f..375d444943 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager @@ -20,6 +21,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -65,10 +67,13 @@ internal fun MarketsList( ) } +@Suppress("LongMethod") @Composable private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { val density = LocalDensity.current val background = LocalMainBottomSheetColor.current.value + val strokeColor = TangemTheme.colors.stroke.primary + val scrolledState = remember { mutableStateOf(false) } Column( modifier = modifier @@ -82,7 +87,7 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi .padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing4, + bottom = TangemTheme.dimens.spacing12, ) .onGloballyPositioned { if (it.size.height > 0) { @@ -93,23 +98,45 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi }, state = state.searchBar, ) - Spacer(Modifier.height(TangemTheme.dimens.spacing20)) Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) { - Title(isInSearchMode = state.isInSearchMode) - AnimatedVisibility(state.isInSearchMode.not()) { + AnimatedVisibility( + visible = scrolledState.value.not(), + ) { Column { + SpacerH8() + Title(isInSearchMode = state.isInSearchMode) SpacerH12() - Options( - sortByTypeUM = state.selectedSortBy, - trendInterval = state.selectedInterval, - onIntervalClick = state.onIntervalClick, - onSortByClick = state.onSortByButtonClick, - ) } } + AnimatedVisibility(state.isInSearchMode.not()) { + Options( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + sortByTypeUM = state.selectedSortBy, + trendInterval = state.selectedInterval, + onIntervalClick = state.onIntervalClick, + onSortByClick = state.onSortByButtonClick, + ) + } } - SpacerH12() + val strokeWidth = TangemTheme.dimens.size0_5 + Box( + Modifier + .fillMaxWidth() + .height(strokeWidth) + .drawBehind { + // draw horizontal line + if (scrolledState.value) { + drawLine( + color = strokeColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = strokeWidth.toPx(), + ) + } + }, + ) ItemsList( + scrolledState = scrolledState, isInSearchMode = state.isInSearchMode, state = state.list, ) @@ -184,10 +211,35 @@ private fun Options( } @Composable -private fun ItemsList(isInSearchMode: Boolean, state: ListUM, modifier: Modifier = Modifier) { +private fun ItemsList( + scrolledState: MutableState, + isInSearchMode: Boolean, + state: ListUM, + modifier: Modifier = Modifier, +) { val searchLazyListState = rememberLazyListState() val mainLazyListState = rememberLazyListState() + val mainScrolled by remember { + derivedStateOf { + mainLazyListState.firstVisibleItemScrollOffset > 0 + } + } + + val searchScrolledState by remember { + derivedStateOf { + searchLazyListState.firstVisibleItemScrollOffset > 0 + } + } + + LaunchedEffect(mainScrolled, isInSearchMode, searchScrolledState) { + scrolledState.value = if (isInSearchMode) { + searchScrolledState + } else { + mainScrolled + } + } + MarketsListLazyColumn( modifier = modifier, state = state, From 0d185108da0cf016a68be7c5794ab7066868e64b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 13:25:28 +0400 Subject: [PATCH 094/197] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 12 ++ .../card/DefaultDerivationsRepository.kt | 15 ++ data/markets/build.gradle.kts | 11 +- .../markets/DefaultMarketsTokenRepository.kt | 41 ++++ .../data/markets/di/MarketsDataModule.kt | 3 + .../card/repository/DerivationsRepository.kt | 2 + domain/markets/build.gradle.kts | 6 +- .../domain/markets/SaveMarketTokensUseCase.kt | 60 ++++++ .../repositories/MarketsTokenRepository.kt | 8 + .../model/AddToPortfolioBSContentUMFactory.kt | 24 ++- .../impl/model/AddToPortfolioManager.kt | 188 ++++++++++++++++++ .../impl/model/MarketsPortfolioModel.kt | 85 ++++---- .../impl/model/MyPortfolioUMFactory.kt | 62 ++---- .../portfolio/impl/model/PortfolioUIData.kt | 4 +- 14 files changed, 431 insertions(+), 90 deletions(-) create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index f32af0c735..293ec473c3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.QuotesRepository import dagger.Module import dagger.Provides @@ -44,4 +46,14 @@ object MarketsDomainModule { fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase { return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository) } + + @Provides + @Singleton + fun provideSaveMarketTokensUseCase( + derivationsRepository: DerivationsRepository, + marketsTokenRepository: MarketsTokenRepository, + currenciesRepository: CurrenciesRepository, + ): SaveMarketTokensUseCase { + return SaveMarketTokensUseCase(derivationsRepository, marketsTokenRepository, currenciesRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 4390c3ca9f..72b36175ef 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -38,6 +38,21 @@ internal class DefaultDerivationsRepository( derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) } + override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") + + derivePublicKeysByNetworks( + userWalletId = userWalletId, + networks = networkIds.mapNotNull { + getNetwork( + blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, + extraDerivationPath = null, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) + }, + ) + } + override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 98b32e7c97..7e23136300 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -14,10 +14,16 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) implementation(projects.core.pagination) - implementation(projects.domain.tokens.models) + + implementation(projects.domain.legacy) implementation(projects.domain.markets) + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.data.common) + implementation(projects.libs.blockchainSdk) + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) @@ -28,7 +34,6 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) - - implementation(projects.libs.blockchainSdk) + implementation(deps.tangem.blockchain) // endregion } diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index fa6e9a355e..8f9fa44f1b 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -1,5 +1,9 @@ package com.tangem.data.markets +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetwork import com.tangem.data.common.utils.retryOnError import com.tangem.data.markets.converters.TokenChartConverter import com.tangem.data.markets.converters.TokenMarketInfoConverter @@ -8,8 +12,12 @@ import com.tangem.data.markets.converters.toRequestParam import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -19,6 +27,7 @@ import java.util.concurrent.atomic.AtomicLong internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, private val tangemTechApi: TangemTechApi, + private val userWalletsStore: UserWalletsStore, private val dispatcherProvider: CoroutineDispatcherProvider, ) : MarketsTokenRepository { @@ -148,4 +157,36 @@ internal class DefaultMarketsTokenRepository( return TokenMarketInfoConverter.convert(response.getOrThrow()).quotes } + + override suspend fun createCryptoCurrency( + userWalletId: UserWalletId, + token: TokenMarketParams, + network: TokenMarketInfo.Network, + ): CryptoCurrency? { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found") + val blockchain = Blockchain.fromNetworkId(network.networkId) ?: error("Unknown network [${network.networkId}]") + + return if (network.contractAddress == null) { + CryptoCurrencyFactory().createCoin( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) + } else { + val currencyNetwork = getNetwork( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) ?: return null + + CryptoCurrencyFactory().createToken( + network = currencyNetwork, + rawId = token.id, + name = token.name, + symbol = token.symbol, + decimals = network.decimalCount ?: error("Unknown decimal"), + contractAddress = network.contractAddress!!, + ) + } + } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt index 22356c0dfd..03cb704bd6 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -4,6 +4,7 @@ import com.tangem.data.markets.DefaultMarketsTokenRepository import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.DevTangemApi +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -21,12 +22,14 @@ internal object MarketsDataModule { fun provideMarketsRepository( @DevTangemApi marketsApi: TangemTechMarketsApi, @DevTangemApi tangemTechApi: TangemTechApi, + userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, ): MarketsTokenRepository { return DefaultMarketsTokenRepository( marketsApi = marketsApi, tangemTechApi = tangemTechApi, dispatcherProvider = dispatchers, + userWalletsStore = userWalletsStore, ) } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt index c4abbb8284..83ea2e4b48 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt @@ -12,6 +12,8 @@ interface DerivationsRepository { @Throws suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) + suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) + @Throws suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index c0461ee9ab..430f975fa5 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -13,12 +13,16 @@ android { dependencies { /* Domain */ api(projects.domain.appCurrency.models) + api(projects.domain.card) api(projects.domain.core) - api(projects.core.pagination) api(projects.domain.markets.models) + api(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) implementation(projects.domain.tokens) + api(projects.core.pagination) + /* Utils */ implementation(deps.kotlin.serialization) implementation(projects.core.utils) diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt new file mode 100644 index 0000000000..bc04fa27ec --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.markets + +import arrow.core.Either +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for saving tokens from Markets + * + * @property derivationsRepository derivations repository + * @property marketsTokenRepository markets token repository + * @property currenciesRepository currencies repository + * +[REDACTED_AUTHOR] + */ +class SaveMarketTokensUseCase( + private val derivationsRepository: DerivationsRepository, + private val marketsTokenRepository: MarketsTokenRepository, + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + tokenMarketParams: TokenMarketParams, + addedNetworks: Set, + removedNetworks: Set, + ): Either = Either.catch { + currenciesRepository.removeCurrencies( + userWalletId = userWalletId, + currencies = removedNetworks.mapNotNull { + marketsTokenRepository.createCryptoCurrency( + userWalletId = userWalletId, + token = tokenMarketParams, + network = it, + ) + }, + ) + + derivationsRepository.derivePublicKeysByNetworkIds( + userWalletId = userWalletId, + networkIds = addedNetworks.map { Network.ID(it.networkId) }, + ) + + currenciesRepository.addCurrencies( + userWalletId = userWalletId, + currencies = addedNetworks.mapNotNull { + marketsTokenRepository.createCryptoCurrency( + userWalletId = userWalletId, + token = tokenMarketParams, + network = it, + ) + }, + ) + + // TODO: [REDACTED_JIRA] + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index e96aa4525c..1e40c55a13 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -1,6 +1,8 @@ package com.tangem.domain.markets.repositories import com.tangem.domain.markets.* +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId interface MarketsTokenRepository { @@ -17,4 +19,10 @@ interface MarketsTokenRepository { suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes + + suspend fun createCryptoCurrency( + userWalletId: UserWalletId, + token: TokenMarketParams, + network: TokenMarketInfo.Network, + ): CryptoCurrency? } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 763a431844..9e95fe3af0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -30,7 +30,11 @@ internal class AddToPortfolioBSContentUMFactory( private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, private val onNetworkSwitchClick: (String, Boolean) -> Unit, private val onWalletSelect: (UserWalletId) -> Unit, - private val onContinueClick: () -> Unit, + private val onContinueClick: ( + selectedWalletId: UserWalletId, + addedNetworks: Set, + removedNetworks: Set, + ) -> Unit, ) { /** @@ -60,7 +64,23 @@ internal class AddToPortfolioBSContentUMFactory( ).convert(value = token), isScanCardNotificationVisible = portfolioUIData.hasMissedDerivations, continueButtonEnabled = isUserChangedNetworks, - onContinueButtonClick = onContinueClick, + onContinueButtonClick = { + val alreadyAddedNetworkIds = portfolioData.walletsWithCurrencies[selectedWallet].orEmpty() + .map { it.status.currency.network.backendId } + .toSet() + + onContinueClick( + selectedWallet.walletId, + portfolioUIData.addToPortfolioData.getAddedNetworks( + userWalletId = selectedWallet.walletId, + alreadyAddedNetworkIds = alreadyAddedNetworkIds, + ), + portfolioUIData.addToPortfolioData.getRemovedNetworks( + userWalletId = selectedWallet.walletId, + alreadyAddedNetworkIds = alreadyAddedNetworkIds, + ), + ) + }, walletSelectorConfig = crateWalletSelectorBSConfig( isShow = portfolioUIData.portfolioBSVisibilityModel.walletSelectorBSVisibility, portfolioData = portfolioData, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt new file mode 100644 index 0000000000..bf366c445c --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt @@ -0,0 +1,188 @@ +package com.tangem.features.markets.portfolio.impl.model + +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +internal typealias WalletsWithNetworks = Map> + +/** + * Manager for tracking changing networks in AddToPortfolio + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class AddToPortfolioManager @Inject constructor() { + + private val availableNetworks = MutableStateFlow?>(value = null) + private val addedNetworks = MutableStateFlow(value = emptyMap()) + private val removedNetworks = MutableStateFlow(value = emptyMap()) + + /** Get [AddToPortfolioData] as flow */ + fun getAddToPortfolioData(): Flow { + return combine( + flow = availableNetworks, + flow2 = addedNetworks, + flow3 = removedNetworks, + transform = ::AddToPortfolioData, + ) + } + + /** Set available networks [networks] */ + fun setAvailableNetworks(networks: List) { + availableNetworks.value = networks.toSet() + } + + /** Add network [networkId] to [userWalletId] */ + fun addNetwork(userWalletId: UserWalletId, networkId: String) { + addedNetworks.add(userWalletId, networkId) + + removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId) + } + + /** Remove network [networkId] from [userWalletId] */ + fun removeNetwork(userWalletId: UserWalletId, networkId: String) { + removedNetworks.add(userWalletId, networkId) + + addedNetworks.cancelPrevChangeIfExist( + userWalletId = userWalletId, + networkId = networkId, + ) + } + + /** Remove all networks by [userWalletId] */ + fun removeAllChanges(userWalletId: UserWalletId) { + addedNetworks.update { + it.toMutableMap().apply { remove(userWalletId) } + } + + removedNetworks.update { + it.toMutableMap().apply { remove(userWalletId) } + } + } + + private fun MutableStateFlow.cancelPrevChangeIfExist( + userWalletId: UserWalletId, + networkId: String, + ) { + if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId) + } + + private fun MutableStateFlow.add(userWalletId: UserWalletId, networkId: String) { + change(userWalletId = userWalletId, networkId = networkId, isAddAction = true) + } + + private fun MutableStateFlow.remove(userWalletId: UserWalletId, networkId: String) { + change(userWalletId = userWalletId, networkId = networkId, isAddAction = false) + } + + private fun MutableStateFlow.change( + userWalletId: UserWalletId, + networkId: String, + isAddAction: Boolean, + ) { + val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId } + + if (network == null) { + Timber.d( + "Network [$networkId] doesn't contain in available networks [%s]", + availableNetworks.value?.joinToString { it.networkId }, + ) + + return + } + + update { + it.toMutableMap().apply { + this[userWalletId] = if (isAddAction) { + this[userWalletId].orEmpty() + network + } else { + this[userWalletId].orEmpty() - network + } + } + } + } + + /** + * Add to portfolio data + * + * @property availableNetworks available networks that user can add to portfolio + * @property addedNetworks networks that user toggled on, but it might have already been added to the wallet + * @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet + * + * Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just + * toggle it. But when we will save user changes, we will check what tokens have already been added or + * haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks] + */ + data class AddToPortfolioData( + val availableNetworks: Set?, + val addedNetworks: WalletsWithNetworks, + private val removedNetworks: WalletsWithNetworks, + ) { + + /** + * Associate network with toggle. If user changed toggle state then use it, otherwise check state by already + * added networks. + * + * @param userWalletId user wallet id + * @param alreadyAddedNetworkIds already added network ids + */ + fun associateWithToggle( + userWalletId: UserWalletId, + alreadyAddedNetworkIds: Set, + ): Map { + // Use user choice or check already added networks + return availableNetworks?.associateWith { availableNetwork -> + val isAddedByUser = addedNetworks[userWalletId]?.contains(availableNetwork) + + if (isAddedByUser == true) return@associateWith true + + val isRemovedByUser = removedNetworks[userWalletId]?.contains(availableNetwork) + + if (isRemovedByUser == true) return@associateWith false + + val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId } + + isAddedBefore + } + .orEmpty() + } + + fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean { + return addedNetworks[userWalletId].orEmpty().isNotEmpty() || + removedNetworks[userWalletId].orEmpty().isNotEmpty() + } + + /** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */ + fun getAddedNetworks( + userWalletId: UserWalletId, + alreadyAddedNetworkIds: Set, + ): Set { + val addedNetworksByUser = addedNetworks[userWalletId].orEmpty() + + return addedNetworksByUser.map { it.networkId } + .minus(alreadyAddedNetworkIds) + .mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } } + .toSet() + } + + /** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */ + fun getRemovedNetworks( + userWalletId: UserWalletId, + alreadyAddedNetworkIds: Set, + ): Set { + val removedNetworksByUser = removedNetworks[userWalletId].orEmpty() + + return alreadyAddedNetworkIds + .minus(removedNetworksByUser.map { it.networkId }.toSet()) + .mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } } + .toSet() + } + } +} \ 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 index 891fc3b8aa..6c6f15a0dd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -5,9 +5,10 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.card.HasMissedDerivationsUseCase +import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -20,6 +21,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -28,12 +30,14 @@ import javax.inject.Inject @ComponentScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + tokenActionsIntentsFactory: TokenActionsHandler.Factory, override val dispatchers: CoroutineDispatcherProvider, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, - private val tokenActionsIntentsFactory: TokenActionsHandler.Factory, + private val saveMarketTokensUseCase: SaveMarketTokensUseCase, + private val addToPortfolioManager: AddToPortfolioManager, ) : Model() { val state: StateFlow get() = _state @@ -41,14 +45,9 @@ internal class MarketsPortfolioModel @Inject constructor( private val params = paramsContainer.require() - private val availableNetworksFlow = MutableStateFlow?>(value = null) - /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) - /** Map of [UserWalletId] and network ids that user is changed */ - private val walletsWithChangedNetworksFlow = MutableStateFlow>>(value = emptyMap()) - private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) private val currentAppCurrency = getSelectedAppCurrencyUseCase() @@ -91,11 +90,11 @@ internal class MarketsPortfolioModel @Inject constructor( } fun setTokenNetworks(networks: List) { - availableNetworksFlow.value = networks + addToPortfolioManager.setAvailableNetworks(networks) } fun setNoNetworksAvailable() { - availableNetworksFlow.value = emptyList() + addToPortfolioManager.setAvailableNetworks(emptyList()) } private fun subscribeOnSelectedMultiWalletUpdates() { @@ -114,7 +113,6 @@ internal class MarketsPortfolioModel @Inject constructor( combine( flow = portfolioDataLoader.load(params.token.id), flow2 = getPortfolioUIDataFlow(), - flow3 = availableNetworksFlow, transform = factory::create, ) .onEach { _state.value = it } @@ -125,13 +123,13 @@ internal class MarketsPortfolioModel @Inject constructor( return combine( flow = portfolioBSVisibilityModelFlow, flow2 = selectedMultiWalletIdFlow, - flow3 = walletsWithChangedNetworksFlow, - transform = { portfolioBSVisibilityModel, selectedWalletId, walletsWithChangedNetworks -> + flow3 = addToPortfolioManager.getAddToPortfolioData(), + transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData -> PortfolioUIData( portfolioBSVisibilityModel = portfolioBSVisibilityModel, selectedWalletId = selectedWalletId, - walletsWithChangedNetworks = walletsWithChangedNetworks, - hasMissedDerivations = hasMissedDerivations(selectedWalletId, walletsWithChangedNetworks), + addToPortfolioData = addToPortfolioData, + hasMissedDerivations = hasMissedDerivations(selectedWalletId, addToPortfolioData), ) }, ) @@ -139,13 +137,13 @@ internal class MarketsPortfolioModel @Inject constructor( private suspend fun hasMissedDerivations( selectedWalletId: UserWalletId?, - walletsWithChangedNetworks: Map>, + addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, ): Boolean { return if (selectedWalletId != null) { hasMissedDerivationsUseCase.invoke( userWalletId = selectedWalletId, - networksWithDerivationPath = walletsWithChangedNetworks[selectedWalletId].orEmpty() - .associate { Network.ID(it) to null }, + networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty() + .associate { Network.ID(it.networkId) to null }, ) } else { false @@ -169,41 +167,46 @@ internal class MarketsPortfolioModel @Inject constructor( } } - private fun onNetworkSwitchClick(id: String, isChecked: Boolean) { - walletsWithChangedNetworksFlow.update { - val selectedWalletId = selectedMultiWalletIdFlow.value + private fun onNetworkSwitchClick(networkId: String, isChecked: Boolean) { + val selectedWalletId = selectedMultiWalletIdFlow.value - if (selectedWalletId == null) { - Timber.e("Impossible ti switch network when selected wallet is null") - return@update it - } + if (selectedWalletId == null) { + Timber.e("Impossible to switch network when selected wallet is null") + return + } - it.toMutableMap().apply { - val networkIds = this[selectedWalletId] ?: emptyList() - - if (isChecked) { - this[selectedWalletId] = networkIds + id - } else { - this[selectedWalletId] = networkIds - id - } - } + if (isChecked) { + addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = networkId) + } else { + addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = networkId) } } private fun onWalletSelect(userWalletId: UserWalletId) { selectedMultiWalletIdFlow.update { prevUserWalletId -> - - // Clear user changes if user select another wallet - walletsWithChangedNetworksFlow.update { - it.toMutableMap().apply { remove(prevUserWalletId) } - } + prevUserWalletId?.let(addToPortfolioManager::removeAllChanges) userWalletId } } - private fun onContinueClick() { - // TODO [REDACTED_JIRA] + private fun onContinueClick( + userWalletId: UserWalletId, + addedNetworks: Set, + removedNetworks: Set, + ) { + modelScope.launch { + saveMarketTokensUseCase( + userWalletId = userWalletId, + tokenMarketParams = params.token, + addedNetworks = addedNetworks, + removedNetworks = removedNetworks, + ) + + onAddToPortfolioBSVisibilityChange(isShow = false) + + addToPortfolioManager.removeAllChanges(userWalletId) + } } private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index fb632b9acc..c7e25a4287 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -23,17 +23,15 @@ internal class MyPortfolioUMFactory( private val tokenActionsHandler: TokenActionsHandler, ) { - fun create( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - availableNetworks: List?, - ): MyPortfolioUM { - if (availableNetworks == null) return MyPortfolioUM.Loading + fun create(portfolioData: PortfolioData, portfolioUIData: PortfolioUIData): MyPortfolioUM { + val addToPortfolioData = portfolioUIData.addToPortfolioData - if (availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable + if (addToPortfolioData.availableNetworks == null) return MyPortfolioUM.Loading + + if (addToPortfolioData.availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable val walletsWithCurrencies = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(networks = availableNetworks) + .filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() if (isPortfolioEmpty) { @@ -44,7 +42,6 @@ internal class MyPortfolioUMFactory( addToPortfolioBSConfig = createAddToPortfolioBSConfig( portfolioData = portfolioData, portfolioUIData = portfolioUIData, - availableNetworks = availableNetworks, ), onAddClick = onAddClick, ) @@ -56,12 +53,10 @@ internal class MyPortfolioUMFactory( return TokensPortfolioUMConverter( appCurrency = portfolioData.appCurrency, isBalanceHidden = portfolioData.isBalanceHidden, - isAllAvailableNetworksAdded = walletsWithCurrencies.isAllAvailableNetworksAdded(availableNetworks), - bsConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - availableNetworks = availableNetworks, + isAllAvailableNetworksAdded = walletsWithCurrencies.isAllAvailableNetworksAdded( + availableNetworks = addToPortfolioData.availableNetworks, ), + bsConfig = createAddToPortfolioBSConfig(portfolioData = portfolioData, portfolioUIData = portfolioUIData), onAddClick = onAddClick, onTokenItemClick = onTokenItemClick, quickActionsIntents = tokenActionsHandler, @@ -72,7 +67,6 @@ internal class MyPortfolioUMFactory( private fun createAddToPortfolioBSConfig( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, - availableNetworks: List, ): TangemBottomSheetConfig { val walletId = portfolioUIData.selectedWalletId ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }?.walletId @@ -81,46 +75,32 @@ internal class MyPortfolioUMFactory( .firstOrNull { it.walletId == walletId } ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") - val changedNetworks = portfolioUIData.walletsWithChangedNetworks[portfolioUIData.selectedWalletId] + val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty() + val alreadyAddedNetworks = requireNotNull( - value = portfolioData.walletsWithCurrencies[selectedWallet], + value = portfolioData.walletsWithCurrencies + .filterAvailableNetworks(availableNetworks)[selectedWallet], lazyMessage = { "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" }, ) - .filterAvailableNetworks(availableNetworks) .map { it.status.currency.network.backendId } + .toSet() return addToPortfolioBSContentUMFactory.create( portfolioData = portfolioData, portfolioUIData = portfolioUIData, selectedWallet = selectedWallet, - networksWithToggle = availableNetworks.associateWithToggle( - changedNetworks = changedNetworks, - alreadyAddedNetworks = alreadyAddedNetworks, + networksWithToggle = portfolioUIData.addToPortfolioData.associateWithToggle( + userWalletId = selectedWallet.walletId, + alreadyAddedNetworkIds = alreadyAddedNetworks, ), - isUserChangedNetworks = changedNetworks != null && alreadyAddedNetworks != changedNetworks, + isUserChangedNetworks = portfolioUIData.addToPortfolioData.isUserChangedNetworks(selectedWallet.walletId), ) } - private fun List.associateWithToggle( - changedNetworks: List?, - alreadyAddedNetworks: List, - ): Map { - // Use user choice or check already added networks - return associateWith { network -> - val isSelectedByUser = changedNetworks?.contains(network.networkId) - - if (isSelectedByUser != null) return@associateWith isSelectedByUser - - val isAlreadyAdded = alreadyAddedNetworks.any { it == network.networkId } - - isAlreadyAdded - } - } - private fun Map>.isAllAvailableNetworksAdded( - availableNetworks: List, + availableNetworks: Set, ): Boolean { val networkIds = availableNetworks.map { it.networkId } @@ -134,14 +114,14 @@ internal class MyPortfolioUMFactory( /** Filter map values by available networks [networks] */ private fun Map>.filterAvailableNetworks( - networks: List, + networks: Set, ): Map> { return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } } /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ private fun List.filterAvailableNetworks( - networks: List, + networks: Set, ): List { val networkIds = networks.map(TokenMarketInfo.Network::networkId) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt index fcb0dd86c0..e9c972b068 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt @@ -7,7 +7,7 @@ import com.tangem.domain.wallets.models.UserWalletId * * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model * @property selectedWalletId selected wallet id - * @property walletsWithChangedNetworks wallets with changed networks + * @property addToPortfolioData add to portfolio data * @property hasMissedDerivations flag that indicates if user has missed derivations * [REDACTED_AUTHOR] @@ -15,6 +15,6 @@ import com.tangem.domain.wallets.models.UserWalletId internal data class PortfolioUIData( val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, val selectedWalletId: UserWalletId?, - val walletsWithChangedNetworks: Map>, + val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, val hasMissedDerivations: Boolean, ) \ No newline at end of file From 2a6fd5ee3d3fd96cce6c9a2037d4f0cb7d7d25d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 11:48:29 +0500 Subject: [PATCH 095/197] Updated on 2026-08-14 --- ...ConfirmationStateResetAssentTransformer.kt | 23 +++++++++++++++++++ .../viewmodel/StakingViewModel.kt | 7 ++++++ 2 files changed, 30 insertions(+) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt new file mode 100644 index 0000000000..ee866db72c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal object SetConfirmationStateResetAssentTransformer : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = prevState.confirmationState + return prevState.copy( + confirmationState = if (confirmationState is StakingStates.ConfirmationState.Data) { + confirmationState.copy( + isPrimaryButtonEnabled = true, + innerState = InnerConfirmationStakingState.ASSENT, + validatorState = confirmationState.validatorState.copySealed(isClickable = true), + ) + } else { + confirmationState + }, + ) + } +} \ 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 a79ffd5875..d6869e83d1 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 @@ -253,6 +253,7 @@ internal class StakingViewModel @Inject constructor( ).getOrElse { Timber.e(it.toString()) stakingEventFactory.createStakingErrorAlert(it) + stateController.update(SetConfirmationStateResetAssentTransformer) return@launch } @@ -266,6 +267,7 @@ internal class StakingViewModel @Inject constructor( ).getOrElse { Timber.e(it.toString()) stakingEventFactory.createStakingErrorAlert(it) + stateController.update(SetConfirmationStateResetAssentTransformer) return@launch } @@ -469,6 +471,7 @@ internal class StakingViewModel @Inject constructor( ), ) stakingEventFactory.createGenericErrorAlert(error.message ?: error.toString()) + stateController.update(SetConfirmationStateResetAssentTransformer) return@launch }, ifRight = { it }, @@ -489,6 +492,7 @@ internal class StakingViewModel @Inject constructor( ), ) stakingEventFactory.createSendTransactionErrorAlert(error) + stateController.update(SetConfirmationStateResetAssentTransformer) }, ifRight = { stateController.update(SetApprovalInProgressTransformer) @@ -687,6 +691,7 @@ internal class StakingViewModel @Inject constructor( ifLeft = { Timber.e(it.toString()) stakingEventFactory.createGenericErrorAlert(it.toString()) + stateController.update(SetConfirmationStateResetAssentTransformer) }, ) getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( @@ -711,6 +716,7 @@ internal class StakingViewModel @Inject constructor( ifLeft = { Timber.e(it.toString()) stakingEventFactory.createGenericErrorAlert(it.toString()) + stateController.update(SetConfirmationStateResetAssentTransformer) }, ) } @@ -759,6 +765,7 @@ internal class StakingViewModel @Inject constructor( ), ) stakingEventFactory.createSendTransactionErrorAlert(error) + stateController.update(SetConfirmationStateResetAssentTransformer) }, ifRight = { transactionHashes -> transactionsInProgress.clear() From eb9e376d40da8dea7957977286aab5eb79dc8f55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 11:51:44 +0500 Subject: [PATCH 096/197] Updated on 2026-08-14 --- .../TokenDetailsLoadedBalanceConverter.kt | 43 +++++++++++++------ .../components/staking/StakingBalanceBlock.kt | 13 +++--- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 +++ 3 files changed, 43 insertions(+), 18 deletions(-) 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 0555333c41..594a80eeb7 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 @@ -6,19 +6,18 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.* -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance @@ -26,6 +25,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.get import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R +import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero @@ -157,6 +157,7 @@ internal class TokenDetailsLoadedBalanceConverter( val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } getStakedState( + status = status, stakingCryptoAmount = stakingCryptoAmount, stakingFiatAmount = stakingFiatAmount, stakingRewardAmount = stakingRewardAmount, @@ -211,6 +212,7 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun getStakedState( + status: CryptoCurrencyStatus, stakingCryptoAmount: BigDecimal?, stakingFiatAmount: BigDecimal?, stakingRewardAmount: BigDecimal?, @@ -228,16 +230,7 @@ internal class TokenDetailsLoadedBalanceConverter( appCurrencyProvider().symbol, ), ), - rewardValue = resourceReference( - R.string.staking_details_rewards_to_claim, - wrappedList( - BigDecimalFormatter.formatFiatAmount( - stakingRewardAmount, - appCurrencyProvider().code, - appCurrencyProvider().symbol, - ), - ), - ), + rewardValue = getRewardText(status, stakingRewardAmount), onStakeClicked = clickIntents::onStakeBannerClick, ) } @@ -302,4 +295,28 @@ internal class TokenDetailsLoadedBalanceConverter( return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) } + + private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { + val isSolana = isSolana(status.currency.network.id.value) + val rewardBlockType = when { + isSolana -> RewardBlockType.RewardUnavailable + stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards + else -> RewardBlockType.Rewards + } + + return when (rewardBlockType) { + RewardBlockType.Rewards -> resourceReference( + R.string.staking_details_rewards_to_claim, + wrappedList( + BigDecimalFormatter.formatFiatAmount( + stakingRewardAmount, + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ), + ), + ) + RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) + RewardBlockType.RewardUnavailable -> TextReference.EMPTY + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index c1ce6e3abd..8d40a79e42 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -68,11 +69,13 @@ internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifie color = TangemTheme.colors.text.tertiary, ) } - Text( - text = state.rewardValue.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) + if (state.rewardValue != TextReference.EMPTY) { + Text( + text = state.rewardValue.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } } Icon( painter = painterResource(id = R.drawable.ic_chevron_right_24), diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 92aa3afdaa..e3618d6cd8 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -72,6 +72,11 @@ object BlockchainUtils { return blockchain == Blockchain.Arbitrum } + fun isSolana(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Solana + } + data class BlockchainInfo( val blockchainId: String, val name: String, From 134df908738882066c722cbf1dcec33ca1e29fa2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 17:58:22 +0500 Subject: [PATCH 097/197] Updated on 2026-08-14 --- .../state/transformers/SetInitialDataStateTransformer.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 cd6d2f6166..b164510f98 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 @@ -23,6 +23,7 @@ import com.tangem.features.staking.impl.presentation.state.converters.RewardsVal import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -60,13 +61,15 @@ internal class SetInitialDataStateTransformer( } override fun transform(prevState: StakingUiState): StakingUiState { + val cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name + return prevState.copy( title = TextReference.Res( R.string.staking_title_stake, - wrappedList(cryptoCurrencyStatusProvider().currency.name), + wrappedList(cryptoCurrencyName), ), walletName = userWalletProvider.invoke().name, - cryptoCurrencyName = cryptoCurrencyStatusProvider.invoke().currency.name, + cryptoCurrencyName = cryptoCurrencyName, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), @@ -79,7 +82,7 @@ internal class SetInitialDataStateTransformer( private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { return StakingStates.InitialInfoState.Data( - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = !cryptoCurrencyStatusProvider().value.amount.isNullOrZero(), aprRange = getAprRange(yield.validators), infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, From 0c4097a20588e0bfac381fc42ed0efd8a5080166 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 11:52:11 +0500 Subject: [PATCH 098/197] Updated on 2026-08-14 --- core/res/src/main/res/values-fr/strings.xml | 146 ++++++++++++++++-- core/res/src/main/res/values-ja/strings.xml | 5 + .../staking/model/stakekit/YieldBalance.kt | 6 + .../state/InnerYieldBalanceState.kt | 4 +- .../converters/YieldBalancesConverter.kt | 26 +++- .../previewdata/InitialStakingStatePreview.kt | 8 +- .../ui/StakingInitialInfoContent.kt | 42 ++--- 7 files changed, 189 insertions(+), 48 deletions(-) diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 188ec9511e..6b76c494d8 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -39,6 +39,7 @@ Trop de tentatives Vous avez désactivé l\'authentification biométrique sur votre téléphone et ne pourrez pas enregistrer de portefeuilles dans l\'application. Pour enregistrer des portefeuilles, veuillez activer la fonction d\'authentification biométrique dans les paramètres de votre téléphone. Démarrer le processus de sauvegarde + Avec votre carte bancaire ou votre compte bancaire %d carte %d cartes @@ -65,10 +66,11 @@ ADA insuffisant pour le transfert de jetons Vous devez conserver un peu d\'ADA car vous avez des jetons sur la blockchain Cardano ADA insuffisant - J\'accepte + Accepter Accès refusé - Tout + Tous Permettre + Analytique Appliquer Approbation Approuver @@ -81,6 +83,8 @@ Aller à %1$s Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité Annuler + Choisissez une action + Réclamez des récompenses Fermer Continuer Copier @@ -90,7 +94,7 @@ Personnalisé %d jour - %d jours + %d jours Supprimer Désactivé @@ -98,6 +102,7 @@ Activer Activé Erreur + Échangez Explorez Explorez l\'historique des transactions Explorateur @@ -108,7 +113,12 @@ Vitesse et frais Obtenir des adresses Aller au fournisseur + Aller au jeton Importez + + dans %d jour + dans %d jours + Plus tard Verrouillé Réseau principal @@ -129,7 +139,7 @@ Rejeter Recharger Renommer - Enregistrer + Enregistrez Sauvegarder les modifications Rechercher Rechercher des jetons @@ -141,13 +151,13 @@ Partager Signez Signez et envoyez - Enjeu + Stake Staking Démarrer Soumettre Avec succès Support - Échange + Échanger termes et conditions Aujourd\'hui La transaction a échoué @@ -156,6 +166,7 @@ Je comprends Il y avait une erreur. Veuillez réessayer. Inaccessible + Unstakez Oui Adresse du contrat copiée ! Réseaux disponibles @@ -163,6 +174,8 @@ Adresse du contrat L\'adresse du contrat n\'est pas valide Veuillez sélectionner le réseau + Ce jeton a déjà été ajouté à la liste + Ce jeton existe déjà Les décimales doivent être un entier valide, jusqu\'à %d Dérivation personnalisée Par exemple m/00\'/0000\'/0\'/0/0 @@ -204,7 +217,7 @@ Emetteur Signé Envoyer un commentaire - Référénces + Détails Vérifiez votre connexion Internet ou passez à un réseau différent Conditions d\'utilisation Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille @@ -222,6 +235,8 @@ Échange par %s Visitez le site Web du fournisseur pour obtenir un remboursement Opération échouée par le fournisseur + Le montant de la transaction a été remboursé en %1$s sur votre portefeuille en raison des règles OKX ou du pont. %2$s + Le montant a été remboursé en %1$s (réseau %2$s) Visitez le site Web du fournisseur pour la vérification Vérification KYC requise par le fournisseur Annulé @@ -241,12 +256,13 @@ Envoyé Données fournies par le fournisseur. Le montant estimé est sujet à modification en raison des conditions du marché. Statut de l\'échange - Verification requise + Vérification requise + En attente du hachage de la transaction Liste de tous les jetons ajoutés à votre portefeuille En cherche des meilleurs taux Taux flottant En utilisant la fonctionnalité d\'échange, vous acceptez les %s - En utilisant la fonctionnalité d\'échange, vous acceptez les conditions %1$s et %2$s du fournisseur. + En utilisant la fonctionnalité d\'échange, vous acceptez les conditions %1$s et %2$s du fournisseur D\'autres fournisseurs arriveront bientôt.\nRestez branchés ! Politique de confidentialité Fournisseur @@ -255,10 +271,12 @@ Disponible à partir de %s Indisponible pour cette paire Permission requise + Recommandé Conditions d\'utilisation Aucun jeton trouvé. Veuillez essayer une autre demande ID : %s ID de transaction copié + Avec une autre devise dans votre portefeuille Les informations suivantes sont facultatives. Vous pouvez les effacer si vous ne souhaitez pas les partager. Dites-nous quelles fonctions vous manquent, et nous essaierons de vous aider. Veuillez nous dire quelle carte vous avez @@ -273,6 +291,8 @@ Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Spécifiez la limite approuvée pour le jeton sélectionné Montant %s + La fonction Approuver est nécessaire pour accorder la permission à une autre adresse d\'utiliser une quantité spécifique de vos jetons. De par leur conception, les contrats intelligents ne peuvent pas accéder à vos jetons sans votre approbation. En « déverrouillant » vos jetons, vous autorisez le contrat intelligent StakeKit à les utiliser. Les mineurs du réseau reçoivent des frais de gaz (payés par vous) pour enregistrer cette action sur la blockchain. Vous pouvez staker votre jeton après avoir donné votre approbation. + Pour continuer, vous devez autoriser le contrat intelligent StakeKit à utiliser votre %s Pour continuer, accordez aux smart contracts de %1s l\'autorisation d\'utiliser votre %2s Donner l\'autorisation Illimité @@ -331,20 +351,68 @@ Mon portfolio Marché Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem + Les données de cette section proviennent des réseaux suivants : %s Impossible de charger les données… + Aucune donnée + Actions rapides Résultat Voir les jetons de moins de 100 000 $ de capitalisation boursière Afficher les jetons Aucun résultat Sélectionnez un réseau Sélectionnez un portefeuille + 1 mois + 1 an + 24 heures + 3 mois + 6 mois + 7 jours + Tout + Acheteurs expérimentés + Évaluation Trier par + Meilleurs Gagnants + Top Perdants + Tendances À propos de %s + + Basé sur %d évaluation + Basé sur %d évaluations + + Site web + Pression d\'achat + La différence entre le volume des acheteurs et le volume des vendeurs + Approvisionnement en circulation + Le nombre total de pièces disponibles pour le trading et en circulation sur le marché + Acheteurs expérimentés + Acheteurs nets avec l\'exigence supplémentaire d\'avoir au moins 100 transactions sortantes + Valorisation entièrement diluée + La valeur théorique totale d\'une crypto-monnaie si toutes les pièces qui pourraient exister étaient en circulation, y compris celles qui ne circulent pas actuellement + Date de la Genesis + Haut + Détenteurs + L\'évolution du nombre de détenteurs de jetons au cours d\'une période donnée Idées Liens + Liquidité + Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée + Indice de liquidité + Faible + Capitalisation boursière + La valeur marchande totale d\'une crypto-monnaie, calculée en multipliant le prix actuel de la pièce par le nombre total de pièces en circulation + Évaluation du marché + Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière + Approvisionnement maximal Métriques + Official links Performance des prix + Dépôt Score de sécurité + Social + Approvisionnement total + Le nombre maximal de pièces ou de jetons pouvant exister pour une crypto-monnaie particulière + Volume des échanges (24h) + Le montant total d\'une crypto-monnaie qui a été échangé au cours des dernières 24 heures, indiquant le niveau d\'activité et de liquidité du marché Vous devez définir un seul code d\'accès pour protéger toutes vos cartes Protéger Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard @@ -388,12 +456,12 @@ Sauvegarde en cours En savoir plus sur les seed phrases - Empty + Vide Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret. Votre seed phrase - Empty + %d mots Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous @@ -416,6 +484,7 @@ Activation de la carte Carte de sauvegarde n°%d Aucune carte de sauvegarde + Notifications Une carte de sauvegarde ajoutée Préparez votre carte Deux cartes de sauvegarde ajoutées @@ -444,6 +513,7 @@ Accès à la caméra refusé %1$s (%2$s) sur le réseau %3$s Envoyez uniquement %s à cette adresse. L\'envoi de toute autre devise entraînera sa perte irréversible. + Affichez un code QR ou partagez votre adresse Participer Échec du chargement des informations sur le programme de parrainage. Veuillez réessayer plus tard. Échec du chargement des informations sur le programme de parrainage. Code d\'erreur : %s. Veuillez réessayer plus tard. @@ -511,7 +581,7 @@ Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille Mémo invalide. Il ne sera pas ajouté à la transaction. - Memo + Mémo Vérifiez votre connexion réseau Informations sur les frais de réseau inaccessibles De @@ -522,7 +592,7 @@ Max Somme maximale Frais jusqu\'à - Memo invalide + Mémo invalide Couverture des frais de réseau Fonds insuffisants pour le transfert, car le total des frais et du montant du transfert dépasse le solde existant Le total dépasse le solde @@ -569,35 +639,71 @@ Nom Actif Afin d\'unstaker vos actifs, appuyez sur le bloc ci-dessus + Le montant à staker doit être au moins %s + Réclamation déstakée Pourcentage de rendement annuel Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. APR + Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement. Disponible Taux de récompense moyen + Qu\'est-ce que le Staking ? %s profit estimatif Cote du marché Métriques Minimum requis Aucune récompense à réclamer Réclamation de récompense - Un moyen de recevoir des récompenses de staking. Il peut être réclamé automatiquement ou manuellement. + Méthode de réception des récompenses de staking.\nElle peut être automatique, où la récompense est créditée sur votre adresse, ou manuelle, où vous devez retirer la récompense en créant une transaction pour la recevoir. Calendrier de récompenses Il s\'agit d\'un calendrier qui détermine le moment où les participants au staking reçoivent leurs récompenses. Récompenses à réclamer: %s Staking %s - Période de détachement + Période de dissociation La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles. Période d\'échauffement Le temps imparti pour activer la participation au staking. + Bloqué + Migrer Native staking + Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent tous les jours. + Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent toutes les heures. + Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent tous les mois. + Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent toutes les semaines. Gagnez des récompenses de staking Les récompenses cessent de s\'accumuler immédiatement après que vous ayez commencé le unstaking. Le processus de unstaking prend %s. + Vos fonds ont été destakés. Vous pouvez désormais les retirer avec vos récompenses gagnées. + Prêt à retirer + Réassocier + Restakez + Restakez des récompenses + Révoquer + Revoter + Auto + Manuel + Bloquer + Jour + Chaque jour + Époque + Ère + Heure + Mois + Semaine Récompenses Stake verrouillé Staker plus + Stake %s + déstaker %s + Dissociation + Débloquer Non-staké Vérifiez non-stakés pour réclamer vos actifs + Unstaking Validateur + Validateurs + Voter + Vote bloqué + Retirer Stockez vos actifs crypto en toute sécurité tout en conservant les clés privées contenues dans votre carte Portefeuille matériel révolutionnaire Jusqu\'à 3 cartes physiques pour un portefeuille @@ -609,6 +715,8 @@ Découvrez Tangem Échangez, achetez des NFT, faites des prêts et des dépôts dans plus de 100 services décentralisés différents Compatible avec Web 3.0 + Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille. + Nouveau fournisseur d\'échange disponible ! Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. Le montant comprend les frais du fournisseur de services. Frais @@ -620,7 +728,7 @@ Fonds insuffisants Donner l\'autorisation En cours - Échanger + Échangez Vous recevez Choisir le jeton non disponible @@ -673,7 +781,7 @@ Utilisez %s ou scannez une carte pour avoir accès à votre portefeuille Restez à jour avec les dernières fonctionnalités et actualités Soyez le premier informé des nouvelles promotions - Souhaitez-vous utiliser les notifications push? + Souhaitez-vous utiliser les\nnotifications push? Ajouter un nouveau portefeuille Êtes-vous sûr de vouloir supprimer ce portefeuille ? Une erreur s\'est produite, veuillez scanner votre carte pour vous connecter @@ -724,6 +832,8 @@ Paramètres du portefeuille Tangem Utilisez %s ou scannez une carte pour déverrouiller l\'accès à votre portefeuille + Le processus d\'octroi des autorisations est actuellement en cours et sera bientôt terminé + Approbation en cours Il semble que l\'activation de la carte ne se soit pas déroulée correctement. Cela peut être dû à un problème avec le module NFC de votre appareil ou à une mauvaise connexion de la carte sur votre appareil. Veuillez contacter notre équipe de support pour obtenir de l’aide. Erreur d\'activation Selon les développeurs du réseau BNB, le support de la norme BEP-2\nprendra fin en juin 2024. Pour éviter de perdre des actifs avec cette norme, veuillez les convertir à la norme BEP-20. Utilisez notre service de d\'échange pour les transférer sur le réseau BNB Smart Chain. @@ -765,6 +875,8 @@ Il ne reste que %s signatures sur cette carte. Vous devez retirer tous vos fonds. Faible nombre de signatures Les jetons sur différents réseaux peuvent avoir des adresses différentes. Vérifiez bien que votre adresse correspond au réseau lorsque vous transférez des fonds. + MATIC est en cours de migration vers POL. Cependant, aucune date limite n\'a été fixée et MATIC n\'est pas encore obsolète. Vous pouvez continuer à utiliser le jeton MATIC en toute sécurité ou utiliser des échanges pour l\'échanger contre POL. + Migration de MATIC vers POL Utilisez votre carte pour obtenir une adresse pour le réseau %d Utilisez votre carte pour obtenir des adresses pour les réseaux %d diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 025170ee41..3d498f8938 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -69,6 +69,7 @@ アクセスが拒否されました すべて 許可する + アナリティクス 適用する 承認 承認 @@ -81,6 +82,7 @@ %1$sへ移動 カメラへのアクセスを許可していません。プライバシー設定を調整してください。 キャンセル + アクションを選択 報酬を受け取る 閉じる 続ける @@ -633,6 +635,7 @@ 年率 ステーキングに参加することで得られる年間収益率。 APR + 報酬は毎日自動的にステーキング残高に蓄積されます。 利用可能 平均報酬率 ステーキングとは? @@ -660,6 +663,8 @@ ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 ステーキング解除プロセスの開始後、報酬の獲得はすぐに停止します。ステーキング解除には%sかかります。 + あなたの資金はステーキング解除されました。獲得した報酬とともに引き出すことができます。 + 準備中 引き出し準備完了 再結束 再度ステーキングする diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index c50607e576..60cfb91e45 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -103,4 +103,10 @@ enum class BalanceType { -> false } } +} + +enum class RewardBlockType { + NoRewards, + Rewards, + RewardUnavailable, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 3056c85688..41fa1c3862 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -12,8 +13,7 @@ sealed class InnerYieldBalanceState { data class Data( val rewardsCrypto: String, val rewardsFiat: String, - val isRewardsToClaim: Boolean, - val isRewardsClaimable: Boolean, + val rewardBlockType: RewardBlockType, val balance: ImmutableList, ) : InnerYieldBalanceState() 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 1524154584..856bec78cc 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 @@ -12,9 +12,9 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.lib.crypto.BlockchainUtils.isSolana 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 @@ -35,9 +35,7 @@ internal class YieldBalancesConverter( val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) val groupedBalances = getGroupedBalance(yieldBalance.balance) - val isRewardsClaimable = yieldBalance.balance.items - .filter { it.type == BalanceType.REWARDS } - .any { it.pendingActions.isNotEmpty() } + InnerYieldBalanceState.Data( rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( cryptoAmount = cryptoRewardsValue, @@ -48,8 +46,7 @@ internal class YieldBalancesConverter( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ), - isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), - isRewardsClaimable = isRewardsClaimable, + rewardBlockType = getRewardBlockType(), balance = groupedBalances, ) } else { @@ -165,6 +162,23 @@ internal class YieldBalancesConverter( } } + private fun getRewardBlockType(): RewardBlockType { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + val isRewardsClaimable = yieldBalance?.balance?.items + ?.filter { it.type == BalanceType.REWARDS } + ?.any { it.pendingActions.isNotEmpty() } + ?: false + + val isSolana = isSolana(cryptoCurrencyStatus.currency.network.id.value) + + return when { + isSolana -> RewardBlockType.RewardUnavailable + isRewardsClaimable -> RewardBlockType.Rewards + else -> RewardBlockType.NoRewards + } + } + private fun Calendar.resetHours() { this[Calendar.HOUR_OF_DAY] = 0 this[Calendar.MINUTE] = 0 diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 091afc897a..b6a1eabe64 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -4,11 +4,10 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState -import com.tangem.features.staking.impl.presentation.state.BalanceState -import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates import kotlinx.collections.immutable.persistentListOf @@ -61,8 +60,7 @@ internal object InitialStakingStatePreview { yieldBalance = InnerYieldBalanceState.Data( rewardsFiat = "100 $", rewardsCrypto = "100 SOL", - isRewardsToClaim = false, - isRewardsClaimable = false, + rewardBlockType = RewardBlockType.RewardUnavailable, balance = persistentListOf( BalanceGroupedState( title = stringReference("Staked"), 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 ec508e38a9..6020b27b49 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 @@ -39,6 +39,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.BalanceState @@ -88,8 +89,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl StakingRewardBlock( rewardCrypto = state.yieldBalance.rewardsCrypto, rewardFiat = state.yieldBalance.rewardsFiat, - isRewardsToClaim = state.yieldBalance.isRewardsToClaim, - isRewardsClaimable = state.yieldBalance.isRewardsClaimable, + rewardBlockType = state.yieldBalance.rewardBlockType, onRewardsClick = clickIntents::openRewardsValidators, ) SpacerH12() @@ -143,27 +143,33 @@ private fun BannerBlock(onClick: () -> Unit) { private fun StakingRewardBlock( rewardCrypto: String, rewardFiat: String, - isRewardsToClaim: Boolean, - isRewardsClaimable: Boolean, + rewardBlockType: RewardBlockType, onRewardsClick: () -> Unit, ) { - val (text, textColor) = if (isRewardsToClaim) { - annotatedReference { - append(PLUS) - appendSpace() - append(rewardFiat) - appendSpace() - append(DOT) - appendSpace() - append(rewardCrypto) - } to TangemTheme.colors.text.primary1 - } else { - resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary + val (text, textColor) = when (rewardBlockType) { + RewardBlockType.Rewards -> { + annotatedReference { + append(PLUS) + appendSpace() + append(rewardFiat) + appendSpace() + append(DOT) + appendSpace() + append(rewardCrypto) + } to TangemTheme.colors.text.primary1 + } + RewardBlockType.RewardUnavailable -> { + resourceReference(R.string.staking_details_auto_claiming_rewards_daily_text) to + TangemTheme.colors.text.tertiary + } + RewardBlockType.NoRewards -> { + resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary + } } InputRowDefault( title = resourceReference(R.string.staking_rewards), text = text, - iconRes = R.drawable.ic_chevron_right_24.takeIf { isRewardsToClaim && isRewardsClaimable }, + iconRes = R.drawable.ic_chevron_right_24.takeIf { rewardBlockType == RewardBlockType.Rewards }, textColor = textColor, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -171,7 +177,7 @@ private fun StakingRewardBlock( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), - enabled = isRewardsToClaim && isRewardsClaimable, + enabled = rewardBlockType == RewardBlockType.Rewards, onClick = onRewardsClick, ), ) From 48f6151f42a4e2cd6b339a539d3ae816414aa313 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 16:04:26 +0500 Subject: [PATCH 099/197] Updated on 2026-08-14 --- .../RewardsValidatorStateConverter.kt | 15 ++--- .../SetInitialDataStateTransformer.kt | 5 +- .../state/transformers/SetTitleTransformer.kt | 55 ++++++++++++------- .../viewmodel/StakingViewModel.kt | 16 ++++++ 4 files changed, 58 insertions(+), 33 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 673e81499b..987866f39b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -1,6 +1,5 @@ package com.tangem.features.staking.impl.presentation.state.converters -import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -13,7 +12,6 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -74,14 +72,11 @@ internal class RewardsValidatorStateConverter( cryptoCurrency = cryptoCurrency, ), ) - val fiatAmount = combinedReference( - stringReference(PLUS), - stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + val fiatAmount = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, ), ) 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 b164510f98..ec70635b66 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 @@ -64,10 +64,7 @@ internal class SetInitialDataStateTransformer( val cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name return prevState.copy( - title = TextReference.Res( - R.string.staking_title_stake, - wrappedList(cryptoCurrencyName), - ), + title = TextReference.EMPTY, walletName = userWalletProvider.invoke().name, cryptoCurrencyName = cryptoCurrencyName, clickIntents = clickIntents, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index 7ad24a9041..2c0dea602d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -1,11 +1,15 @@ package com.tangem.features.staking.impl.presentation.state.transformers +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.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle import com.tangem.utils.transformer.Transformer internal object SetTitleTransformer : Transformer { @@ -14,26 +18,39 @@ internal object SetTitleTransformer : Transformer { val actionType = prevState.actionType val currentStep = prevState.currentStep - val title = when { - currentStep == StakingStep.Amount -> { - resourceReference(R.string.send_amount_label) - } + val title = when (currentStep) { + StakingStep.Amount -> resourceReference(R.string.send_amount_label) + StakingStep.Validators -> resourceReference(R.string.staking_validators) + StakingStep.RewardsValidators -> resourceReference(R.string.common_claim_rewards) + StakingStep.InitialInfo -> resourceReference( + R.string.staking_title_stake, + wrappedList(prevState.cryptoCurrencyName), + ) - currentStep == StakingStep.Validators -> { - resourceReference(R.string.staking_validators) - } + StakingStep.Confirmation -> { + when (actionType) { + StakingActionCommonType.ENTER -> resourceReference( + R.string.staking_title_stake, + wrappedList(prevState.cryptoCurrencyName), + ) + StakingActionCommonType.EXIT -> resourceReference( + R.string.staking_title_unstake, + wrappedList(prevState.cryptoCurrencyName), + ) + else -> { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val title = confirmationState?.pendingAction?.type?.getPendingActionTitle() - actionType == StakingActionCommonType.EXIT && currentStep != StakingStep.InitialInfo -> { - resourceReference( - R.string.staking_title_unstake, - wrappedList(prevState.cryptoCurrencyName), - ) - } - else -> { - resourceReference( - R.string.staking_title_stake, - wrappedList(prevState.cryptoCurrencyName), - ) + if (title == null || title == TextReference.EMPTY) { + resourceReference( + R.string.staking_title_stake, + wrappedList(prevState.cryptoCurrencyName), + ) + } else { + title + } + } + } } } 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 d6869e83d1..e2e06a8b8e 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 @@ -162,6 +162,13 @@ internal class StakingViewModel @Inject constructor( override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) { if (actionTypeToOverwrite != null) { stateController.update { it.copy(actionType = actionTypeToOverwrite) } + stateController.update { + val confirmationState = it.confirmationState as? StakingStates.ConfirmationState.Data + it.copy( + confirmationState = confirmationState?.copy(pendingAction = pendingAction) + ?: StakingStates.ConfirmationState.Empty(), + ) + } } stakingStateRouter.onNextClick() when { @@ -407,6 +414,14 @@ internal class StakingViewModel @Inject constructor( ShowActionSelectorBottomSheetTransformer( pendingActions = activeStake.pendingActions, onActionSelect = { + stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) + stateController.update( + AmountChangeStateTransformer( + cryptoCurrencyStatus, + activeStake.cryptoValue, + yield, + ), + ) onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, pendingAction = it) stateController.update(DismissBottomSheetStateTransformer()) }, @@ -548,6 +563,7 @@ internal class StakingViewModel @Inject constructor( currencyCheck = currencyStatus, isSubtractAvailable = isAmountSubtractAvailable, feeError = feeError, + yield = yield, ), ) } From dd8660eaa21b9f588ab3b2151c4a4b8b3af56eab Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 16:04:53 +0500 Subject: [PATCH 100/197] Updated on 2026-08-14 --- .../common/ui/notifications/NotificationUM.kt | 16 ++++ core/res/src/main/res/values/strings.xml | 3 + .../presentation/state/StakingNotification.kt | 32 ++++++-- .../ConfirmationStatePreviewData.kt | 2 +- .../AddStakingNotificationsTransformer.kt | 74 ++++++++++++++++++- .../SetConfirmationStateLoadingTransformer.kt | 49 +----------- .../state/transformers/SetTitleTransformer.kt | 9 +-- .../ui/block/NotificationsBlock.kt | 5 +- 8 files changed, 129 insertions(+), 61 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 20dad94e6b..79c6c304fb 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -183,6 +183,22 @@ sealed class NotificationUM(val config: NotificationConfig) { ) } + open class Info( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_circle_24, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ), + ) + sealed interface Cardano { data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 566ba9fae5..66d4cf1e8c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -84,6 +84,7 @@ You have not given access to your camera, please adjust your privacy settings Cancel Choose action + Claim Claim rewards Close Continue @@ -666,11 +667,13 @@ Locked Migrate Native staking + Earned rewards are sent directly to your address. Once claimed, the funds are available for use. 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 + Reinvests your earned rewards in your staked amount, increasing potential earnings. Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. Your funds have been unstaked. You can now withdraw them along with your earned rewards. Preparing 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 682b777b25..3f8be15c42 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 @@ -29,6 +29,7 @@ internal object StakingNotification { subtitle = subtitle, ) } + sealed class Warning( title: TextReference, subtitle: TextReference, @@ -40,17 +41,35 @@ internal object StakingNotification { iconResId = R.drawable.ic_alert_circle_24, buttonsState = buttonsState, onCloseClick = onCloseClick, + ) { + data class TransactionInProgress( + val title: TextReference, + val description: TextReference, + ) : StakingNotification.Warning(title = title, subtitle = description) + } + + sealed class Info( + title: TextReference, + subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM.Info( + title = title, + subtitle = subtitle, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ) { data class EarnRewards( val subtitleText: TextReference, - ) : StakingNotification.Warning( + ) : StakingNotification.Info( title = resourceReference(R.string.staking_notification_earn_rewards_title), subtitle = subtitleText, ) data class Unstake( val cooldownPeriodDays: Int, - ) : StakingNotification.Warning( + ) : StakingNotification.Info( title = resourceReference(R.string.common_unstake), subtitle = resourceReference( R.string.staking_notification_unstake_text, @@ -64,9 +83,12 @@ internal object StakingNotification { ), ) - data class TransactionInProgress( + data class PendingAction( val title: TextReference, - val description: TextReference, - ) : StakingNotification.Warning(title = title, subtitle = description) + val text: TextReference, + ) : StakingNotification.Info( + title = title, + subtitle = text, + ) } } \ 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 25879ce343..443491c582 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 @@ -79,7 +79,7 @@ internal object ConfirmationStatePreviewData { ), footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", notifications = persistentListOf( - StakingNotification.Warning.EarnRewards( + StakingNotification.Info.EarnRewards( subtitleText = resourceReference( id = R.string.staking_notification_earn_rewards_text_period_day, formatArgs = wrappedList("Solana"), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index fd4fb47e43..f7738e94b5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -11,12 +11,17 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachable import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.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.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -40,6 +45,7 @@ internal class AddStakingNotificationsTransformer( private val feeError: GetFeeError?, private val currencyCheck: CryptoCurrencyCheck, private val isSubtractAvailable: Boolean, + private val yield: Yield, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() @@ -87,7 +93,7 @@ internal class AddStakingNotificationsTransformer( isFeeCoverage = isFeeCoverage && isEnterAction, ) - addAll(confirmationState.notifications) + addInfoNotifications(prevState) }.toImmutableList() return prevState.copy( @@ -186,4 +192,70 @@ internal class AddStakingNotificationsTransformer( onReduceClick = prevState.clickIntents::onAmountReduceToClick, ) } + + private fun MutableList.addInfoNotifications(prevState: StakingUiState) { + when (prevState.actionType) { + StakingActionCommonType.EXIT -> { + add( + StakingNotification.Info.Unstake( + cooldownPeriodDays = yield.metadata.cooldownPeriod.days, + ), + ) + } + StakingActionCommonType.ENTER -> { + add( + StakingNotification.Info.EarnRewards( + subtitleText = resourceReference( + id = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + formatArgs = wrappedList(yield.token.name), + ), + ), + ) + } + else -> { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val pendingActionType = confirmationState?.pendingAction?.type + val (titleId, textId) = when (pendingActionType) { + StakingActionType.CLAIM_REWARDS -> { + R.string.common_claim to R.string.staking_notification_claim_rewards_text + } + StakingActionType.RESTAKE_REWARDS -> { + R.string.staking_restake to R.string.staking_notification_restake_rewards_text + } + else -> null to null + } + + if (titleId != null && textId != null) { + add( + StakingNotification.Info.PendingAction( + title = resourceReference(titleId), + text = resourceReference(textId), + ), + ) + } + } + } + } + + private fun getEarnRewardsPeriod(rewardSchedule: Yield.Metadata.RewardSchedule): Int { + return when (rewardSchedule) { + 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/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 e158860e89..2e09f5d90f 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,14 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateLoadingTransformer( @@ -30,52 +24,13 @@ internal class SetConfirmationStateLoadingTransformer( chosenValidator = chosenValidator, availableValidators = yield.validators, ), - notifications = getNotifications(prevState), + notifications = persistentListOf(), footerText = "", transactionDoneState = TransactionDoneState.Empty, - pendingAction = null, + pendingAction = possibleConfirmationState?.pendingAction, isApprovalNeeded = false, reduceAmountBy = null, ), ) } - - private fun getNotifications(prevState: StakingUiState): ImmutableList { - return persistentListOf( - if (prevState.actionType == StakingActionCommonType.EXIT) { - StakingNotification.Warning.Unstake( - cooldownPeriodDays = yield.metadata.cooldownPeriod.days, - ) - } else { - StakingNotification.Warning.EarnRewards( - subtitleText = resourceReference( - id = getEarnRewardsPeriod(yield.metadata.rewardSchedule), - formatArgs = wrappedList(yield.token.name), - ), - ) - }, - ) - } - - private fun getEarnRewardsPeriod(rewardSchedule: Yield.Metadata.RewardSchedule): Int { - return when (rewardSchedule) { - 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/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index 2c0dea602d..d296d29596 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -41,14 +41,11 @@ internal object SetTitleTransformer : Transformer { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data val title = confirmationState?.pendingAction?.type?.getPendingActionTitle() - if (title == null || title == TextReference.EMPTY) { - resourceReference( + title?.takeIf { it.isNullOrEmpty() } + ?: resourceReference( R.string.staking_title_stake, wrappedList(prevState.cryptoCurrencyName), ) - } else { - title - } } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index dca4eefe62..a46918a130 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -33,8 +33,11 @@ internal fun NotificationsBlock(notifications: ImmutableList) { Notification( config = notification.config, iconTint = when (notification) { - is StakingNotification.Warning -> TangemTheme.colors.icon.accent + is StakingNotification.Info, + is NotificationUM.Info, + -> TangemTheme.colors.icon.accent + is StakingNotification.Warning, is NotificationUM.Error.ExceedsBalance, is NotificationUM.Warning, -> null From 8f48ebb4bbf327b56d9a640bede88523de7e2522 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 20:38:27 +0500 Subject: [PATCH 101/197] Updated on 2026-08-14 --- .../NavigationButtonsBlock.kt | 46 ++++++++++++++++++- core/res/src/main/res/values/strings.xml | 1 + .../impl/presentation/state/StakingUiState.kt | 2 +- .../ConfirmationStatePreviewData.kt | 3 +- ...etConfirmationStateCompletedTransformer.kt | 2 + ...tConfirmationStateInProgressTransformer.kt | 2 + .../SetConfirmationStateLoadingTransformer.kt | 43 ++++++++++++++++- .../SetInitialDataStateTransformer.kt | 2 +- .../ui/StakingConfirmationContent.kt | 17 ------- .../impl/presentation/ui/StakingScreen.kt | 2 + .../viewmodel/StakingViewModel.kt | 4 +- 11 files changed, 100 insertions(+), 24 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index 58cc324958..3f12092079 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -2,11 +2,13 @@ package com.tangem.common.ui.navigationButtons import android.content.res.Configuration import androidx.compose.animation.* +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,14 +16,18 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview +import com.tangem.core.ui.components.Keyboard 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.TangemButtonsDefaults +import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -29,12 +35,17 @@ import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList @Composable -fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifier = Modifier) { +fun NavigationButtonsBlock( + buttonState: NavigationButtonsState, + modifier: Modifier = Modifier, + footerText: TextReference? = null, +) { val state = buttonState as? NavigationButtonsState.Data Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.fillMaxWidth(), ) { + InfoText(footerText) ExtraButtons(state?.extraButtons, state?.txUrl) Row( modifier = Modifier.fillMaxWidth(), @@ -134,6 +145,39 @@ private fun ExtraButtons(extraButtons: ImmutableList?, txUrl: } } +@Composable +private fun InfoText(footerText: TextReference?, modifier: Modifier = Modifier) { + var isVisibleProxy by remember { mutableStateOf(!footerText.isNullOrEmpty()) } + val keyboard by keyboardAsState() + + // the text should appear when the keyboard is closed + LaunchedEffect(footerText, keyboard) { + if (footerText.isNullOrEmpty() && keyboard is Keyboard.Opened) { + return@LaunchedEffect + } + isVisibleProxy = !footerText.isNullOrEmpty() + } + + AnimatedVisibility( + visible = isVisibleProxy, + modifier = modifier, + enter = slideInVertically() + fadeIn(), + exit = fadeOut(tween(durationMillis = 300)), + label = "Animate footer text appearance", + ) { + val text = remember(this) { requireNotNull(footerText) } + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = TangemTheme.dimens.spacing12), + ) + } +} + @Composable private fun rememberNavigationButton(button: NavigationButton?): MutableState { return remember( diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 66d4cf1e8c..3c7f4884ca 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -696,6 +696,7 @@ Rewards Stake locked Stake more + You stake %1$s and will be receiving ~%2$s yearly Stake %s Unstake %s Unbonding 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 637d40f910..bdf349c499 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 @@ -89,7 +89,7 @@ internal sealed class StakingStates { val validatorState: ValidatorState, val pendingAction: PendingAction?, val notifications: ImmutableList, - val footerText: String, + val footerText: TextReference, val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, val reduceAmountBy: BigDecimal?, 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 443491c582..09143b30de 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 @@ -4,6 +4,7 @@ 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.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield @@ -77,7 +78,7 @@ internal object ConfirmationStatePreviewData { chosenValidator = validatorList[0], availableValidators = validatorList, ), - footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", + footerText = stringReference("You stake \$715.11 and will be receiving ~\$35 monthly"), notifications = persistentListOf( StakingNotification.Info.EarnRewards( subtitleText = resourceReference( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index d2e2435917..32936b8d7e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* @@ -37,6 +38,7 @@ internal class SetConfirmationStateCompletedTransformer( validatorState = validatorState.copySealed( isClickable = false, ), + footerText = TextReference.EMPTY, notifications = persistentListOf(), transactionDoneState = TransactionDoneState.Content( timestamp = System.currentTimeMillis(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt index e17aad84b4..24bdd43c84 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState @@ -19,6 +20,7 @@ internal class SetConfirmationStateInProgressTransformer : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -25,7 +35,7 @@ internal class SetConfirmationStateLoadingTransformer( availableValidators = yield.validators, ), notifications = persistentListOf(), - footerText = "", + footerText = getFooter(prevState), transactionDoneState = TransactionDoneState.Empty, pendingAction = possibleConfirmationState?.pendingAction, isApprovalNeeded = false, @@ -33,4 +43,35 @@ internal class SetConfirmationStateLoadingTransformer( ), ) } + + private fun getFooter(state: StakingUiState): TextReference { + val amountState = state.amountState as? AmountState.Data + val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + + val isEnterAction = state.actionType == StakingActionCommonType.ENTER + + val apr = validatorState?.chosenValidator?.apr.orZero() + val amountDecimal = amountState?.amountTextField?.fiatAmount?.value + val potentialReward = amountDecimal?.multiply(apr) + + val amountValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amountDecimal, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + val potentialRewardValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = potentialReward, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + return if (isEnterAction && amountDecimal != null && potentialReward != null) { + resourceReference( + id = R.string.staking_summary_description_text, + formatArgs = wrappedList(amountValue, potentialRewardValue), + ) + } else { + TextReference.EMPTY + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index ec70635b66..796723c946 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 @@ -211,7 +211,7 @@ internal class SetInitialDataStateTransformer( feeState = FeeState.Loading, validatorState = ValidatorState.Loading, notifications = persistentListOf(), - footerText = "", + footerText = TextReference.EMPTY, transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = isApprovalNeeded, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index d7a9431621..128ff53e50 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -5,20 +5,16 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.common.ui.amountScreen.ui.AmountBlock -import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -71,22 +67,9 @@ internal fun StakingConfirmationContent( } StakingFeeBlock(feeState = state.feeState) NotificationsBlock(notifications = state.notifications) - SpacerHMax() - FooterText(text = state.footerText) } } -@Composable -private fun FooterText(text: String) { - Text( - modifier = Modifier.fillMaxWidth(), - text = text, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - ) -} - @Preview(widthDp = 360, showBackground = true) @Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable 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 568d824f9e..54f2aedeb1 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 @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { val snackbarHostState = remember { SnackbarHostState() } + val confirmationState = uiState.confirmationState as? StakingStates.ConfirmationState.Data BackHandler(onBack = uiState.clickIntents::onPrevClick) Column( @@ -55,6 +56,7 @@ internal fun StakingScreen(uiState: StakingUiState) { ) NavigationButtonsBlock( buttonState = uiState.buttonsState, + footerText = confirmationState?.footerText.takeIf { uiState.currentStep == StakingStep.Confirmation }, modifier = Modifier.padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, 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 e2e06a8b8e..dd305ad949 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 @@ -172,7 +172,7 @@ internal class StakingViewModel @Inject constructor( } stakingStateRouter.onNextClick() when { - isInitState() -> stateController.update(SetConfirmationStateLoadingTransformer(yield)) + isInitState() -> stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) isAssentState() -> getFee(pendingAction) } } @@ -183,7 +183,7 @@ internal class StakingViewModel @Inject constructor( override fun getFee(pendingAction: PendingAction?) { viewModelScope.launch { - stateController.update(SetConfirmationStateLoadingTransformer(yield)) + stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) val cryptoCurrencyValue = cryptoCurrencyStatus.value val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") From cb74a242fc268453981208c6935f54632415e69b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 18:08:45 +0500 Subject: [PATCH 102/197] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 095b8f4ea0..57484a0139 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef +Subproject commit 57484a0139299d1229872c626b2c9cb34442038c From cc3a64bb05ceb69f3dc0950e7f91742229061a56 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 13:40:23 +0500 Subject: [PATCH 103/197] Updated on 2026-08-14 --- .../NavigationButtonsBlock.kt | 4 +- .../NavigationButtonsState.kt | 1 + .../preview/NavigationButtonsPreview.kt | 1 + core/res/src/main/res/values-de/strings.xml | 4 +- core/res/src/main/res/values-fr/strings.xml | 4 +- core/res/src/main/res/values-ja/strings.xml | 4 +- core/res/src/main/res/values-ru/strings.xml | 4 +- .../src/main/res/values-uk-rUA/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 5 +- .../state/StakingStateController.kt | 7 ++- .../SetButtonsStateTransformer.kt | 6 +- .../ui/StakingInitialInfoContent.kt | 50 ++++++++++++++- .../impl/presentation/ui/StakingScreen.kt | 5 +- .../impl/presentation/ui/StakingTosText.kt | 62 +++++++++++++++++++ .../tangem/feature/swap/ui/StateBuilder.kt | 4 +- 15 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index 3f12092079..8cb08d87c0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -52,13 +52,13 @@ fun NavigationButtonsBlock( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { PreviousButton(state?.prevButton) - PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) + NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) } } } @Composable -private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { +fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { val wrappedButton by rememberNavigationButton(primaryButton) AnimatedContent( targetState = wrappedButton, diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index e9a1fd8a83..cb7a3743ec 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -12,6 +12,7 @@ sealed class NavigationButtonsState { val prevButton: NavigationButton?, val extraButtons: ImmutableList, val txUrl: String? = null, + val onTextClick: (String) -> Unit, ) : NavigationButtonsState() } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index b98e5de371..9d2e71fe85 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -54,5 +54,6 @@ internal object NavigationButtonsPreview { prevButton = prev, extraButtons = extraButtons, txUrl = "https://tangem.com", + onTextClick = {}, ) } \ 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 b0e97efb9f..2f740dd9d7 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -132,6 +132,7 @@ Primär Karte Passphrase Einfügen + Datenschutzbestimmungen %1$s-%2$s %1$s — %2$s Weiterlesen @@ -159,6 +160,7 @@ Unterstützung Tauschen Allgemeine Geschäftsbedingungen + Nutzungsbedingungen Heute Transaktion fehlgeschlagen Transaktionen @@ -264,7 +266,6 @@ Durch die Nutzung der Swap-Funktion erklärst du dich mit den folgenden Bedingungen des Anbieters einverstanden %s Durch die Nutzung der Swap-Funktionalität erklärst du dich mit des Anbieters %1$s und %2$s einverstanden Weitere Anbieter werden bald folgen.\nBleib dabei! - Datenschutzbestimmungen Anbieter Bester Preis Verfügbar bis zu %s @@ -272,7 +273,6 @@ Für dieses Paar nicht verfügbar Erlaubnis erforderlich Empfohlen - Nutzungsbedingungen Keine Token gefunden. Bitte versuche eine andere Anfrage ID: %s Transaktions-ID kopiert diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 6b76c494d8..87595dbdf6 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -132,6 +132,7 @@ Carte principale Passphrase Coller + Politique de confidentialité %1$s-%2$s %1$s — %2$s En savoir plus @@ -159,6 +160,7 @@ Support Échanger termes et conditions + Conditions d\'utilisation Aujourd\'hui La transaction a échoué Transactions @@ -264,7 +266,6 @@ En utilisant la fonctionnalité d\'échange, vous acceptez les %s En utilisant la fonctionnalité d\'échange, vous acceptez les conditions %1$s et %2$s du fournisseur D\'autres fournisseurs arriveront bientôt.\nRestez branchés ! - Politique de confidentialité Fournisseur Meilleur taux Disponible jusqu\'à %s @@ -272,7 +273,6 @@ Indisponible pour cette paire Permission requise Recommandé - Conditions d\'utilisation Aucun jeton trouvé. Veuillez essayer une autre demande ID : %s ID de transaction copié diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 3d498f8938..33f3202506 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -129,6 +129,7 @@ プライマリーカード パスフレーズ ペースト + プライバシーポリシー %1$s-%2$s %1$s — %2$s 続きを読む @@ -156,6 +157,7 @@ サポート スワップ 利用規約 + 利用規約 今日 取引が失敗しました 取引 @@ -261,7 +263,6 @@ スワップ機能を使用すると、プロバイダーの%sに同意したことになります。 スワップ機能を使用すると、プロバイダーの%1$sおよび%2$sに同意したことになります。 さらに多くのプロバイダーを追加予定です。 \nお楽しみに。 - プライバシーポリシー プロバイダー ベストレート 最大 %s まで使用可能 @@ -269,7 +270,6 @@ このペアは利用できません 許可が必要です 推奨 - 利用規約 トークンが見つかりません。別のリクエストをお試しください。 ID: %s 取引IDをコピーしました diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2f4348ef70..4d2fc19ca5 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -136,6 +136,7 @@ Основная карта Парольная фраза Вставить + Политикой конфиденциальности %1$s-%2$s %1$s — %2$s Подробнее @@ -163,6 +164,7 @@ Поддержка Обмен условия участия + Условиями использования Сегодня Ошибка транзакции Транзакции @@ -266,7 +268,6 @@ Пользуясь сервисом, вы соглашаетесь с %s Пользуясь сервисом, вы соглашаетесь с %1$s и %2$s Больше провайдеров на подходе.\nСледите за обновлениями! - Политикой конфиденциальности Провайдер Лучший курс Доступно до %s @@ -274,7 +275,6 @@ Недоступно для этой пары Требуется разрешение Рекомендовано - Условиями использования Токены не найдены. Пожалуйста, попробуйте другой запрос ID: %s ID транзакции скопирован 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 83d2675b15..ca0bc61e34 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -129,6 +129,7 @@ Основна картка Парольна фраза Вставити + Політикою конфіденційності %1$s-%2$s %1$s — %2$s Детальніше @@ -156,6 +157,7 @@ Підтримка Обмін умови участі + Умовами використання Сьогодні Помилка транзакції Транзакції @@ -259,7 +261,6 @@ Використовуючи сервіс обміну, ви погоджуєтеся з його %s Використовуючи сервіс обміну, ви погоджуєтеся з його %1$s та %2$s Незабаром з\'являться нові провайдери.\nСлідкуйте за новинами! - Політикою конфіденційності Провайдер Найкращий курс Доступно до %s @@ -267,7 +268,6 @@ Недоступно для цієї пари Потрібен дозвіл Рекомендовано - Умовами використання Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s ID транзакції скопійовано diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 3c7f4884ca..b89491b528 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -133,6 +133,7 @@ Primary Card Passphrase Paste + Privacy Policy %1$s-%2$s %1$s — %2$s Read more @@ -160,6 +161,7 @@ Support Swap terms and conditions + Terms of Use Today Transaction failed Transactions @@ -265,7 +267,6 @@ By using swap functionality, you agree with provider’s %s By using swap functionality, you agree with provider’s %1$s and %2$s More providers are coming soon.\nStay tuned! - Privacy Policy Provider Best rate Available up to %s @@ -273,7 +274,6 @@ Unavailable for this pair Permission Required Recommended - Terms of Use No tokens found. Please try another request ID: %s Transaction ID copied @@ -664,6 +664,7 @@ The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period The allocated time for activating participation in staking. + By using staking functionality, you agree with provider’s %1$s and %2$s Locked Migrate Native staking 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 6f007f76cb..8a47bfe35d 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 @@ -2,6 +2,7 @@ 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.navigation.url.UrlOpener import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference @@ -19,7 +20,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -internal class StakingStateController @Inject constructor() { +internal class StakingStateController @Inject constructor( + private val urlOpener: UrlOpener, +) { val value: StakingUiState get() = uiState.value @@ -27,7 +30,7 @@ internal class StakingStateController @Inject constructor() { val uiState: StateFlow get() = mutableUiState.asStateFlow() - private val buttonsTransformer = SetButtonsStateTransformer() + private val buttonsTransformer = SetButtonsStateTransformer(urlOpener) private val titleTransformer = SetTitleTransformer fun update(function: (StakingUiState) -> StakingUiState) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 5ad39ad9be..d9f2906670 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButtonsState +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -12,7 +13,9 @@ import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -internal class SetButtonsStateTransformer : Transformer { +internal class SetButtonsStateTransformer( + private val urlOpener: UrlOpener, +) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data @@ -23,6 +26,7 @@ internal class SetButtonsStateTransformer : Transformer { prevButton = getPrevButton(prevState), extraButtons = getExtraButtons(prevState), txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, + onTextClick = urlOpener::openUrl, ) } else { NavigationButtonsState.Empty 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 6020b27b49..5e2f5c710b 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 @@ -28,6 +28,9 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Density +import com.tangem.common.ui.navigationButtons.NavigationButtonsState +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault @@ -56,14 +59,21 @@ import kotlinx.collections.immutable.ImmutableList private const val BANNER_BLOCK_KEY = "BannerBlock" private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock" private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock" +private const val STAKE_PRIMARY_BUTTON_KEY = "StakePrimaryButton" @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, clickIntents: StakingClickIntents) { +internal fun StakingInitialInfoContent( + state: StakingStates.InitialInfoState, + buttonState: NavigationButtonsState, + clickIntents: StakingClickIntents, +) { if (state !is StakingStates.InitialInfoState.Data) return LazyColumn( + verticalArrangement = alignLastToBottom(), modifier = Modifier + .fillMaxSize() .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { @@ -105,6 +115,10 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl } } } + + item(STAKE_PRIMARY_BUTTON_KEY) { + StakeButtonBlock(buttonState) + } } } @@ -236,6 +250,21 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli } } +@Composable +private fun StakeButtonBlock(buttonState: NavigationButtonsState) { + val state = buttonState as? NavigationButtonsState.Data + val primaryButton = state?.primaryButton + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + state?.onTextClick?.let { StakingTosText(it) } + NavigationPrimaryButton(primaryButton = primaryButton) + } +} + @Composable private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextReference { return when (balanceType) { @@ -289,6 +318,24 @@ private val textGradientColors = listOf( Color(0xff8fb4df), ) +@Composable +private fun alignLastToBottom() = remember { + object : Arrangement.Vertical { + override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) { + var currentOffset = 0 + + sizes.forEachIndexed { index, size -> + if (index == sizes.lastIndex) { + outPositions[index] = totalSize - size + } else { + outPositions[index] = currentOffset + currentOffset += size + } + } + } + } +} + // region preview @Preview(showBackground = true, widthDp = 360) @@ -300,6 +347,7 @@ private fun StakingInitialInfoContent_Preview( TangemThemePreview { StakingInitialInfoContent( state = feeState, + buttonState = NavigationButtonsState.Empty, clickIntents = StakingClickIntentsStub, ) } 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 54f2aedeb1..48b48cd7a0 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 @@ -16,6 +16,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock +import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference @@ -55,7 +56,8 @@ internal fun StakingScreen(uiState: StakingUiState) { modifier = Modifier.weight(1f), ) NavigationButtonsBlock( - buttonState = uiState.buttonsState, + buttonState = uiState.buttonsState.takeUnless { uiState.currentStep == StakingStep.InitialInfo } + ?: NavigationButtonsState.Empty, footerText = confirmationState?.footerText.takeIf { uiState.currentStep == StakingStep.Confirmation }, modifier = Modifier.padding( start = TangemTheme.dimens.spacing16, @@ -153,6 +155,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M when (state) { StakingStep.InitialInfo -> StakingInitialInfoContent( state = uiState.initialInfoState, + buttonState = uiState.buttonsState, clickIntents = uiState.clickIntents, ) StakingStep.RewardsValidators -> { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt new file mode 100644 index 0000000000..2f239888e8 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt @@ -0,0 +1,62 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.foundation.text.ClickableText +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.extensions.appendColored +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.R + +private const val TERMS_OF_USE_KEY = "termsOfUse" +private const val PRIVACY_POLICY_KEY = "privacyPolicy" + +private const val TERMS_OF_USE_URL = "https://docs.stakek.it/docs/terms-of-use" +private const val PRIVACY_POLICY_URL = "https://docs.stakek.it/docs/privacy-policy" + +@Composable +internal fun StakingTosText(onTextClick: (String) -> Unit) { + val termsOfUse = stringResource(R.string.common_terms_of_use) + val privacyPolicy = stringResource(R.string.common_privacy_policy) + val tosText = stringResource(R.string.staking_legal, termsOfUse, privacyPolicy) + + val clickableAnnotation = buildAnnotatedString { + append(tosText.substringBefore(termsOfUse)) + + pushStringAnnotation(TERMS_OF_USE_KEY, "") + appendColored(termsOfUse, TangemTheme.colors.text.accent) + pop() + + append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy)) + + pushStringAnnotation(PRIVACY_POLICY_KEY, "") + appendColored(privacyPolicy, TangemTheme.colors.text.accent) + pop() + } + + ClickableText( + text = clickableAnnotation, + style = TangemTheme.typography.caption2.copy( + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ), + onClick = { offset -> + clickableAnnotation.getStringAnnotations( + tag = TERMS_OF_USE_KEY, + start = offset, + end = offset, + ).firstOrNull()?.let { + onTextClick(TERMS_OF_USE_URL) + } + + clickableAnnotation.getStringAnnotations( + tag = PRIVACY_POLICY_KEY, + start = offset, + end = offset, + ).firstOrNull()?.let { + onTextClick(PRIVACY_POLICY_URL) + } + }, + ) +} \ 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 157e1ee276..34bf5fb2ec 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 @@ -323,14 +323,14 @@ internal class StateBuilder( return TosState( tosLink = swapProvider.termsOfUse?.let { LegalState( - title = resourceReference(R.string.express_terms_of_use), + title = resourceReference(R.string.common_terms_of_use), link = it, onClick = actions.onTosClick, ) }, policyLink = swapProvider.privacyPolicy?.let { LegalState( - title = resourceReference(R.string.express_privacy_policy), + title = resourceReference(R.string.common_privacy_policy), link = it, onClick = actions.onPolicyClick, ) From f55f6f8461e80d17b25991464721e156469ac1c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Sep 2024 18:00:06 +0300 Subject: [PATCH 104/197] Updated on 2026-08-14 --- .../wallet/redux/OnboardingWalletAction.kt | 4 ++- .../redux/OnboardingWalletMiddleware.kt | 29 +++++++++++++---- .../wallet/redux/OnboardingWalletReducer.kt | 16 ++++++++-- .../wallet/redux/OnboardingWalletState.kt | 8 ++++- .../wallet/ui/OnboardingWalletFragment.kt | 32 +++++++++---------- .../com/tangem/datasource/utils/UriExt.kt | 7 ++++ gradle/dependencies.toml | 2 +- 7 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/UriExt.kt diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index c624e36825..3538c3bb27 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -23,7 +23,9 @@ sealed class OnboardingWalletAction : Action { data object ResumeBackup : OnboardingWalletAction() data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction() - class SetArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() + class SetPrimaryCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() + class SetSecondCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() + class SetThirdCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() data object OnBackPressed : OnboardingWalletAction() } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 0e32a392c4..95b2fd2452 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.net.Uri import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.CompletionResult +import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.common.extensions.ifNotNull @@ -11,6 +12,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.extensions.withIOContext import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO @@ -106,7 +108,7 @@ private fun handleWalletAction(action: Action) { val cardPublicKey = backupService.primaryPublicKey if (primaryCardId != null && cardPublicKey != null) { // uses when no scanResponse and backup state restored - loadArtworkForUnfinishedBackup( + loadArtworkForCard( cardId = primaryCardId, cardPublicKey = cardPublicKey, defaultArtwork = action.cardArtworkUriForUnfinishedBackup, @@ -119,7 +121,7 @@ private fun handleWalletAction(action: Action) { .takeIf { it != Artwork.DEFAULT_IMG_URL } ?.let { Uri.parse(it) } } - store.dispatchOnMain(OnboardingWalletAction.SetArtworkUrl(cardArtwork)) + store.dispatchOnMain(OnboardingWalletAction.SetPrimaryCardArtworkUrl(cardArtwork)) } } is OnboardingWalletAction.CreateWallet -> { @@ -232,11 +234,7 @@ private suspend fun readCard(onSuccess: (ScanResponse) -> Unit) { ) } -private suspend fun loadArtworkForUnfinishedBackup( - cardId: String, - cardPublicKey: ByteArray, - defaultArtwork: Uri?, -): Uri { +private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri { return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) { is Result.Success -> { val artworkId = cardInfo.data.artwork?.id @@ -443,6 +441,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false)) when (result) { is CompletionResult.Success -> { + updateArtworks(backupService.addedBackupCardsCount, result.data) store.dispatchOnMain(BackupAction.AddBackupCard.Success) } is CompletionResult.Failure -> { @@ -630,6 +629,22 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } } +fun updateArtworks(addedBackupCardsCount: Int, card: Card) { + mainScope.launch { + withIOContext { + val imageUri = loadArtworkForCard(card.cardId, card.cardPublicKey, Uri.EMPTY) + when (addedBackupCardsCount) { + 1 -> { + store.dispatchOnMain(OnboardingWalletAction.SetSecondCardArtworkUrl(imageUri)) + } + 2 -> { + store.dispatchOnMain(OnboardingWalletAction.SetThirdCardArtworkUrl(imageUri)) + } + } + } + } +} + internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List { return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) .filterNotNull() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index 1a400ea778..035db68bae 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -21,8 +21,20 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet step = OnboardingWalletStep.CreateWallet, ) is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup) - is OnboardingWalletAction.SetArtworkUrl -> { - state.copy(cardArtworkUri = action.artworkUri) + is OnboardingWalletAction.SetPrimaryCardArtworkUrl -> { + state.copy( + walletImages = state.walletImages.copy(primaryCardImage = action.artworkUri), + ) + } + is OnboardingWalletAction.SetSecondCardArtworkUrl -> { + state.copy( + walletImages = state.walletImages.copy(secondCardImage = action.artworkUri), + ) + } + is OnboardingWalletAction.SetThirdCardArtworkUrl -> { + state.copy( + walletImages = state.walletImages.copy(thirdCardImage = action.artworkUri), + ) } is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done) else -> state diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 6663311df8..853678f202 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -12,7 +12,7 @@ data class OnboardingWalletState( val step: OnboardingWalletStep = OnboardingWalletStep.None, val wallet2State: OnboardingWallet2State? = null, val backupState: BackupState = BackupState(), - val cardArtworkUri: Uri? = null, + val walletImages: WalletImages = WalletImages(), val showConfetti: Boolean = false, val isRingOnboarding: Boolean = false, ) : StateType { @@ -49,6 +49,12 @@ data class OnboardingWalletState( private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0 } +data class WalletImages( + val primaryCardImage: Uri? = null, + val secondCardImage: Uri? = null, + val thirdCardImage: Uri? = null, +) + data class OnboardingWallet2State( val maxProgress: Int, ) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index e0577994f9..fb9acfefd5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -26,6 +26,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.ui.extensions.setStatusBarColor +import com.tangem.datasource.utils.isNullOrEmpty import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource @@ -186,30 +187,27 @@ class OnboardingWalletFragment : when { state.wallet2State != null -> { seedPhraseStateHandler.newState(this, state, seedPhraseViewModel) - state.cardArtworkUri?.let { - seedPhraseViewModel.setCardArtworkUri(it.toString()) - if (state.isRingOnboarding) { - binding.imvFrontCard.load(R.drawable.img_ring_placeholder) - } else { - loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard) - } - loadImageIntoImageView(it, binding.imvFirstBackupCard) - loadImageIntoImageView(it, binding.imvSecondBackupCard) - } + updateWalletImagesState(state.walletImages) } else -> { - if (state.isRingOnboarding) { - binding.imvFrontCard.load(R.drawable.img_ring_placeholder) - } else { - loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard) - } - loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard) - loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard) + updateWalletImagesState(state.walletImages) handleOnboardingStep(state) } } } + private fun updateWalletImagesState(walletImages: WalletImages) { + if (!walletImages.primaryCardImage.isNullOrEmpty()) { + loadImageIntoImageView(walletImages.primaryCardImage, binding.imvFrontCard) + } + if (!walletImages.secondCardImage.isNullOrEmpty()) { + loadImageIntoImageView(walletImages.secondCardImage, binding.imvFirstBackupCard) + } + if (!walletImages.thirdCardImage.isNullOrEmpty()) { + loadImageIntoImageView(walletImages.thirdCardImage, binding.imvSecondBackupCard) + } + } + private fun loadImageIntoImageView(uri: Uri?, view: ImageView) { view.load(uri) { placeholder(R.drawable.card_placeholder_black) diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/UriExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/UriExt.kt new file mode 100644 index 0000000000..dc9fda4985 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/UriExt.kt @@ -0,0 +1,7 @@ +package com.tangem.datasource.utils + +import android.net.Uri + +fun Uri?.isNullOrEmpty(): Boolean { + return this == null || this == Uri.EMPTY +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 511d96e96e..4b1fb256b2 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -90,7 +90,7 @@ markdown = "0.7.2" # region Tangem tangemBlockchainSdk = "develop-759" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-378" +tangemCardSdk = "develop-380" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem16" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 0fdcdda8d52027cc51359d693881209cc0fc138f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 14:24:34 +0500 Subject: [PATCH 105/197] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 19 +- .../tap/di/domain/TokensDomainModule.kt | 6 - .../DefaultCustomTokensRepository.kt | 15 ++ .../DefaultManageTokensRepository.kt | 203 +++--------------- .../managetokens/di/ManageTokensDataModule.kt | 8 - .../repository/DefaultCurrenciesRepository.kt | 20 -- domain/manage-tokens/build.gradle.kts | 1 + .../CheckHasLinkedTokensUseCase.kt | 28 +++ .../managetokens/SaveManagedTokensUseCase.kt | 116 +++++++++- .../repository/CustomTokensRepository.kt | 6 + .../repository/ManageTokensRepository.kt | 9 +- .../tokens/CheckHasLinkedTokensUseCase.kt | 17 -- .../tokens/repository/CurrenciesRepository.kt | 2 - .../repository/MockCurrenciesRepository.kt | 4 - .../utils/list/ManageTokensListManager.kt | 9 +- 15 files changed, 213 insertions(+), 250 deletions(-) create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckHasLinkedTokensUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 02bb0f9562..611bce95ac 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -4,6 +4,9 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -57,11 +60,17 @@ internal object ManageTokensDomainModule { @Provides @Singleton fun provideSaveManagedTokensUseCase( - manageTokensRepository: ManageTokensRepository, + customTokensRepository: CustomTokensRepository, + walletManagersFacade: WalletManagersFacade, + currenciesRepository: CurrenciesRepository, + networksRepository: NetworksRepository, derivationsRepository: DerivationsRepository, ): SaveManagedTokensUseCase { return SaveManagedTokensUseCase( - manageTokensRepository = manageTokensRepository, + customTokensRepository = customTokensRepository, + walletManagersFacade = walletManagersFacade, + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, derivationsRepository = derivationsRepository, ) } @@ -81,4 +90,10 @@ internal object ManageTokensDomainModule { ): ValidateDerivationPathUseCase { return ValidateDerivationPathUseCase(customTokensRepository) } + + @Provides + @Singleton + fun provideCheckHasLinkedTokensUseCase(repository: ManageTokensRepository): CheckHasLinkedTokensUseCase { + return CheckHasLinkedTokensUseCase(repository) + } } \ No newline at end of file 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 5169a4b83a..8ec7199796 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 @@ -431,12 +431,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase { - return CheckHasLinkedTokensUseCase(currenciesRepository) - } - @Provides @Singleton fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 752cef4833..3f637d2d33 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -130,6 +130,21 @@ internal class DefaultCustomTokensRepository( return cryptoCurrencyFactory.createCoin(network) } + override fun createToken( + managedCryptoCurrency: ManagedCryptoCurrency.Token, + sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default, + rawId: String?, + ): CryptoCurrency.Token { + return cryptoCurrencyFactory.createToken( + network = sourceNetwork.network, + rawId = rawId, + name = managedCryptoCurrency.name, + symbol = managedCryptoCurrency.symbol, + decimals = sourceNetwork.decimals, + contractAddress = sourceNetwork.contractAddress, + ) + } + override suspend fun createCustomToken( networkId: Network.ID, derivationPath: Network.DerivationPath, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index caea014d2c..90409a1f9d 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -3,27 +3,16 @@ package com.tangem.data.managetokens import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.data.common.api.safeApiCall -import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.getBlockchain -import com.tangem.data.common.currency.hasCoinForToken import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory -import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.express.TangemExpressApi -import com.tangem.datasource.api.express.models.TangemExpressValues -import com.tangem.datasource.api.express.models.request.AssetsRequestBody -import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver @@ -35,8 +24,6 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.tokens.model.Network -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.pagination.BatchFetchResult @@ -44,8 +31,6 @@ import com.tangem.pagination.BatchListSource import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext -import timber.log.Timber @Suppress("LongParameterList") internal class DefaultManageTokensRepository( @@ -54,15 +39,9 @@ internal class DefaultManageTokensRepository( private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, - private val walletManagerFacade: WalletManagersFacade, - private val tangemExpressApi: TangemExpressApi, - private val expressAssetsStore: ExpressAssetsStore, ) : ManageTokensRepository { private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory() - private val cryptoCurrencyFactory = CryptoCurrencyFactory() - private val userTokensResponseFactory = UserTokensResponseFactory() - private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() // region getTokenListBatchFlow override fun getTokenListBatchFlow( @@ -108,7 +87,7 @@ internal class DefaultManageTokensRepository( retryOnError(call = call) } - val tokensResponse = getStoredUserTokens(request.params.userWalletId) + val tokensResponse = request.params.userWalletId?.let { getSavedUserTokensResponseSync(it) } val items = if (isFirstBatchFetching && tokensResponse != null && userWallet != null && @@ -135,16 +114,6 @@ internal class DefaultManageTokensRepository( }, ) - private suspend fun getStoredUserTokens(userWalletId: UserWalletId?): UserTokensResponse? { - return if (userWalletId != null) { - appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), - ) - } else { - null - } - } - private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find a user wallet with provided ID: $userWalletId" @@ -160,158 +129,44 @@ internal class DefaultManageTokensRepository( } // endregion - // region addManagedCurrencies - override suspend fun saveManagedCurrencies( + override suspend fun hasLinkedTokens( userWalletId: UserWalletId, - currenciesToAdd: Map>, - currenciesToRemove: Map>, - ) { - return withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, - ) + network: Network, + tempAddedTokens: Map>, + tempRemovedTokens: Map>, + ): Boolean { + val addedTokens = tempAddedTokens.mapToResponseTokens() + val removedTokens = tempRemovedTokens.mapToResponseTokens() - val filteredTokens = currenciesToAdd.mapToTokenDataList() - .filterNot { tokenData -> - val blockchain = getBlockchain(networkId = tokenData.sourceNetwork.network.id) - val networkId = blockchain.toNetworkId() - val contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress - savedCurrencies.tokens.firstOrNull { token -> - token.contractAddress == contractAddress && - token.networkId == networkId && - token.derivationPath == tokenData.sourceNetwork.network.derivationPath.value - } != null - } - val newCoins = createCoinsForNewTokens( - userWalletId = userWalletId, - newTokensNetworks = filteredTokens.mapNotNull { tokenData -> - (tokenData.sourceNetwork as? SourceNetwork.Default)?.network - }, - savedCurrencies = savedCurrencies.tokens, - ) - val tokensToSave = filteredTokens.mapToUserResponseToken() - - val tokensToRemove = currenciesToRemove.mapToTokenDataList() - removeCurrenciesFromWalletManager(userWalletId = userWalletId, tokensDataList = tokensToRemove) - - val newCurrencies = (newCoins + tokensToSave).distinct() - val updatedResponse = savedCurrencies.copy( - tokens = savedCurrencies.tokens + newCurrencies - tokensToRemove.mapToUserResponseToken().toSet(), - ) - storeAndPushTokens(userWalletId = userWalletId, response = updatedResponse) - fetchExchangeableUserMarketCoinsByIds(userWalletId = userWalletId, userTokens = updatedResponse) - } - } - - private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { - val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) - appPreferencesStore.storeObject( - key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), - value = compatibleUserTokensResponse, + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(userWalletId), + lazyMessage = { "Unable to find tokens response for user wallet with provided ID: $userWalletId" }, ) + val newTokensList = storedTokens.tokens + addedTokens - removedTokens.toSet() - pushTokens(userWalletId, response) - } - - private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { - safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) { - Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}") + return newTokensList.any { + it.contractAddress != null && + it.networkId == network.backendId && + it.derivationPath == network.derivationPath.value } } - private suspend fun fetchExchangeableUserMarketCoinsByIds( - userWalletId: UserWalletId, - userTokens: UserTokensResponse, - ) { - try { - val tokensList = userTokens.tokens - .map { - LeastTokenInfo( - contractAddress = it.contractAddress ?: TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE, - network = it.networkId, - ) - } - - if (tokensList.isNotEmpty()) { - val response = tangemExpressApi.getAssets( - AssetsRequestBody( - tokensList = tokensList, - ), - ) - - expressAssetsStore.store(userWalletId, response.getOrThrow()) - } - } catch (e: Throwable) { - Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") - } - } - - private suspend fun removeCurrenciesFromWalletManager( - userWalletId: UserWalletId, - tokensDataList: List, - ) { - walletManagerFacade.removeTokensByTokenInfo( - userWalletId = userWalletId, - tokenInfos = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData -> - val contractAddress = - (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress ?: return@mapNotNullTo null - TokenInfo( - network = tokenData.sourceNetwork.network, - name = tokenData.token.name, - symbol = tokenData.token.symbol, - contractAddress = contractAddress, - decimals = tokenData.sourceNetwork.decimals, - id = tokenData.token.id.value, - ) - }, - ) - walletManagerFacade.remove( - userWalletId = userWalletId, - networks = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData -> - (tokenData.sourceNetwork as? SourceNetwork.Main)?.network - }, - ) - } - - private suspend fun createCoinsForNewTokens( - userWalletId: UserWalletId, - newTokensNetworks: List, - savedCurrencies: List, - ): List { - return newTokensNetworks - .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins - .mapNotNull { - cryptoCurrencyFactory.createCoin( - blockchain = getBlockchain(networkId = it.id), - extraDerivationPath = it.derivationPath.value, - derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, - ) - } - .distinct() - .map(userTokensResponseFactory::createResponseToken) - } - - private fun List.mapToUserResponseToken(): List { - return map { tokenData -> - val blockchain = getBlockchain(tokenData.sourceNetwork.network.id) - UserTokensResponse.Token( - id = tokenData.token.id.value, - networkId = blockchain.toNetworkId(), - derivationPath = tokenData.sourceNetwork.network.derivationPath.value, - name = tokenData.token.name, - symbol = tokenData.token.symbol, - decimals = tokenData.sourceNetwork.decimals, - contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress, - ) - } - } - - private fun Map>.mapToTokenDataList(): List { + private fun Map>.mapToResponseTokens(): List { return flatMap { (token, networks) -> token.availableNetworks .filter { sourceNetwork -> networks.contains(sourceNetwork.network) } - .map { sourceNetwork -> TokenWithSourceNetwork(token = token, sourceNetwork = sourceNetwork) } + .map { sourceNetwork -> + val blockchain = getBlockchain(sourceNetwork.network.id) + UserTokensResponse.Token( + id = token.id.value, + networkId = blockchain.toNetworkId(), + derivationPath = sourceNetwork.network.derivationPath.value, + name = token.name, + symbol = token.symbol, + decimals = sourceNetwork.decimals, + contractAddress = (sourceNetwork as? SourceNetwork.Default)?.contractAddress, + ) + } } } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 64317a96e8..d5d9009ec3 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -3,10 +3,8 @@ package com.tangem.data.managetokens.di import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher -import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -30,9 +28,6 @@ internal object ManageTokensDataModule { manageTokensUpdateFetcher: ManageTokensUpdateFetcher, appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, - walletManagersFacade: WalletManagersFacade, - tangemExpressApi: TangemExpressApi, - expressAssetsStore: ExpressAssetsStore, ): ManageTokensRepository { return DefaultManageTokensRepository( tangemTechApi, @@ -40,9 +35,6 @@ internal object ManageTokensDataModule { manageTokensUpdateFetcher, appPreferencesStore, dispatchers, - walletManagersFacade, - tangemExpressApi, - expressAssetsStore, ) } 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 8fda73fff1..d66f067313 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 @@ -478,26 +478,6 @@ internal class DefaultCurrenciesRepository( ) ?: error("Unable to create token") } - override suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean { - val userWallet = getUserWallet(userWalletId) - fetchTokensIfCacheExpired(userWallet, refresh = false) - - 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 && - it.networkId == network.backendId && - it.derivationPath == network.derivationPath.value - } - } - @OptIn(ExperimentalCoroutinesApi::class) override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { return userWalletsStore.userWallets.flatMapLatest { userWallets -> diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index 51a0e5f6ab..5ed6875afa 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { api(projects.domain.core) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) implementation(projects.domain.card) implementation(projects.domain.legacy) diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckHasLinkedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckHasLinkedTokensUseCase.kt new file mode 100644 index 0000000000..c0acb6bffd --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckHasLinkedTokensUseCase.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class CheckHasLinkedTokensUseCase( + private val repository: ManageTokensRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + tempAddedTokens: Map>, + tempRemovedTokens: Map>, + ): Either { + return Either.catch { + repository.hasLinkedTokens( + userWalletId = userWalletId, + network = network, + tempAddedTokens = tempAddedTokens, + tempRemovedTokens = tempRemovedTokens, + ) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 5a6ad0a7ce..2db0a45d91 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -4,12 +4,19 @@ import arrow.core.Either import arrow.core.flatten import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId class SaveManagedTokensUseCase( - private val manageTokensRepository: ManageTokensRepository, + private val customTokensRepository: CustomTokensRepository, + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, private val derivationsRepository: DerivationsRepository, ) { @@ -19,15 +26,102 @@ class SaveManagedTokensUseCase( currenciesToRemove: Map>, ): Either { return Either.catch { - derivationsRepository.derivePublicKeysByNetworks( - userWalletId = userWalletId, - networks = currenciesToAdd.values.flatten(), - ) - manageTokensRepository.saveManagedCurrencies( - userWalletId = userWalletId, - currenciesToAdd = currenciesToAdd, - currenciesToRemove = currenciesToRemove, - ) + // TODO: Currently order is matter. [REDACTED_JIRA] + removeCurrencies(userWalletId, currenciesToRemove) + addCurrencies(userWalletId, currenciesToAdd) + } + } + + private suspend fun removeCurrencies( + userWalletId: UserWalletId, + currenciesToRemove: Map>, + ) { + if (currenciesToRemove.isEmpty()) return + + val currencies = currenciesToRemove.mapToCryptoCurrencies() + currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies) + + walletManagersFacade.remove( + userWalletId = userWalletId, + networks = currencies + .filterIsInstance() + .mapTo(hashSetOf(), CryptoCurrency::network), + ) + + walletManagersFacade.removeTokens( + userWalletId = userWalletId, + tokens = currencies.filterIsInstance().toSet(), + ) + } + + private suspend fun addCurrencies( + userWalletId: UserWalletId, + currenciesToAdd: Map>, + ) { + if (currenciesToAdd.isEmpty()) return + + val existingCurrencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + val currencies = currenciesToAdd.mapToCryptoCurrencies() + derivationsRepository.derivePublicKeysByNetworks( + userWalletId = userWalletId, + networks = currenciesToAdd.values.flatten(), + ) + currenciesRepository.addCurrencies(userWalletId, currencies) + refreshUpdatedNetworks( + userWalletId = userWalletId, + existingCurrencies = existingCurrencies, + currenciesToAdd = currencies, + ) + } + + private suspend fun refreshUpdatedNetworks( + userWalletId: UserWalletId, + currenciesToAdd: List, + existingCurrencies: List, + ) { + val networksToUpdate = currenciesToAdd + .asSequence() + .filterIsInstance() + .map(CryptoCurrency.Token::network) + .filterTo(hashSetOf()) { hasCoinForNetwork(existingCurrencies, it) } + + val networkToUpdate = currenciesToAdd.map { it.network } + .subtract(existingCurrencies.map { it.network }.toSet()) + + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = networksToUpdate + networkToUpdate, + refresh = true, + ) + } + + /** + * Determines if the [existingCurrencies] list contains a coin that corresponds + * to the given [network]. + */ + private fun hasCoinForNetwork(existingCurrencies: List, network: Network): Boolean { + return existingCurrencies.any { currency -> + currency is CryptoCurrency.Coin && currency.network == network + } + } + + private fun Map>.mapToCryptoCurrencies(): List { + return flatMap { (token, networks) -> + token.availableNetworks + .filter { sourceNetwork -> networks.contains(sourceNetwork.network) } + .map { sourceNetwork -> + when (sourceNetwork) { + is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken( + managedCryptoCurrency = token, + sourceNetwork = sourceNetwork, + rawId = token.id.value, + ) + is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin( + networkId = sourceNetwork.id, + derivationPath = sourceNetwork.network.derivationPath, + ) + } + } } } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index c0f898207e..eb45ba52ad 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -26,6 +26,12 @@ interface CustomTokensRepository { fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin + fun createToken( + managedCryptoCurrency: ManagedCryptoCurrency.Token, + sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default, + rawId: String?, + ): CryptoCurrency.Token + suspend fun createCustomToken( networkId: Network.ID, derivationPath: Network.DerivationPath, diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt index fab91ff3bb..c977402d75 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt @@ -10,9 +10,10 @@ interface ManageTokensRepository { fun getTokenListBatchFlow(context: ManageTokensListBatchingContext, batchSize: Int): ManageTokensListBatchFlow - suspend fun saveManagedCurrencies( + suspend fun hasLinkedTokens( userWalletId: UserWalletId, - currenciesToAdd: Map>, - currenciesToRemove: Map>, - ) + network: Network, + tempAddedTokens: Map>, + tempRemovedTokens: Map>, + ): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.kt deleted file mode 100644 index 503731cabc..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.models.UserWalletId - -class CheckHasLinkedTokensUseCase( - private val currenciesRepository: CurrenciesRepository, -) { - - suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { - return Either.catch { - currenciesRepository.hasTokens(userWalletId, network) - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index dcf638ef97..f2578bd4c4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -234,8 +234,6 @@ interface CurrenciesRepository { networkId: String, ): CryptoCurrency.Token - suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean - /** Get crypto currencies by [currencyRawId] from all user wallets */ fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 317a1a3dbe..68d9c77d7f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -150,10 +150,6 @@ internal class MockCurrenciesRepository( error("not implemented") } - override suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean { - return false - } - override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { return emptyFlow() } 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 565bb8de58..3282da26fe 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 @@ -12,7 +12,7 @@ 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.managetokens.CheckHasLinkedTokensUseCase import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -198,7 +198,12 @@ internal class ManageTokensListManager @Inject constructor( } override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean { - return checkHasLinkedTokensUseCase(userWalletId, network).getOrElse { + return checkHasLinkedTokensUseCase( + userWalletId = userWalletId, + network = network, + tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value, + tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value, + ).getOrElse { Timber.e( it, """ From f58543481d6916fbd0f5a9474dade1fdfe9a1c15 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 14:16:10 +0300 Subject: [PATCH 106/197] Updated on 2026-08-14 --- data/staking/build.gradle.kts | 4 ++- .../data/staking/DefaultStakingRepository.kt | 35 +++++++++++++++---- .../data/staking/di/StakingDataModule.kt | 3 ++ .../staking/GetStakingAvailabilityUseCase.kt | 12 +++++-- .../staking/repositories/StakingRepository.kt | 5 +-- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 11 +++--- .../repository/MockStakingRepository.kt | 6 ++-- .../viewmodels/TokenDetailsViewModel.kt | 4 +-- 8 files changed, 55 insertions(+), 25 deletions(-) diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 017cc43973..6bcc1ccd0b 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -21,8 +21,10 @@ dependencies { /** Domain modules */ implementation(projects.domain.tokens.models) implementation(projects.domain.staking) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.legacy) + implementation(projects.domain.models) /** Feature Api modules */ implementation(projects.features.staking.api) @@ -39,8 +41,8 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(projects.libs.blockchainSdk) + implementation(projects.libs.crypto) implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) { 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 562d6d1bd7..c17d722c51 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 @@ -1,6 +1,7 @@ package com.tangem.data.staking import android.util.Base64 +import arrow.core.getOrElse import arrow.core.raise.catch import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain @@ -51,9 +52,11 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero +import com.tangem.lib.crypto.BlockchainUtils.isSolana import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -69,6 +72,7 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggle: StakingFeatureToggles, private val walletManagersFacade: WalletManagersFacade, + private val getUserWalletUseCase: GetUserWalletUseCase, moshi: Moshi, ) : StakingRepository { @@ -159,17 +163,19 @@ internal class DefaultStakingRepository( } } - override suspend fun getStakingAvailabilityForActions( - cryptoCurrencyId: CryptoCurrency.ID, - symbol: String, + override suspend fun getStakingAvailability( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, ): StakingAvailability { - val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable + if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) return StakingAvailability.Unavailable + + val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: return StakingAvailability.Unavailable return withContext(dispatchers.io) { val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable - val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) - val isSupported = isStakingSupported(getIntegrationKey(cryptoCurrencyId)) + val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, cryptoCurrency.symbol) + val isSupported = isStakingSupported(getIntegrationKey(cryptoCurrency.id)) when { prefetchedYield != null && isSupported -> { @@ -183,6 +189,21 @@ internal class DefaultStakingRepository( } } + private fun checkForInvalidCardBatch(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + val userWallet = getUserWalletUseCase(userWalletId).getOrElse { + error("Failed to get user wallet") + } + + return when { + isSolana(cryptoCurrency.network.id.value) -> { + INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId) + } + else -> { + false + } + } + } + override suspend fun createAction( userWalletId: UserWalletId, network: Network, @@ -625,6 +646,8 @@ internal class DefaultStakingRepository( const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" + val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79") + // uncomment items as implementation is ready val integrationIdMap = mapOf( Blockchain.Solana.run { id + toCoinId() } to SOLANA_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 32a9a1cdae..fc2baff6dd 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 @@ -14,6 +14,7 @@ import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -37,6 +38,7 @@ internal object StakingDataModule { stakingFeatureToggle: StakingFeatureToggles, cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, + getUserWalletUseCase: GetUserWalletUseCase, @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( @@ -48,6 +50,7 @@ internal object StakingDataModule { cacheRegistry = cacheRegistry, stakingFeatureToggle = stakingFeatureToggle, walletManagersFacade = walletManagersFacade, + getUserWalletUseCase = getUserWalletUseCase, moshi = moshi, ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 46a2f010be..7133219a03 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -6,6 +6,7 @@ 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.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId /** * Use case for getting info about staking capability in tangem app. @@ -16,11 +17,16 @@ class GetStakingAvailabilityUseCase( ) { suspend operator fun invoke( - cryptoCurrencyId: CryptoCurrency.ID, - symbol: String, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, ): Either { return Either - .catch { stakingRepository.getStakingAvailabilityForActions(cryptoCurrencyId, symbol) } + .catch { + stakingRepository.getStakingAvailability( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + } .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file 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 69016e1209..f95af53048 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 @@ -32,10 +32,7 @@ interface StakingRepository { suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield - suspend fun getStakingAvailabilityForActions( - cryptoCurrencyId: CryptoCurrency.ID, - symbol: String, - ): StakingAvailability + suspend fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): StakingAvailability suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 99ec404379..a1e112bb15 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -129,14 +129,13 @@ class GetCryptoCurrencyActionsUseCase( // staking if (stakingFeatureToggles.isStakingEnabled) { - if (isStakingAvailable(cryptoCurrency)) { + if (isStakingAvailable(userWallet, cryptoCurrency)) { val yield = kotlin.runCatching { stakingRepository.getYield( cryptoCurrencyId = cryptoCurrency.id, symbol = cryptoCurrency.symbol, ) }.getOrNull() - activeList.add( TokenActionsState.ActionState.Stake( unavailabilityReason = ScenarioUnavailabilityReason.None, @@ -301,10 +300,10 @@ class GetCryptoCurrencyActionsUseCase( return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } - private suspend fun isStakingAvailable(cryptoCurrency: CryptoCurrency): Boolean { - return stakingRepository.getStakingAvailabilityForActions( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, + private suspend fun isStakingAvailable(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Boolean { + return stakingRepository.getStakingAvailability( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, ) is StakingAvailability.Available } } \ 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 9bc782020a..3000900a10 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 @@ -113,9 +113,9 @@ class MockStakingRepository : StakingRepository { isAvailable = false, ) - override suspend fun getStakingAvailabilityForActions( - cryptoCurrencyId: CryptoCurrency.ID, - symbol: String, + override suspend fun getStakingAvailability( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, ): StakingAvailability = StakingAvailability.Unavailable override suspend fun fetchSingleYieldBalance( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f959f9632a..949d9deaa0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -393,8 +393,8 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateStakingInfo() { viewModelScope.launch { val stakingAvailability = getStakingAvailabilityUseCase( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, ).getOrElse { StakingAvailability.Unavailable } internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) From ab482d1966ad602b1659dbb8470ac188f0b529d0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 00:29:59 +0500 Subject: [PATCH 107/197] Updated on 2026-08-14 --- .../main/res/values-es/strings-blockchain.xml | 25 + core/res/src/main/res/values-es/strings.xml | 916 ++++++++++++++++++ 2 files changed, 941 insertions(+) create mode 100644 core/res/src/main/res/values-es/strings-blockchain.xml create mode 100644 core/res/src/main/res/values-es/strings.xml diff --git a/core/res/src/main/res/values-es/strings-blockchain.xml b/core/res/src/main/res/values-es/strings-blockchain.xml new file mode 100644 index 0000000000..709f3ab9dd --- /dev/null +++ b/core/res/src/main/res/values-es/strings-blockchain.xml @@ -0,0 +1,25 @@ + + + Por defecto + Legado + No se pudo obtener la tarifa + Debido a limitaciónes sobre %1$s, solos %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. + No hay fondos suficientes para la transacción. Por favor, recargue su cuenta. + Ocurrió un error. Código: %s + Para utilizar la red %1$s, debe pagar la reserva de cuenta (%2$s%3$s), que bloquea y oculta ese monto indefinidamente + La cuenta de destino no está activa. Envíe %s o más para activar la cuenta. + Para crear una cuenta, envíe fondos a esta dirección + El monto mínimo es %s + La cantidad es demasiado baja + Tarifa no válida + El saldo mínimo es %s + No se ha creado la cuenta de destino. El monto a enviar debe ser %s + comisiones o más + Error desconocido + El monto excede el saldo + Cantidad no válida + La tarifa excede el saldo + El monto total excede el saldo + No, enviar todo + Réduire de %s XTZ + Para evitar pagar una comisión mayor la próxima vez que recargue su billetera, reduzca el monto en %s + diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..d69ca4e032 --- /dev/null +++ b/core/res/src/main/res/values-es/strings.xml @@ -0,0 +1,916 @@ + + + Elige red + Agregue un token personalizado + Gestionar tokens + Envíe solo %1$s (%2$s) desde redes como %3$s a esta dirección. Usar otros tokens y redes puede resultar en la pérdida de fondos. + Cómo escanear + Solicitar soporte + Inténtelo de nuevo + Esta función está desactivada en el modo Demo + Razón: %s + No se puede completar una transacción + El seleccionado no admite la red %1$s + Para activar el %1$s cifrado criptográfico de la blockchain, necesitarás reiniciar la billetera a los ajustes de fábrica. Por favor, retire sus fondos antes de hacerlo para asegurarte de no perderlos, y luego complete el proceso de reinicio. El acceso a la billetera actual no será posible después del reinicio. + Esta tarjeta no admite %1$s tokens de red debido a una limitación del firmware. + ¿Tiene dificultades para escanear su tarjeta? + Esta tarjeta no está diseñada para funcionar con Tangem + Tarifa por defecto + Habilite las Tarifas predeterminadas para establecer automáticamente las tarifas de transacción y omitir la página de Tarifas al enviar fondos. Siempre puede volver a esta página si es necesario. + Vaya a ajustes para habilitar la autenticación biométrica en la Tangem App + Habilitar autenticación biométrica + Esto eliminará todos los códigos de acceso guardados de la billetera. Cualquier operación posterior con la billetera requerirá introducir el código de acceso. + Eliminar la tarjeta guardada borra todos las billeteras guardadas y sus códigos de acceso de la app. + Guardar código de acceso + Se solicitará la autenticación biométrica en lugar del código de acceso para interactuar con su tarjeta. + Mantener la billetera en la app + Habilite para vincular todos las billeteras a la app Tangem. Se requerirá autenticación biométrica para desbloquear la app. La firma de transacciones requiere tocar su tarjeta Tangem. + Oscuro + Claro + Predeterminado del sistema + Tema + Ajustes de la app + Para ocultar o mostrar tus saldos, simplemente gira la pantalla de tu dispositivo hacia abajo, o desactívalo en Ajustes + No mostrar de nuevo + Entendido + Los saldos están ocultos + Por favor, escanee la tarjeta + Por favor, inténtelo de nuevo en 30 segundos o escanee la tarjeta + Demasiados intentos + Ha desactivado la autenticación biométrica en su teléfono y no podrá guardar billeteras en la aplicación. Para guardar billeteras, active la función de autenticación biométrica en los ajustes de su teléfono. + Iniciar proceso de backup + Con su tarjeta bancaria o cuenta bancaria + + %d tarjeta + %d tarjetas + + Desactive esta opción si no quiere que esta tarjeta se use para reiniciar códigos de acceso en otras tarjetas de esta billetera. Tenga en cuenta que esto también evitará que reinicie el código de acceso en esta tarjeta. + Permite usar esta tarjeta para reiniciar el código de acceso en otras tarjetas de esta billetera + Recuperación de código de acceso + Reiniciar + ¿Está seguro de que quiere hacer esto? + Cambiar código de acceso + El código de acceso se cambiará solo en esta tarjeta + Todas las tarjetas de la billetera seleccionada se han restablecido a la configuración de fábrica. Ahora puedes crear una nueva billetera. + Reinicio completado + ¿Quiere restablecer la siguiente tarjeta en esta billetera? + Reinicio de tarjeta + Recomendamos completar el proceso de reinicio para todas las tarjetas en esta billetera. + No ha reseteado todas sus tarjetas + Restablecer a ajustes de fábrica + Modo de seguridad + Ajustes de la tarjeta + Además de la tarifa de red, la red Cardano cobra %1$s ADA al realizar transacciones con el %2$s token + Requisitos de transacción de Cardano + Para realizar una %1$s transacción, deba depositar algo de ADA para cubrir la tarifa de red y el valor mínimo de ADA (se recomiendan 5 ADA) + ADA insuficiente para la transferencia de tokens + Deba mantener algo de ADA porque tiene algunos tokens en la blockchain de Cardano + ADA insuficiente + Aceptar + Acceso denegado + Todos + Autorizar + Analítica + Aplicar + Aprobación + Aprobar + Atención + Soldo: %s + Soldo + autenticación biométrica + biometría + Comprar + Ir a %1$s + No ha otorgado acceso a su cámara, cambie su configuración de privacidad + Cancelar + Elige una acción + Reclame recompensas + Cerrar + Continuar + Copiar + Copiar la dirección + Crear + %1$s (%2$s) + Personalizado + + %d día + %d días + + Suprimir + Desactivado + Listo + Activar + Activado + Error + Intercambie + Explore + Explore historial de transacciones + Explorador(a) + Las tarifas de red son cargos que los usuarios pagan para procesar y confirmar transacciones. El importe de la tarifa puede verse afectado por la congestión de la red, el tamaño de la transacción y la prioridad de ejecución. %s + Rápido + Mercado + Lento + Velocidad y tarifa + Obtener direcciones + Ir al proveedor + Ir al token + Importe + + en %d día + en %d días + + Más tarde + Bloqueado + Red principal + Tarifa de la red + La cantidad enviada se reducirá en %1$s (%2$s) para cubrir el nivel de tarifa seleccionado + Siguiente + No + Ninguna dirección + Ahora + OK + Tarjeta principal + Frase de contraseña + Pegar + %1$s-%2$s + %1$s — %2$s + Leer más + Recibir + Rechazar + Recargar + Renombrar + Guarde + Guardar cambios + Buscar en el mercado + Buscar tokens + Seed phrase + Seleccione una acción + Vender + Enviar + El servidor no está disponible, por favor inténtelo de nuevo más tarde + Compartir + Firme + Firme y envíe + Stake + Staking + Empezar + Enviar + Con éxito + Soporte + Intercambiar + términos y condiciones + Hoy + Transacción fallida + Transacciones + Transferencia + Entiendo + Hubo un error. Por favor inténtelo de nuevo. + Inaccesible + Termine el staking + + Dirección del contract copiada + Redes disponibles + Agregue un token + Dirección del contract + La dirección del contract no es válida + Por favor, seleccione la red + Este token ya ha sido agregado a la lista + Este token ya existe + Los decimales deben ser un número entero válido, hasta %d + Derivación personalizada + Por ejemplo m/00\'/0000\'/0\'/0/0 + Introduzque una derivación personalizada + Decimales + Ruta de derivación + Por defecto + Coin de tipo BIP44 + La ruta de derivación que ha ingresado no es válida + Por ejemplo USD coin + Nombre + No elegido + Red + Red de tokens + Puede agregar manualmente un token que no sea compatible de forma nativa con Tangem + Por ejemplo USDC + Símbolo + Símbolo del token + Este token/red ya ha sido añadido a su lista + Tenga en cuenta que cualquiera puede crear tokens. Tenga cuidado al añadir tokens fraudulentos, pueden no costar nada. + Tenga cuidado al agregar tokens fraudulentos, pueden no costar nada + Tenga en cuenta que cualquiera puede crear tokens + Comprar Tangem Wallet + Chat + Código de acceso + Deberá ingresar la contraseña correcta antes de escanear la tarjeta. + Sostenga la tarjeta firmemente + Este mecanismo protege contra ataques sin contacto a la tarjeta. Hay un retraso entre la recepción y la ejecución del pedido. Después de la primera transacción firmada, ese teléfono se asociará con la tarjeta y las transacciones se firmarán inmediatamente. + Contraseña + Antes de ejecutar un comando que cambie el estado de la tarjeta, deberá ingresar una contraseña. + Programa de referidos + Gire la pantalla de su dispositivo hacia abajo para ocultar y mostrar rápidamente los saldos + %s hashes + ID de la tarjeta + Contacta con el equipo de soporte + Vincular más tarjetas + Moneda de la aplicación + Girar para ocultar saldos + Editor + Firmado + Enviar comentarios + Detalles + Compruebe su conexión a internet o cambia a una red diferente + Condiciones de uso + Ha usado una tarjeta de otro wallet. Toque la tarjeta asociada con esta billetera + Mis tokens + Aún no ha añadido ningún token. Agregue tokens a través de Market para hacer swap + No se puede intercambiar por %s + Proporcionado por + Estado + Tangem ofrece swaps de tokens a través de proveedores externos según los términos de cada proveedor + Elige un proveedor + Ha ocurrido un error. Código: %s + El proveedor seleccionado no está disponible actualmente. Inténtelo de nuevo más tarde. (Código: %s) + Los cambios no están disponibles en este momento. Inténtelo de nuevo más tarde. (Código: %s) + Cantidad estimada + Intercambio por %s + Visite el sitio web del proveedor para reembolsar su dinero + Operación fallida por el proveedor + El monto de la transacción fue reembolsado en %1$s a su billetera debido a OKX o reglas puente. %2$s + El monto fue reembolsado en %1$s (red %2$s) + Visite el sitio web del proveedor para la verificación + Verificación KYC requerida por el proveedor + Cancelado + Confirmado + Esperando confirmación + En proceso de confirmación + Intercambio + En intercambio + En intercambio + Fallido + Depósito recibido + Esperando depósito + Esperando depósito + Reembolsado + Transfiriendo a su cuenta + Enviando... + Enviado + Datos proporcionados por el proveedor. La cantidad estimada puede cambiar debido a las condiciones del mercado. + Estado del intercambio + Verificación requerida + Esperando hash de transacción + Lista de todos los tokens agregados a su billetera + Buscando las mejores tarifas… + Tasa flotante + Al utilizar la función de intercambio, acepta el %s + Al utilizar la función de intercambio, aceptas las condiciones de %1$s y %2$s del proveedor + Próximamente habrá más proveedores.\n¡Estén atentos! + Política de privacidad + Proveedor + Mejor tarifa + Disponible hasta %s + Disponible desde %s + No disponible para este par + Permiso requerido + Recomendado + Condiciones de uso + No se encontraron fichas. Por favor intenta con otra solicitud + ID : %s + ID de transacción copiado + Con otra moneda en su billetera + La siguiente información es opcional. Puede borrarla si no quiere compartirla. + Cuéntenos qué funciones echa de menos y trataremos de ayudarle. + Por favor, díganos qué tarjeta tiene + Hola equipo de soporte, + Por favor, cuéntenos más sobre tu problema. Cada pequeño detalle puede ayudar. + Mis recomendaciones + No se puede escanear una tarjeta + Comentarios + Comentarios en Tangem + No se puede completar una transacción + Esta transacción + La red cobrará una tarifa de aprobación de token para verificar que está autorizando el uso de su token para el swap. + Especifique el límite aprobado para el token seleccionado. + Montante %s + La función Aprobar es necesaria para otorgar permiso a otra dirección para usar una cantidad específica de sus tokens. Por diseño, los contratos inteligentes no pueden acceder a sus tokens sin su aprobación. Al \"desbloquear\" sus tokens, autoriza al contrato inteligente de StakeKit a usarlos. Los mineros de la red reciben una tarifa de gas (pagada por usted) por registrar esta acción en la cadena de bloques. Puede apostar su token después de dar su aprobación. + Para continuar, debe autorizar el contrato inteligente de StakeKit para utilizar su %s + Para continuar, de autorización a los smart contracts de %1s para usar su %2s + Dar autorización + Ilimitado + Ordenar + Escanee + Toque para cambiar la contraseña + Toque para cambiar la contraseña + Para crear la billetera, toque la tarjeta como se muestra arriba y no la retire hasta el final de la operación + Toque la tarjeta no. %s de la billetera + Coloque para escanear + Toque para firmar + Coloque la tarjeta + Ha actualizado la biometría, escanee su tarjeta para entrar + Su saldo debe ser mayor que el valor de la tarifa para hacer una transferencia + Saldo insuficiente + No tiene suficiente Mana para esta transacción. Por favor, espere hasta que el Mana se recargue. Su saldo de Mana es %1$s/%2$s + Mana insuficiente + Solo puede transferir %s debido al límite de Mana impuesto por la red Koinos + Límite de Mana + La red Koinos requiere Mana para las tarifas de red. Tienes %1$s/%2$s Mana + Nivel de Mana + Para comenzar a seguir tus activos crypto y transacciones, agregue tokens + Gestionar tokens + Para acceder a todas las redes necesita escanear la tarjeta + Escanee su tarjeta + Benefíciese de %1$s tarifa de servicio en intercambios a través de Changelly del %2$s al %3$s de febrero + Intercambio con Changelly, %s tarifa + Tokens + Agregar + Editar + Capitalización de mercado de la moneda + Blockchain en la que se creó inicialmente la criptomoneda + Red nativa + El uso de redes no nativas para tokens permite la interoperabilidad entre blockchains, permitiendo que los activos se utilicen en diversas aplicaciones descentralizadas y smart contracts en diferentes plataformas. Sin embargo, esto a menudo implica un custodio o smart contract para mantener el activo original de forma segura, introduciendo centralización y riesgo de contraparte. + No es la blockchain original o primaria donde se aloja el token + Redes no nativas + Elegir redes + Billetera + No se pudo encontrar este token, puedes añadirlo manualmente + + %1$d de %2$d billetera + %1$d de %2$d billeteras + + Eliminar + por ejemplo, BTC I trust, hodl I must + Su cartera ha sido actualizada + El token seleccionado no está disponible actualmente para acciones dentro de la billetera cripto. Pero no se preocupa, puede expresar su interés votando a favor. + Votar a favor + Elegir wallet + La billetera no admite más de una red + Para comprar, intercambiar o recibir este activo, agréguelo a su cartera + Este activo no está disponible + Añadir al portafolio + Agregue un token + Redes disponibles + Mi portafolio + Mercado + Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem + Los datos de este apartado proceden de las siguientes redes: %s + No se pueden cargar los datos… + Sin datos + Acciones rápidas + Resultado + Ver tokens con una capitalización de mercado de menos de $100,000 + Mostrar tokens + Sin resultado + Seleccione una red + Seleccione una billetera + 1 mes + 1 año + 24 horas + 3 meses + 6 meses + 7 días + Todo + Compradores experimentados + Evaluación + Ordenar por + Mejores Ganadores + Top Perdedores + Tendencias + Acerca de %s + + Basado en %d evaluación + Basado en %d evaluaciónes + + Sitio web + Presión de compra + La diferencia entre el volumen de compradores y el volumen de vendedores + Suministro circulante + El número total de monedas que están disponibles para el comercio y que circulan en el mercado. + Compradores experimentados + Compradores netos con el requisito adicional de tener al menos 100 transacciones salientes + Valoración totalmente diluida + El valor teórico total de una criptomoneda si todas las monedas que podrían existir estuvieran en circulación, incluidas las que actualmente no circulan. + Dato de Genesis + Alto + Titulares + El cambio en el número de poseedores de tokens dentro de un período de tiempo específico + Ideas + Enlaces + Liquidez + El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado + Índice de liquidez + Bajo + Capitalización de mercado + El valor total de mercado de una criptomoneda, calculado multiplicando el precio actual de la moneda por el número total de monedas en circulación. + Evaluación de mercado + Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado + Suministro máximo + Métrica + Enlaces oficiales + Rendimiento de precios + Repositorio + Puntuación de seguridad + Social + Suministro total + La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular + Volumen de operaciones (24 horas) + La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado. + Deba configurar un único código de acceso para proteger todas sus tarjetas + Proteger + Puede configurar un código de acceso individual en cada tarjeta más tarde + Personalizar + El código de acceso se puede recuperar con una tarjeta vinculada. No guarde todas las tarjetas en un mismo lugar. + Restaurar + Elige cualquier palabra, frase o número que quieras como su código de acceso + Crear un código de acceso + Vuelva a ingresar su contraseña para evitar un error + Vuelva a introducir su código de acceso + El código de acceso debe tener al menos 4 caracteres + El código de acceso introducido no coincide con el código de acceso inicial + Por favor repita la operación. La tarjeta se restablecerá a la configuración de fábrica. + Error de activación + Agregar tokens + Ha agregado una tarjeta de backup. Cuando el proceso de backup finalice, no podrá añadir más tarjetas de backup. Si tiene otra tarjeta, agréguela al backup. ¿Quiere continuar el proceso de backup? + El proceso de backup está parcialmente completo. No puede salir ahora. + La frase de contraseña es una característica de seguridad avanzada utilizada por las billeteras criptográficas. Agrega una palabra o frase adicional de su elección a su frase de recuperación ya existente para desbloquear un conjunto completamente nuevo de direcciones. + Agregar una tarjeta de backup + Escanee la tarjeta no. %d + Hacer backup ahora + Escanee la tarjeta principal + Continuar a mi billetera + Finalizar el backup + Recibir cripto + Escanee la tarjeta principal + Finalizar más tarde + ¿Cómo funciona? + Vamos a generar todas las claves en su tarjeta y crear una billetera segura + Crear una billetera + Crear una billetera + Otras opciones + Sus claves se generarán de forma segura dentro de la tarjeta. No hay seed phrase, lo que significa que nadie puede exportarla ni robarla. + Generar claves de forma privada + Su tarjeta está activada y lista para usar + ¡Éxito! + En este caso, necesitará empezar desde el principio. + ¿Quiere salir del proceso de activación? + Inicializando + Ya se ha creado otra billetera en la tarjeta que está intentando agregar. Si tiene fondos en esta billetera, por favor retírelos y luego reinicie esta tarjeta y agréguela como backup. + Creando un backup + Leer más sobre seed phrase + + Vacío + Escriba estas %d palabras en el orden que se indica a continuación y guárdelas en un lugar seguro y secreto. + + Su seed phrase + + Vacío + %d palabras + + Para importar su billetera, ingrese su seed phrase en el campo de abajo + Generar seed phrase + Importe una billetera + Una seed phrase es una serie de palabras que se permite recuperar su billetera. A diferencia de las claves generadas por la tarjeta, las seed phrases no están protegidas y pueden ser copiadas y robadas. Use esta opción bajo su propia responsabilidad. + Usar seed phrase + Seed phrase no válida. Por favor, comprueba el orden de las palabras. + Seed phrase no válida. Por favor, comprueba tu ortografía. + Legado + Para comprobar si has escrito correctamente su seed phrase, por favor introduce las palabras 2ª, 7ª y 11ª + Bien, vamos a comprobar + Para iniciar el proceso de backup, agregue hasta dos tarjetas de backup. + Puede agregar una tarjeta más o finalizar el proceso de backup + Prepare su tarjeta de backup con el no.%s + Escanee la tarjeta principal para iniciar el proceso de backup. + Prepare la tarjeta principal con el número %s + Su tarjeta billetera está configurada y lista para usar. + Número máximo de tarjetas añadido. Finalice el proceso de backup. + Activando tarjeta + Tarjeta de backup no.%d + Ningunas tarjetas de backup + Notificaciones + Una tarjeta de backup agregada + Prepare su tarjeta + Dos tarjetas de backup agregadas + Para empezar, simplemente recargue la billetera con cualquier cantidad + Para empezar, simplemente recargue la billetera con más de %1$s %2$s + Comprar cripto + Mostrar la dirección de la billetera + Activar una billetera + El proceso de emparejamiento está parcialmente completo. No puede salir ahora. + Si el proceso de creación del wallet se interrumpe de alguna manera, tendrá que empezar de nuevo + Puede hacer backup de sus claves en hasta dos tarjetas Tangem Wallet en blanco. + El código de acceso se puede restaurar con una de las tarjetas de backup. + Todas las tarjetas de backup se pueden usar como funcionales con las claves idénticas. + Podrá establecer un código de acceso para proteger sus billeteras. + Billetera de respaldo + Restaurar código de acceso + Tarjetas idénticas + Código de acceso + Agrupar + Por saldo + Organizar tokens + Desagrupar + Seleccione de la galería + Ajustes + No ha dado acceso a su cámara + Acceso a la cámara denegado + %1$s (%2$s) en la red %3$s + Envíe solo %s a esta dirección. Enviar cualquier otra moneda resultará en su pérdida irreversible. + Muestra un código QR o comparte tu dirección + Participar + Error al cargar la información sobre el programa de referidos. Por favor, inténtelo de nuevo más tarde. + Error al cargar la información sobre el programa de referidos. Código de error: %s. Por favor, inténtelo de nuevo más tarde. + Pagos próximos + Sus amigos compraron + Menos + Más + No hay pagos próximos + + para %d billetera + para %d billeteras + + Obtendrá ^^%1$s^^ por cada wallet comprada por su amigo en su dirección %3$s de la red %2$s ^^30 días después^^ + Usted + Obtendrá un + al comprar una billetera en tangem.com + %s descuento + Su amigo + ¡Código personal copiado! + Su código personal + ¡Compra Tangem Wallet con descuento!\n%s + Recomiende Tangem a sus amigos + Ha aceptado + Al tocar este botón, ud. acepta + del programa de referidos + + %d billetera + %d billeteras + + Restablecer la tarjeta + Entiendo que después de realizar esta acción, ya no tendré acceso a la billetera actual + Entiendo que no puedo usar esta tarjeta para recuperar mi código de acceso en las otras tarjetas de la wallet actual + El restablecimiento de fábrica eliminará completamente la billetera de la tarjeta seleccionada. No podrá restaurar la billetera actual ni usar la tarjeta para recuperar el código de acceso. + El restablecimiento de fábrica eliminará completamente la billetera de la tarjeta seleccionada y lo eliminará de la app. No podrá restaurar la billetera actual. + ¿Tiene una tarjeta bancaria de otro país y un permiso de residencia o registro fuera de la Federación Rusa? + Las tarjetas bancarias rusas no se aceptan actualmente + Inicie sesión en la app y comprueba su saldo sin escanear la tarjeta + Acceder a la app + Permitir el uso de biometría + Se solicitará la biometría en lugar del código de acceso para interactuar con su billetera + Código de acceso + Parece que tiene la autenticación biométrica deshabilitada, es necesaria para guardar billeteras + Activar la autorización biométrica + ¿Le gustaría usar la biometría? + Tenga en cuenta que realizar una transacción con sus fondos seguirá requiriendo su tarjeta + Escanee su tarjeta + Escanee la tarjeta para cambiar sus ajustes. Los cambios afectarán solo a la tarjeta que has escaneado y no afectarán a otras tarjetas vinculadas a su billetera. + ¡Prepare su tarjeta! + Ya incluido en la dirección ingresada + El monto de la comisión es %s veces el monto recomendado. Asegúrese de que la configuración personalizada sea correcta. + Ha especificado una comisión inferior a la cantidad recomendada, lo que podría provocar un retraso en su transacción. ¿Continuar? + Razón: %1$s\nCódigo: %2$s + La transacción está incompleta + Montante + Puede establecer tu tarifa de transacción ajustando el valor en el campo\nSatoshi por vByte. + La tarifa que se cobrará por tu transacción. Puedes establecer tu propio valor. + Tarifa máxima + Este es el coste que está dispuesto(a) a pagar por cada unidad de gas. Cuanto más alto sea el precio del gas, más rápido se procesará su transacción. (Tarifa prioritaria incluida) + Tarifa prioritaria + La tarifa que un usuario puede pagar a los mineros o validadores para acelerar la inclusión de su transacción en un bloque. + La tarifa que se cobra por utilizar cada salida de transacción no gastada (UTXO) en la red Kaspa. Cuanto más UTXO utilice en una transacción, mayor será la tarifa. + KAS por UTXO + %1$s, %2$s + ID de destino + Introduce la dirección + La dirección es la misma que su billetera. + Memo no válido. No se añadirá a la transacción. + Memo + Compruebe su conexión de red + Información de tarifa de red no accesible + Desde + Límite de gas + Este es el máximo de gas que se gastará para completar una transacción o contrato. Un límite de gas evita cargos inesperados o ilimitados al ejecutar una transacción. + Precios del gas + Este es el coste que estás dispuesto a pagar por cada unidad de gas. Cuanto más alto sea el precio del gas, más rápido se procesará tu transacción. + Max + Importe máximo + Tarifa hasta + Memo no válido + Cobertura de tarifa de red + Fondos insuficientes para la transferencia, ya que el total de la tarifa y el importe de la transferencia supera el saldo existente + El total supera el saldo + Se requiere un saldo de al menos %s para mantener su cuenta en blockchain para evitar riesgos de seguridad. Este monto permanecerá en su saldo y no podrá retirarse. + Depósito existencial + El importe de la comisión es %s veces la cantidad recomendada. Asegúrese de que los ajustes personalizados sean correctos. + Los aranceles aduaneros son altos + Debido a las peculiaridades de la red %1$s, la tarifa por transferir el saldo completo es más alta. Para reducir la comisión, puedes dejar %2$s. + La tarifa es más alta + La comisión incluida excede el monto de la transferencia, lo que resulta en un valor negativo + Cantidad no válida + La cantidad mínima de envío es %1$s. Asegúrate de que el saldo restante después del envío no sea menor que %2$s. + La cuenta de destino no está creada. Por favor, cambia la cantidad a enviar. + La cantidad a enviar debe ser al menos %s + Salir %s + Reducir por %s + Reducir a %s + Tenga en cuenta que su transacción puede experimentar retrasos bajo configuraciones específicas de tarifas + Es posible que haya retrasos en las transacciones + Debido a las limitaciones de %1$s, solo %2$s UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Necesitas reducir la cantidad. + Limitación de transacciones + Opcional + Por favor, alinee su código QR con el cuadrado para escanearlo. Asegúrese de escanear la dirección de la red %s. + Reciente + Destinatario + No es una dirección válida + Asegúrese de que la dirección de la billetera receptora esté en la red %s para evitar perder sus tokens + Destinatario + Un Memo/ID de destino es un ID único para diferenciar transacciones enviadas al mismo destinatario en la misma red. Precaución: Omitir un memo puede llevar a la pérdida de fondos + Mis billeteras + Una forma de medir las tarifas de transacción de Bitcoin. Indique el número de la unidad más pequeña de Bitcoin (Satoshi) por cada byte virtual en una transacción. Cuanto más alto sea el número, más rápido procesarán los mineros la transacción. + Satoshi / vByte + Enviando... + Toque cualquier campo para editarlo + Enviar %s + Está enviando **%1$s** incluida una tarifa de red de %2$s + Está enviando**%1$s** y %2$s + La transacción se firmó con éxito y se envió al nodo blockchain. El saldo de la billetera se actualizará después de un tiempo. + %1$s es un activo en la red Tron. Para calcular la tarifa y realizar una transacción, deba depositar algo de Tron (TRX) en su cuenta. + Transacción enviada + Escanee la tarjeta que quiere configurar + Olvidar la billetera + Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. + Nombre + Activo + Para eliminar el staking de sus activos, presione el bloque de arriba + El monto del staking debe ser al menos %s + Unstaking de la reclamación + Porcentaje de rendimiento anual + El porcentaje de rendimiento anual que podrá ganar como participante en el staking. + APR + Las recompensas se acumulan automáticamente en su saldo de apustaking esta cada día. + Disponible + Tasa de recompensa promedio + ¿Qué es el Staking? + %s beneficio estimado + Calificación de mercado + Métrica + Mínimo requerido + No hay recompensas para reclamar + Reclamo de recompensa + Método para recibir recompensas por apuesta.\nPuede ser automático, donde la recompensa se acredita en tu dirección, o manual, donde debes retirar la recompensa creando una transacción para recibirla. + Calendario de recompensas + Este es un cronograma que determina cuándo los participantes reciben sus recompensas. + Recompensas para reclamar: %s + Staking %s + Periodo de disociación + El período que debes esperar después de solicitar el retiro de fondos del staking antes de que los tokens estén disponibles. + Periodo de calentamiento + El tiempo permitido para activar la participación en la apuesta. + Bloqueado + Migrar + Native staking + El staking le permite ganar %1$s. Sus recompensas por apostar llegan todos los días. + El staking le permite ganar %1$s. Sus recompensas del staking llegan cada hora. + El staking le permite ganar %1$s. Sus recompensas del staking llegan todos los meses. + El staking le permite ganar %1$s. Sus recompensas por apostar llegan todas las semanas. + Gane recompensas por staking + Las recompensas se acumulan inmediatamente después de comenzar el unstaking. El proceso de unstaking dura %s. + El unstaking de sus fondos está hecho. Ahora puede retirarlos con las recompensas obtenidas + Listo para retirar + Reunir + Haga el staking de nuevo + Restaking de las recompensas + Revocar + Revotar + Auto + Manual + Bloquear + Día + Cada día + Época + Era + Hora + Mes + Semana + Recompensas + El stake está bloqueado + Hacer más staking + Stake %s + El unstaking de %s + Desunión + Desbloquear + Sin staking + Marque activos sin staking para reclamarlos + Unstaking + Validador + Validadores + Votar + Voto bloqueado + Retirar + Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta + Billetera de hardware revolucionaria + Hasta 3 tarjetas físicas por billetera + Backup ultra seguro + Una billetera de hardware para su Bitcoin, Ethereum y muchas más monedas simultáneamente – todo en una sola tarjeta + Miles de monedas + Úselo en cualquier lugar, en cualquier momento. Sin cables ni baterías. Solo toque la tarjeta con su teléfono cuando necesites su cripto. + La billetera para todos + Descubra Tangem + Intercambie, compre NFT, haga préstamos y depósitos en más de 100 servicios descentralizados diferentes + Compatible con Web 3.0 + Intercambie más tokens a mejores tasas directamente en su billetera. + ¡Nuevo proveedor de intercambio disponible! + El monto incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. + El importe incluye los honorarios del proveedor de servicios. + Tarifa + Todos los exchanges descentralizados requieren aprobaciones para evitar que los smart contracts accedan a su billetera sin su permiso. Por diseño, los smart contracts no pueden acceder a tus tokens a menos que lo apruebes. Al \"desbloquear\" sus tokens, autoriza al smart contract de 1-inch a gastarlos. Los mineros de la red reciben una tarifa de gas (pagada por voz) para registrar esta acción en la blockchain. Puede intercambiar su token después de dar la aprobación. + Aprobar + Error en la estimación de la tarifa. Envíe sus comentarios al servicio de asistencia. + Usted intercambia + Hacer swap de esta cantidad de tokens seleccionados causará un impacto significativo en el precio y reducirá su resultado. + Fondos insuficientes + Dar autorización + En progreso + Intercambie + Usted recibe + Elige token + no disponible + Saldos ocultos + Saldos mostrados + Cancelar + Esta operación no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. + Comprar %s no está disponible en este momento. Por favor revise sus actualizaciones. + No tiene fondos para vender. Recargue su cuenta para poder vender fondos desde ella. + No tienes fondos para enviar. Recarga tu cuenta para poder enviar fondos desde ella. + El servicio de intercambio %s no está disponible en este momento. Por favor consulte nuestras actualizaciones. + La venta de fondos estará disponible una vez que la(s) transacción(es) pendiente(s) en la red %s se complete + El envío de fondos estará disponible una vez que se completen las transacciones pendientes en la red %s. + La venta de %s no está disponible en este momento. Por favor consulte nuestras actualizaciones. + El staking %s no está disponible en este momento. Por favor consulte nuestras actualizaciones. + Generar XPUB + Ocultar + Está a punto de ocultar este token de la pantalla principal. Puede volver a agregarlo en cualquier momento a través de la página de gestión de tokens. + Ocultar %s + Ocultar el token + El staking le permite ganar %1$s y obtener recompensas cada %2$s días + Gane hasta %s recompensa del staking por año + Token de %1$s en la red %%image%% %2$s + Token en la %image% red %1$s + El token %1$s (%2$s) es la moneda principal en la red %3$s y no se puede ocultar mientras tengas otros tokens de esta red en la lista + No se puede ocultar %s + Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. + Intercambio con Changelly, %s tarifa + Intercambie ahora + contacto: %s + Aún no tiene ninguna transacción + Error al cargar el historial de transacciones.\nHaga clic en el botón de recarga para actualizar la información. + Múltiples direcciones + El historial de transacciones no está soportado actualmente para esta blockchain. Pero no se preocupe, ¡estamos trabajando en ello! Mientras tanto, puede consultarlo en el explorador. + Operación + desde: %s + a: %s + Inténtelo de nuevo + Ha escaneado la misma tarjeta. Para crear una billetera gemela, necesite escanear la tarjeta con no. %d + Ha escaneado la tarjeta gemela incorrecta. Por favor, intente con otra + Esta que tiene en sus manos y la otra con el número %s.\n\nAmbas tarjetas pueden usarse para extraer fondos de esta billetera. + Una billetera. Dos tarjetas. + Escanee la tarjeta no. %s + Creando la billetera + Escanear tarjeta gemela no. %s + Preparando la tarjeta + Tangem Twin + Esta acción es irreversible. No tendrá acceso la billetera antigua. + Toque la tarjeta gemela con el número %s y no la retira hasta el final de la operación + Use %s o escanee una tarjeta para tener acceso a su billetera + Manténgase actualizado con las últimas funciones y noticias + Sea el primero en enterarte de nuevas promociones + ¿Quiere utilizar\nnotificaciones push? + Agregar una nueva billetera + ¿Está seguro(a) de que quieres eliminar esta billetera? + Ha ocurrido un error, por favor escanee su tarjeta para iniciar sesión + Esta billetera ya se ha guardado, puede agregar otro + La billetera con el nombre %s ya existe + Nombre de la billetera + Renombrar la billetera + Desbloquear todo + Desbloquear todo con %s + La blockchain está Inaccesible. Inténtelo más tarde + Escanee la tarjeta + Solicitud para firmar un mensaje.\n\n%s + Dapp %1$s, solicitando\nfirmar transacción BNB.\n\n%2$s + Orden de intercambio por %1$s\nPrecio: %2$s\nCantidad a recibir: %3$s\nCantidad a pagar: %4$s + Detalles de la transacción:\nDesde: %1$s\nA: %2$s\nMontante: %3$s + El portapapeles contiene un código de WalletConnect. Use el valor copiado o escanee el código QR + Solicitud para crear una transacción para %1$s\n%2$s\n\nMontante: %3$s\nComisión: %4$s\nTotal: %5$s\nSaldo: %6$s + No se puede completar la transacción. Fondos insuficientes. + Error al establecer la sesión de WalletConnect. Por favor, inténtelo más tarde. + Error al firmar el mensaje.\nPor favor, inténtalo de nuevo + Error al establecer la sesión de WalletConnect: error de tiempo de espera. Por favor, inténtelo más tarde. + La solicitud de sesión contiene blockchains no compatibles para la conexión WalletConnect. Blockchains no compatibles:\n + La conexión con esta dApp no se puede establecer debido a su implementación técnica. + Hemos encontrado un error desconocido. Mensaje de error: %s. Si el problema persiste, no duda en contactar con nuestro soporte + Tarjeta incorrecta seleccionada en Tangem App + Error al crear la transacción a partir de los datos de la dApp. Código: %s + Hemos encontrado un error desconocido. Mensaje de error: %s. Si el problema persiste, contacte con nuestro soporte + No hay sesiones abiertas de WalletConnect + ¡Qué lástima! No hay sesiones. + Error al emparejar la sesión de WalletConnect: %1$s + Pegar desde el portapapeles + Mensaje para %1$s:\n%2$s + Solicitud para iniciar una sesión para\n%1$s\n\nRED: %2$s\n\nURL:%3$s + No se pudo completar la operación.\n\nYa ha establecido una sesión de WalletConnect con estos parámetros. + Escanear nuevo código + Esta tarjeta no se puede usar para establecer una sesión de WalletConnect + Esta red no es compatible. Por favor, seleccione otra red. + Seleccione una red + Sesiones de WalletConnect + Conectar a dApps + WalletConnect + La conexión puede tardar unos segundos. + %s Precio de mercado + últimas 24h + %s red + La dirección se copió correctamente + Sin conexión a internet + Ajusta de la wallet + Tangem + Use %s o escanee una tarjeta para desbloquear el acceso a su billetera + El proceso de obtención de permisos está actualmente en marcha y se completará pronto. + Aprobación en curso + Parece que la activación de la tarjeta no ha ido correctamente. Esto puede deberse a un problema con el módulo NFC de su dispositivo o a una mala conexión de la tarjeta de su dispositivo. Comuníquese con nuestro equipo de soporte para obtener ayuda. + Error de activación + Según los desarrolladores de la red BNB, el soporte para el estándar BEP-2\nfinalizará en junio de 2024. Para evitar perder activos con este estándar, por favor conviértalos al estándar BEP-20. Usa nuestro servicio de swap para transferirlos a la red BNB Smart Chain. + BNB Beacon Chain se cerrará + Podría ser mejor + Me gusta + Entendido + ¡Realmente genial! + Actualizar + Actualmente estás en el modo Demo + Modo demo activo + La tarjeta que ha escaneado es una tarjeta de desarrollador. No la use para crear su billetera. + ¡No para usuarios! + La red %1$s requiere un Depósito Existencial. Si su cuenta cae por debajo de %2$s, se desactivará y se destruirán los fondos restantes. + La red requiere Depósito Existencial + El intercambio estará disponible una vez que se complete la transacción de %s. + Tiene una transacción activa + La aprobación del intercambio está en progreso y se completará en breve + Aprobación en proceso + El monto mínimo de transacción es %1$s. Asegúrese de que el saldo restante después del canje no sea inferior a %2$s. + No tiene %s monedas negociables en su lista + No hay tokens disponibles para intercambiar + Para realizar una transacción necesita depositar %1$s %2$s + No se pueden cubrir %s tarifa + El monto a recibir debe ser de al menos %s + Esto puede suceder porque el proveedor actualmente no puede operar con el par que usted seleccionó. Espere un momento e inténtelo de nuevo. (Código %s) + El par seleccionado no está disponible temporalmente + Servicio no disponible temporalmente + La cantidad de tokens a intercambiar no debe exceder %s + El monto a cambiar debe ser de al menos %s + Por favor cambie la cantidad a cambiar + Esta tarjeta podría ser una muestra de producción o una falsificación + La verificación de autenticidad falló + Asociar + Este token debe estar asociado a tu cuenta de Hedera antes de que pueda recibirlo. Tarifa de asociación ~%1$s %2$s + Este token debe estar asociado con su cuenta de Hedera antes de poder recibirlo. + Asocie su token + No hay suficiente %s. Recargue su cuenta de Hedera para asociar este token + Solo quedan %s firmas en esta tarjeta. Deba retirar todos sus fondos. + Recuento de firmas bajo + Los tokens en diferentes redes pueden tener direcciones diferentes. Verifique que su dirección coincida con la red cuando transfieras fondos. + Actualmente, MATIC está migrando a POL. Sin embargo, no se ha fijado ninguna fecha límite y MATIC aún no está obsoleto. Puede seguir usando el token MATIC de forma segura o utilizar intercambios para cambiarlo por POL. + Migración de MATIC a POL + + Use su tarjeta para obtener una dirección para la red + Use su tarjeta para obtener direcciónes para la red + + Faltan algunas direcciones + La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. + La red no está disponible + Recargue su billetera + Su billetera no ha sido respaldado. Realice este procedimiento para proteger sus activos ahora. + Falta backup + Esta tarjeta se ha utilizado previamente para transacciones. Si la recibió de una fuente no confiable, considere retirar todos los fondos. Si es su tarjeta, no se requiere ninguna acción. + La tarjeta ya ha firmado transacciones + Su opinión nos motiva a hacer Tangem Wallet aún mejor + ¿Disfrutando de Tangem? + Deba asociar su token antes de recibir tokens + Se requiere tarifa de alquiler de red + %1$s un activo en la red %2$s. Para realizar una transacción %3$s, deposite %4$s (%5$s) para cubrir la tarifa de red. + %1$s insuficiente para cubrir la tarifa de red + La red Solana está congestionada. Si su transacción no se procesa dentro de 2 minutos, repita la transacción. + Alerta de red Solana + La red Solana cobra un alquiler de %1$s cada %2$s días. Las cuentas que no pueden pagar el alquiler son eliminadas de la red. Deposite en su cuenta más de 2 para usarla de forma gratuita. + Algunas redes no están disponibles actualmente. Por favor, inténtalo de nuevo más tarde. + Algunas redes no están disponibles + Esta es una tarjeta de Testnet. No puede procesar transacciones y solo deba usarse para pruebas y desarrollo. + Solo para fines de prueba + Ignorar + Tiene un backup interrumpido. ¿Quiere reanudarlo? + Sí, reanudar + Descartar + Si descarta el backup ahora, tendrá que restablecer las tarjetas a los ajustes de fábrica para empezar de nuevo + Reanudar backup + Esta es una acción irreversible + Iniciar sesión con %s + Escanee la tarjeta + Use %s o escanee una tarjeta para acceder a la app + ¡Bienvenido de nuevo! + From 0802dfb9fc94051745755eecfc6b0063ad5620ff Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 12:50:27 +0400 Subject: [PATCH 108/197] Updated on 2026-08-14 --- .../com/tangem/tap/di/domain/SettingsDomainModule.kt | 8 ++++++++ .../datasource/local/preferences/PreferencesKeys.kt | 2 ++ .../tangem/data/settings/DefaultSettingsRepository.kt | 11 +++++++++++ .../settings/ShouldShowMarketsTooltipUseCase.kt | 10 ++++++++++ .../settings/repositories/SettingsRepository.kt | 4 ++++ 5 files changed, 35 insertions(+) create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowMarketsTooltipUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index bebc27226f..1b5860c2c0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -57,6 +57,14 @@ internal object SettingsDomainModule { return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository) } + @Provides + @Singleton + fun provideShouldShowMarketsTooltipUseCase( + settingsRepository: SettingsRepository, + ): ShouldShowMarketsTooltipUseCase { + return ShouldShowMarketsTooltipUseCase(settingsRepository = settingsRepository) + } + @Provides @Singleton fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 9b01024cc8..d2c600c572 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -83,6 +83,8 @@ object PreferencesKeys { val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") } + val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } + val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 0fd87454fa..d9122fb1b3 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -88,4 +88,15 @@ internal class DefaultSettingsRepository( preferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY] = count + 1 } } + + override suspend fun shouldShowMarketsTooltip(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, + default = true, + ) + } + + override suspend fun setMarketsTooltipShown(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, value = !value) + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowMarketsTooltipUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowMarketsTooltipUseCase.kt new file mode 100644 index 0000000000..14acc20013 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowMarketsTooltipUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +class ShouldShowMarketsTooltipUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.shouldShowMarketsTooltip() + + suspend operator fun invoke(isShown: Boolean) = settingsRepository.setMarketsTooltipShown(isShown) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 50b3e3624d..6067b99a64 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -29,4 +29,8 @@ interface SettingsRepository { suspend fun setShouldSaveAccessCodes(value: Boolean) suspend fun incrementAppLaunchCounter() + + suspend fun shouldShowMarketsTooltip(): Boolean + + suspend fun setMarketsTooltipShown(value: Boolean) } \ No newline at end of file From 1974534e52651574e5ab4960aa2bd853a0246b9b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 12:54:14 +0400 Subject: [PATCH 109/197] Updated on 2026-08-14 --- .../common/preview/WalletScreenPreviewData.kt | 2 ++ .../wallet/state/WalletStateController.kt | 2 ++ .../wallet/state/model/WalletScreenState.kt | 2 ++ .../InitializeWalletsTransformer.kt | 1 + .../wallet/viewmodels/WalletViewModel.kt | 23 +++++++++++++++---- .../intents/WalletContentClickIntents.kt | 11 +++++++++ 6 files changed, 37 insertions(+), 4 deletions(-) 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 63585b4bbe..77ae8c206f 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 @@ -158,5 +158,7 @@ internal object WalletScreenPreviewData { onWalletChange = {}, event = consumedEvent(), isHidingMode = false, + showMarketsOnboarding = false, + onDismissMarketsOnboarding = {}, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index a54ca605c0..6dc50ed38f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -94,6 +94,8 @@ internal class WalletStateController @Inject constructor() { onWalletChange = {}, event = consumedEvent(), isHidingMode = false, + showMarketsOnboarding = false, + onDismissMarketsOnboarding = {}, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 160e0d11da..ce541eaaa0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -13,4 +13,6 @@ internal data class WalletScreenState( val onWalletChange: (Int) -> Unit, val event: StateEvent, val isHidingMode: Boolean, + val showMarketsOnboarding: Boolean, + val onDismissMarketsOnboarding: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 0ead18d878..24b9bb0afd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -34,6 +34,7 @@ internal class InitializeWalletsTransformer( } .toImmutableList(), onWalletChange = clickIntents::onWalletChange, + onDismissMarketsOnboarding = clickIntents::onDismissMarketsOnboarding, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index e4fc8b0bff..9e6aa0e4ce 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -7,10 +7,7 @@ import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled -import com.tangem.domain.settings.ShouldAskPermissionUseCase -import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase @@ -30,6 +27,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull @@ -58,6 +56,7 @@ internal class WalletViewModel @Inject constructor( private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, + private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -69,6 +68,7 @@ internal class WalletViewModel @Inject constructor( private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, + private val marketsFeatureToggles: MarketsFeatureToggles, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -83,6 +83,7 @@ internal class WalletViewModel @Inject constructor( analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) suggestToEnableBiometrics() + suggestToOpenMarkets() maybeMigrateNames() subscribeToUserWalletsUpdates() @@ -121,6 +122,20 @@ internal class WalletViewModel @Inject constructor( } } + private fun suggestToOpenMarkets() { + viewModelScope.launch { + withContext(dispatchers.io) { delay(timeMillis = 1_800) } + + if (marketsFeatureToggles.isFeatureEnabled && shouldShowMarketsTooltipUseCase()) { + stateHolder.update { + it.copy(showMarketsOnboarding = true) + } + } + + shouldShowMarketsTooltipUseCase(isShown = true) + } + } + private suspend fun isShowSaveWalletScreenEnabled(): Boolean { return router.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase() } 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 6b0d74852e..ba469eaf52 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 @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.TokensAction @@ -35,6 +36,8 @@ internal interface WalletContentClickIntents { fun onOrganizeTokensClick() + fun onDismissMarketsOnboarding() + fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus) fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) @@ -52,6 +55,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val reduxStateHolder: ReduxStateHolder, @@ -98,6 +102,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } + override fun onDismissMarketsOnboarding() { + stateHolder.update { it.copy(showMarketsOnboarding = false) } + viewModelScope.launch { + shouldShowMarketsTooltipUseCase(isShown = true) + } + } + override fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send(PortfolioEvent.TokenTapped) router.openTokenDetails(stateHolder.getSelectedWalletId(), currencyStatus) From 49658c18bcb71fadf55bd96916bb7a0e8fa6726a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 12:54:43 +0400 Subject: [PATCH 110/197] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 + .../presentation/wallet/ui/WalletScreen.kt | 173 +++++++++++++++--- 2 files changed, 149 insertions(+), 26 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b89491b528..2a762eeab8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -414,6 +414,8 @@ The maximum number of coins or tokens that can ever exist for a particular cryptocurrency Trading volume (24h) The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market + Pull this up or tap the search bar to add tokens directly from the market + Add tokens You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index d384ce9f67..1991328d2c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -3,10 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.TweenSpec -import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.* import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -22,7 +22,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.luminance import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity @@ -34,6 +37,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -58,6 +63,9 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.core.ui.utils.lineTo +import com.tangem.core.ui.utils.moveTo +import com.tangem.core.ui.utils.toPx import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* @@ -75,10 +83,11 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balances import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator import com.tangem.features.markets.entry.BottomSheetState -import com.tangem.features.markets.entry.BottomSheetState.* import com.tangem.features.markets.entry.MarketsEntryComponent import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.math.roundToInt @Composable internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent?) { @@ -223,7 +232,7 @@ private fun WalletContent( } if (marketsEntryComponent != null) { - val bottomSheetState = remember { mutableStateOf(COLLAPSED) } + val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } var headerSize by remember { mutableStateOf(0.dp) } @@ -357,8 +366,9 @@ private inline fun BaseScaffoldWithMarkets( snackbarHostState = snackbarHostState, ) - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() } + val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() } val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight val maxHeight = LocalWindowSize.current.height @@ -420,28 +430,139 @@ private inline fun BaseScaffoldWithMarkets( }, ) - Column { - WalletTopBar(config = state.topBarConfig) - Box( - modifier = Modifier.pullRefresh(pullRefreshState), - ) { - content() + Box { + Column { + WalletTopBar(config = state.topBarConfig) + Box( + modifier = Modifier.pullRefresh(pullRefreshState), + ) { + content() - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } } - } - BottomSheetScrim( - color = BottomSheetDefaults.ScrimColor, - visible = bottomSheetState.targetValue == SheetValue.Expanded, - onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, - ) + BottomSheetScrim( + color = BottomSheetDefaults.ScrimColor, + visible = bottomSheetState.targetValue == SheetValue.Expanded || state.showMarketsOnboarding, + onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, + ) + + MarketsTooltip( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 24.dp) + .fillMaxWidth(fraction = 0.7f), + isVisible = state.showMarketsOnboarding, + availableHeight = maxHeight - statusBarHeight - bottomBarHeight, + bottomSheetState = bottomSheetState, + ) + } }, ) + + LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) { + if (state.showMarketsOnboarding && bottomSheetState.targetValue == SheetValue.Expanded) { + state.onDismissMarketsOnboarding() + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun MarketsTooltip( + availableHeight: Dp, + bottomSheetState: SheetState, + isVisible: Boolean, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + val tooltipOffset by remember { + derivedStateOf { + val bottomSheetOffset = try { + // Can throw exception during the first composition + with(density) { bottomSheetState.requireOffset().toDp() } + } catch (e: Exception) { + 0.dp + } + + bottomSheetOffset - availableHeight + } + } + + var visible by remember { mutableStateOf(value = false) } + LaunchedEffect(isVisible) { + if (isVisible) { + delay(timeMillis = 300) + } + + visible = isVisible + } + + val slideOffset = 40.dp.toPx() + AnimatedVisibility( + modifier = modifier.absoluteOffset(y = tooltipOffset), + visible = visible, + enter = slideIn( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + visibilityThreshold = IntOffset.VisibilityThreshold, + ), + initialOffset = { _ -> IntOffset(y = -slideOffset.roundToInt(), x = 0) }, + ) + fadeIn(), + exit = fadeOut(), + ) { + MarketsTooltipContent() + } +} + +@Composable +private fun MarketsTooltipContent(modifier: Modifier = Modifier) { + val backgroundColor = TangemTheme.colors.background.primary + val cornerRadius = CornerRadius(x = 14.dp.toPx()) + val tipDpSize = DpSize(width = 20.dp, height = 8.dp) + + Column( + modifier = modifier + .padding(bottom = tipDpSize.height) + .drawBehind { + val rect = size.toRect() + val tipSize = tipDpSize.toSize() + val tipRect = Rect( + offset = Offset( + x = rect.center.x - tipSize.center.x, + y = rect.bottom, + ), + size = tipSize, + ) + drawRoundRect(color = backgroundColor, cornerRadius = cornerRadius) + + val tipPath = Path().apply { + moveTo(tipRect.topLeft) + lineTo(tipRect.bottomCenter) + lineTo(tipRect.topRight) + } + drawPath(color = backgroundColor, path = tipPath) + } + .padding(all = 12.dp), + verticalArrangement = Arrangement.spacedBy(space = 4.dp), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = stringResource(id = R.string.markets_tooltip_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResource(id = R.string.markets_tooltip_message), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) } } @@ -449,7 +570,7 @@ private inline fun BaseScaffoldWithMarkets( private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { val alpha by animateFloatAsState( targetValue = if (visible) 1f else 0f, - animationSpec = TweenSpec(), + animationSpec = tween(), label = "scrim", ) val dismissSheet = if (visible) { @@ -557,9 +678,9 @@ private fun BottomSheetStateEffects( LaunchedEffect(isSheetHidden) { onBottomSheetStateChange( if (isSheetHidden) { - COLLAPSED + BottomSheetState.COLLAPSED } else { - EXPANDED + BottomSheetState.EXPANDED }, ) } From 40a7fa31abb798a20d2377d439f382775c01238e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 13:33:54 +0400 Subject: [PATCH 111/197] Updated on 2026-08-14 --- .../core/ui/components/rows/ChainRow.kt | 33 +++++++--- .../core/ui/components/rows/RowComponents.kt | 2 +- .../preview/PreviewManageTokensComponent.kt | 26 +++++--- .../entity/item/CurrencyItemUM.kt | 8 +++ .../managetokens/ui/ManageTokensScreen.kt | 62 +++++++++++++++---- .../utils/ui/CurrencyItemOperations.kt | 8 ++- 6 files changed, 106 insertions(+), 33 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt index c57b6ebc1a..7b8f2c9fff 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt @@ -25,15 +25,8 @@ import com.tangem.core.ui.res.TangemThemePreview * */ @Composable fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composable BoxScope.() -> Unit = {}) { - RowContentContainer( - modifier = modifier - .heightIn(min = TangemTheme.dimens.size68) - .padding(vertical = TangemTheme.dimens.spacing8) - .padding( - start = TangemTheme.dimens.spacing8, - end = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ChainRowContainer( + modifier = modifier, icon = { CurrencyIcon( state = model.icon, @@ -57,6 +50,28 @@ fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composab ) } +@Composable +inline fun ChainRowContainer( + icon: @Composable BoxScope.() -> Unit, + text: @Composable BoxScope.() -> Unit, + action: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, +) { + RowContentContainer( + modifier = modifier + .heightIn(min = TangemTheme.dimens.size68) + .padding(vertical = TangemTheme.dimens.spacing8) + .padding( + start = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + icon = icon, + text = text, + action = action, + ) +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt index f93f42b41d..ede73396a0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -12,7 +12,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @Composable -internal inline fun RowContentContainer( +inline fun RowContentContainer( icon: @Composable BoxScope.() -> Unit, text: @Composable BoxScope.() -> Unit, action: @Composable BoxScope.() -> Unit, 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 76be782c89..ff77045832 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 @@ -25,7 +25,9 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -internal class PreviewManageTokensComponent : ManageTokensComponent { +internal class PreviewManageTokensComponent( + private val isLoading: Boolean = false, +) : ManageTokensComponent { private val changedItemsIds: MutableSet = mutableSetOf() @@ -94,10 +96,14 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { } private fun initItems() = List(size = 30) { index -> - if (index < 2) { - getCustomItem(index) + if (isLoading) { + CurrencyItemUM.Loading(index) } else { - getBasicItem(index) + if (index < 2) { + getCustomItem(index) + } else { + getBasicItem(index) + } } }.toPersistentList() @@ -138,8 +144,8 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { network = Network( id = Network.ID(networkIndex.toString()), backendId = networkIndex.toString(), - name = "", - currencySymbol = "", + name = "Network $networkIndex", + currencySymbol = "N$networkIndex", derivationPath = Network.DerivationPath.Card(""), isTestnet = false, standardType = Network.StandardType.ERC20, @@ -164,7 +170,9 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { CurrencyItemUM.Basic.NetworksUM.Collapsed }, ) - is CurrencyItemUM.Custom -> return + is CurrencyItemUM.Custom, + is CurrencyItemUM.Loading, + -> return } previewState.update { state -> @@ -199,7 +207,9 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { item.copy(networks = updatedNetworks) } - is CurrencyItemUM.Custom -> return + is CurrencyItemUM.Custom, + is CurrencyItemUM.Loading, + -> return } val id = "${currencyIndex}_$networkIndex" diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyItemUM.kt index b61c576c2f..d81b14e587 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyItemUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyItemUM.kt @@ -40,4 +40,12 @@ internal sealed class CurrencyItemUM { override val icon: CurrencyIconState, val onRemoveClick: () -> Unit, ) : CurrencyItemUM() + + class Loading(val index: Int) : CurrencyItemUM() { + + override val id: ManagedCryptoCurrency.ID = ManagedCryptoCurrency.ID(value = "loading_$index") + override val name: String = "loading" + override val symbol: String = "loading" + override val icon: CurrencyIconState = CurrencyIconState.Loading + } } \ No newline at end of file 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 1cff9a28cb..d84cc432eb 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 @@ -29,20 +29,24 @@ 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 +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.core.ui.components.BottomFade -import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState 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.ChainRowContainer import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.components.snackbar.TangemSnackbarHost @@ -53,6 +57,7 @@ import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM @@ -183,14 +188,6 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) } - Crossfade(targetState = state.isInitialBatchLoading, label = "ManageTokensLoadingContent") { isVisible -> - if (isVisible) { - ProgressIndicator( - modifier = Modifier.fillMaxSize(), - ) - } - } - EventEffect(event = state.scrollToTop) { listState.animateScrollToItem(index = 0) } @@ -234,6 +231,11 @@ private fun Currencies( item = item, ) } + is CurrencyItemUM.Loading -> { + LoadingItem( + modifier = Modifier.fillMaxWidth(), + ) + } } } @@ -265,6 +267,30 @@ private fun ProgressIndicator(modifier: Modifier = Modifier) { } } +@Composable +private fun LoadingItem(modifier: Modifier = Modifier) { + ChainRowContainer( + modifier = modifier, + icon = { + CurrencyIcon(CurrencyIconState.Loading) + }, + text = { + TextShimmer( + modifier = Modifier.width(70.dp), + style = TangemTheme.typography.subtitle2, + ) + }, + action = { + RectangleShimmer( + modifier = Modifier.size( + width = 24.dp, + height = 16.dp, + ), + ) + }, + ) +} + @Composable private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = Modifier) { ChainRow( @@ -393,9 +419,19 @@ private fun NetworksList( @Preview(showBackground = true, widthDp = 360, heightDp = 800) @Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ManageTokens() { +private fun Preview_ManageTokens( + @PreviewParameter(PreviewManageTokensComponentProvider::class) component: ManageTokensComponent, +) { TangemThemePreview { - PreviewManageTokensComponent().Content(Modifier.fillMaxWidth()) + component.Content(Modifier.fillMaxWidth()) } } + +private class PreviewManageTokensComponentProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewManageTokensComponent(), + PreviewManageTokensComponent(isLoading = true), + ) +} // endregion Preview \ 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 index 557b49d960..a539a0524d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt @@ -15,7 +15,9 @@ internal fun CurrencyItemUM.toggleExpanded( if (currency !is ManagedCryptoCurrency.Token) return this return when (this) { - is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Custom, + is CurrencyItemUM.Loading, + -> this is CurrencyItemUM.Basic -> { val isExpanded = networks !is NetworksUM.Expanded @@ -35,7 +37,9 @@ internal fun CurrencyItemUM.toggleExpanded( internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyItemUM { return when (this) { - is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Custom, + is CurrencyItemUM.Loading, + -> this is CurrencyItemUM.Basic -> { if (currency !is ManagedCryptoCurrency.Token) { return this From 4869f4dda39be2dff97ce9e9b78954e4e7661df0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 16:11:38 +0400 Subject: [PATCH 112/197] Updated on 2026-08-14 --- .../managetokens/model/ManageTokensModel.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 030e583e14..5c4d924942 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 @@ -28,7 +28,7 @@ import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -91,7 +91,7 @@ internal class ManageTokensModel @Inject constructor( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, - items = persistentListOf(), + items = getLoadingItems(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_search_tokens), onBackButtonClick = router::pop, @@ -112,7 +112,7 @@ internal class ManageTokensModel @Inject constructor( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, - items = persistentListOf(), + items = getLoadingItems(), topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, @@ -175,9 +175,12 @@ internal class ManageTokensModel @Inject constructor( is PaginationStatus.InitialLoading, -> { if (state.search.isActive) { - state + state.copySealed( + items = getLoadingItems(), + ) } else { state.copySealed( + items = getLoadingItems(), isInitialBatchLoading = true, ) } @@ -224,6 +227,12 @@ internal class ManageTokensModel @Inject constructor( } } + private fun getLoadingItems(): ImmutableList { + return List(size = 10) { index -> + CurrencyItemUM.Loading(index) + }.toPersistentList() + } + private fun consumeScrollToTopEvent() { this.state.update { state -> state.copySealed( From 5c4f10f90e8e62a1afe9a54132fb223145695aaf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 17:59:02 +0500 Subject: [PATCH 113/197] Updated on 2026-08-14 --- .../staking/impl/presentation/state/events/StakingEvent.kt | 2 ++ .../impl/presentation/state/events/StakingEventFactory.kt | 5 +++++ .../staking/impl/presentation/ui/StakingEventEffect.kt | 5 +++++ .../staking/impl/presentation/viewmodel/StakingViewModel.kt | 4 +--- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt index 7e7700595e..a1b9cce6d9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt @@ -10,4 +10,6 @@ internal sealed class StakingEvent { data class ShowSnackBar(val text: TextReference) : StakingEvent() data class ShowAlert(val alert: AlertUM) : StakingEvent() + + data class ShowShareDialog(val txUrl: String) : 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 index 173975c66c..5c29cbbf5b 100644 --- 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 @@ -41,4 +41,9 @@ internal class StakingEventFactory( ) stateController.updateEvent(alert) } + + fun createShareDialog(txUrl: String) { + val event = StakingEvent.ShowShareDialog(txUrl) + stateController.updateEvent(event) + } } \ 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 index 80ba0d9029..b134e9d549 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 @@ -11,11 +11,13 @@ 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 +import com.tangem.core.ui.extensions.shareText 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 context = LocalContext.current val resources = LocalContext.current.resources var alertConfig by remember { mutableStateOf(value = null) } @@ -38,6 +40,9 @@ internal fun StakingEventEffect(event: StateEvent, snackbarHostSta is StakingEvent.ShowAlert -> { alertConfig = value.alert } + is StakingEvent.ShowShareDialog -> { + context.shareText(value.txUrl) + } } }, ) 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 dd305ad949..1f1afe7e27 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 @@ -15,7 +15,6 @@ import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -96,7 +95,6 @@ internal class StakingViewModel @Inject constructor( private val getAllowanceUseCase: GetAllowanceUseCase, private val getFeeUseCase: GetFeeUseCase, private val isApproveNeededUseCase: IsApproveNeededUseCase, - private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, private val feedbackManager: FeedbackManager, private val getCardInfoUseCase: GetCardInfoUseCase, @@ -643,7 +641,7 @@ internal class StakingViewModel @Inject constructor( if (txUrl != null) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = txUrl) + stakingEventFactory.createShareDialog(txUrl = txUrl) } // TODO staking [REDACTED_TASK_KEY] From 2cf60002a4c78f691212eb0aa88104bb983932d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 21:32:48 +0300 Subject: [PATCH 114/197] Updated on 2026-08-14 --- .../common/adapter/UnknownEnumMoshiAdapter.kt | 2 ++ .../models/response/model/YieldDTO.kt | 21 +++++++++++++++++-- data/staking/build.gradle.kts | 4 ++++ .../data/staking/converters/YieldConverter.kt | 21 ++++++++++++++++--- data/transaction/build.gradle.kts | 1 + .../domain/staking/model/stakekit/Yield.kt | 13 ++++++++++-- .../ConfirmationStatePreviewData.kt | 7 ++++--- .../previewdata/InitialStakingStatePreview.kt | 3 ++- 8 files changed, 61 insertions(+), 11 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index f5627c6f96..d594d91b51 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardClaimingDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.RewardTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO @@ -35,6 +36,7 @@ fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, + ValidatorStatusDTO::class.java to ValidatorStatusDTO.UNKNOWN, ) return apply { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index cff84d1ce7..614184aee4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -67,7 +67,7 @@ data class YieldDTO( @Json(name = "address") val address: String, @Json(name = "status") - val status: String, + val status: ValidatorStatusDTO, @Json(name = "name") val name: String, @Json(name = "image") @@ -84,7 +84,24 @@ data class YieldDTO( val votingPower: Double?, @Json(name = "preferred") val preferred: Boolean, - ) + ) { + @JsonClass(generateAdapter = true) + enum class ValidatorStatusDTO { + @Json(name = "active") + ACTIVE, + + @Json(name = "deactivating") + DEACTIVATING, + + @Json(name = "inactive") + INACTIVE, + + @Json(name = "jailed") + JAILED, + + UNKNOWN, + } + } @JsonClass(generateAdapter = true) data class MetadataDTO( diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 6bcc1ccd0b..4a9ba3d819 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -30,14 +30,18 @@ dependencies { implementation(projects.features.staking.api) // region DI + implementation(deps.hilt.android) kapt(deps.hilt.kapt) + // endregion // region Others dependencies + implementation(deps.androidx.datastore) implementation(deps.jodatime) implementation(deps.kotlin.coroutines) + implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index e383d63f49..c08359c098 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -3,10 +3,13 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule +import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList class YieldConverter( private val tokenConverter: TokenConverter, @@ -24,9 +27,11 @@ class YieldConverter( rewardType = convertRewardType(value.rewardType), metadata = convertMetadata(value.metadata), validators = value.validators - .filter { it.preferred } + .asSequence() + .filter { it.preferred && it.status == ValidatorStatusDTO.ACTIVE } + .sortedByDescending { it.apr } .map { convertValidator(it) } - .sortedByDescending { it.apr }, + .toImmutableList(), isAvailable = value.isAvailable, ) } @@ -109,7 +114,7 @@ class YieldConverter( private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator { return Yield.Validator( address = validatorDTO.address, - status = validatorDTO.status, + status = convertValidatorStatus(validatorDTO.status), name = validatorDTO.name, image = validatorDTO.image, website = validatorDTO.website, @@ -129,6 +134,16 @@ class YieldConverter( } } + private fun convertValidatorStatus(validatorStatusDTO: ValidatorStatusDTO): ValidatorStatus { + return when (validatorStatusDTO) { + ValidatorStatusDTO.ACTIVE -> ValidatorStatus.ACTIVE + ValidatorStatusDTO.DEACTIVATING -> ValidatorStatus.DEACTIVATING + ValidatorStatusDTO.INACTIVE -> ValidatorStatus.INACTIVE + ValidatorStatusDTO.JAILED -> ValidatorStatus.JAILED + else -> ValidatorStatus.UNKNOWN + } + } + private fun convertRewardSchedule(rewardTypeDTO: RewardScheduleDTO): RewardSchedule { return when (rewardTypeDTO) { RewardScheduleDTO.BLOCK -> RewardSchedule.BLOCK diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index 41baa4a4ba..709f743912 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -32,5 +32,6 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) + /** Other */ implementation(deps.timber) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index 036f187b28..4be9bad438 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -53,7 +53,7 @@ data class Yield( @Serializable data class Validator( val address: String, - val status: String, + val status: ValidatorStatus, val name: String, val image: String? = null, val website: String? = null, @@ -62,7 +62,16 @@ data class Yield( val stakedBalance: String? = null, val votingPower: Double? = null, val preferred: Boolean, - ) + ) { + + enum class ValidatorStatus { + ACTIVE, + DEACTIVATING, + INACTIVE, + JAILED, + UNKNOWN, + } + } @Serializable data class Metadata( 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 09143b30de..5e63fa4e32 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 @@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.stringReference 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.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import kotlinx.collections.immutable.persistentListOf @@ -18,7 +19,7 @@ internal object ConfirmationStatePreviewData { private val validatorList = listOf( Yield.Validator( address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951", - status = "active", + status = ValidatorStatus.ACTIVE, name = "Luganodes", image = "https://assets.stakek.it/validators/luganodes.png", apr = BigDecimal("0.054823398040640445"), @@ -30,7 +31,7 @@ internal object ConfirmationStatePreviewData { ), Yield.Validator( address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32", - status = "active", + status = ValidatorStatus.ACTIVE, name = "InfStones", image = "https://assets.stakek.it/validators/infstones.png", apr = BigDecimal("0.057786472172836965"), @@ -42,7 +43,7 @@ internal object ConfirmationStatePreviewData { ), Yield.Validator( address = "0xd14a87025109013b0a2354a775cb335f926af65a", - status = "active", + status = ValidatorStatus.ACTIVE, name = "Kiln", image = "https://assets.stakek.it/validators/kiln.png", apr = BigDecimal("0.057786472172836965"), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index b6a1eabe64..a6ac4963e9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -77,7 +78,7 @@ internal object InitialStakingStatePreview { rawCurrencyId = null, validator = Yield.Validator( address = "address", - status = "status", + status = ValidatorStatus.ACTIVE, name = "Binance", image = null, website = null, From a7f5b54078b35bf92a4e034587f89cc91859becf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Sep 2024 19:47:22 +0500 Subject: [PATCH 115/197] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 7 - core/res/src/main/res/values-fr/strings.xml | 7 - core/res/src/main/res/values-ja/strings.xml | 12 +- core/res/src/main/res/values-ru/strings.xml | 9 - .../src/main/res/values-uk-rUA/strings.xml | 3 - core/res/src/main/res/values/strings.xml | 13 +- .../components/inputrow/InputRowImageBase.kt | 23 ++- .../components/inputrow/InputRowImageInfo.kt | 9 +- .../main/res/drawable/ic_connection_18.xml | 21 +++ .../converters/YieldBalanceConverter.kt | 1 + .../staking/model/stakekit/YieldBalance.kt | 26 +-- .../state/InnerYieldBalanceState.kt | 20 +-- .../state/converters/BalanceItemConverter.kt | 125 ++++++++++++++ .../RewardsValidatorStateConverter.kt | 31 ++-- .../converters/YieldBalancesConverter.kt | 136 +-------------- .../previewdata/InitialStakingStatePreview.kt | 56 +++---- .../ui/StakingInitialInfoContent.kt | 156 +++++++----------- 17 files changed, 319 insertions(+), 336 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_connection_18.xml create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 2f740dd9d7..a50e97f246 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -115,10 +115,6 @@ Zum Anbieter gehen Zum Token Importieren - - in %d Tag - in %d Tagen - Später Gesperrt Hauptnetz @@ -641,8 +637,6 @@ Entferne diese Wallet Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name - Aktiv - Um deine Vermögenswerte freizugeben, tippe auf den Block oben Die Anzahl der zu stakenden Krypros muss mindesten %s betragen Nicht gestakte beanspruche Jährliche prozentuale Rendite @@ -702,7 +696,6 @@ Lösen der Bindungen Gelocktes unlocken Unstaken - Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen Staking beenden Validator/ Prüfer Validatoren diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 87595dbdf6..dd3a253e57 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -115,10 +115,6 @@ Aller au fournisseur Aller au jeton Importez - - dans %d jour - dans %d jours - Plus tard Verrouillé Réseau principal @@ -637,8 +633,6 @@ Oublier le portefeuille Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Nom - Actif - Afin d\'unstaker vos actifs, appuyez sur le bloc ci-dessus Le montant à staker doit être au moins %s Réclamation déstakée Pourcentage de rendement annuel @@ -697,7 +691,6 @@ Dissociation Débloquer Non-staké - Vérifiez non-stakés pour réclamer vos actifs Unstaking Validateur Validateurs diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 33f3202506..5e94e5d5f8 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -83,6 +83,7 @@ カメラへのアクセスを許可していません。プライバシー設定を調整してください。 キャンセル アクションを選択 + 請求 報酬を受け取る 閉じる 続ける @@ -113,9 +114,6 @@ プロバイダーへ移動 トークンへ移動 インポート - - %d日で - 後で ロックされています メインネットワーク @@ -628,8 +626,6 @@ ウォレット削除 これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 名前 - アクティブ - 資産のステーキングを解除するには、上のブロックをタップしてください ステーキング金額は %s 以上である必要があります ステーキング解除分を請求する 年率 @@ -654,14 +650,17 @@ ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。 ウォームアップ期間 ステーキングへの参加を有効にするために割り当てられた時間。 + ステーキング機能を使用すると、プロバイダーの%1$sと%2$sに同意したことになります ロック中 移行 ネイティブステーキング + 獲得した報酬はあなたのアドレスに直接送られます。受け取り後、資金は使用可能になります。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎時間受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 + 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 ステーキング解除プロセスの開始後、報酬の獲得はすぐに停止します。ステーキング解除には%sかかります。 あなたの資金はステーキング解除されました。獲得した報酬とともに引き出すことができます。 準備中 @@ -684,13 +683,14 @@ 報酬 ステーキングはロックされています もっとステーキングする + %1$sをステーキングし、年間 ~ %2$sを受け取ります %sをステーキングする %sのステーキング解除 ステーキング解約中 ステーキング解除はロックされています ステーキングされていない - 資産を請求するために、unstakedを確認してください ステーキング解除 + ステーキング解除中の資産%s バリデーター バリデーター 投票する diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 4d2fc19ca5..9011127a90 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -117,12 +117,6 @@ К провайдеру Перейти в токен Импортировать - - через %d день - через %d дня - через %d дней - через %d дней - Позже Заблокирован Основная сеть @@ -645,8 +639,6 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя - Активно - Для завершения стейкинга нажмите на блок выше Сумма для стейкинга должна быть не менее %s Забрать средства Процентная ставка @@ -701,7 +693,6 @@ Отзыв Разблокировать Вывод из стейкинга - Проверьте процесс завершения стейкинга, чтобы вывести свои средства. Завершение стейкинга Завершение стейкинга %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 ca0bc61e34..96f929750f 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -642,8 +642,6 @@ Забути гаманець Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. Ім\'я - Активний - Для завершення стейкінгу натисніть на блок вище Сума для стейкінгу має бути не менше %s Зняти кошти Процентна ставка @@ -696,7 +694,6 @@ Зняти зі стейкінгу %s Розблокувати Вивід зі стейкінгу - Перевірити незастейкані, щоб отримати свої активи Зняти зі стейкінгу Валідатор Проголосувати diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b89491b528..6c64f890e2 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -116,11 +116,8 @@ Go to provider Go to token Import - - in %d day - in %d days - Later + %1$s left Locked Main network Network fee @@ -414,6 +411,8 @@ The maximum number of coins or tokens that can ever exist for a particular cryptocurrency Trading volume (24h) The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market + Pull this up or tap the search bar to add tokens directly from the market + Add tokens You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later @@ -638,8 +637,6 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name - Active - To unstake your assets, tap the block above The amount to stake must be at least %s Claim unstaked Annual percentage rate @@ -698,12 +695,13 @@ Stake locked Stake more You stake %1$s and will be receiving ~%2$s yearly + Tap to unlock + Tap to withdraw Stake %s Unstake %s Unbonding Unlock locked Unstaked - Check unstaked to claim your assets Unstaking Unstaking assets %s Validator @@ -711,6 +709,7 @@ Vote Vote locked Withdraw + Your stakes Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index fd4913770c..0991dcdf5a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -1,7 +1,9 @@ package com.tangem.core.ui.components.inputrow import android.content.res.Configuration +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -42,11 +44,13 @@ import com.tangem.core.ui.res.TangemThemePreview @Composable internal fun InputRowImageBase( subtitle: TextReference, - imageUrl: String?, modifier: Modifier = Modifier, caption: TextReference? = null, + imageUrl: String? = null, + @DrawableRes iconRes: Int? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, + iconTint: Color = TangemTheme.colors.icon.informative, isGrayscaleImage: Boolean = false, iconEndRes: Int? = null, extraContent: (@Composable RowScope.() -> Unit)? = null, @@ -65,6 +69,23 @@ internal fun InputRowImageBase( ) SpacerW12() } + if (iconRes != null) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(TangemTheme.dimens.spacing36) + .clip(TangemTheme.shapes.roundedCornersXLarge) + .background(iconTint.copy(alpha = 0.08f)), + ) { + Icon( + painter = rememberVectorPainter(image = ImageVector.vectorResource(id = iconRes)), + tint = iconTint, + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size18), + ) + } + SpacerW12() + } Column { Text( text = subtitle.resolveReference(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index 16ea439603..19bb005a00 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.inputrow import android.content.res.Configuration +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -40,15 +41,17 @@ import com.tangem.core.ui.res.TangemThemePreview fun InputRowImageInfo( subtitle: TextReference, infoTitle: TextReference, - imageUrl: String?, modifier: Modifier = Modifier, title: TextReference? = null, caption: TextReference? = null, infoSubtitle: TextReference? = null, + imageUrl: String? = null, + @DrawableRes iconRes: Int? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, + iconTint: Color = TangemTheme.colors.icon.informative, isGrayscaleImage: Boolean = false, - iconEndRes: Int? = null, + @DrawableRes iconEndRes: Int? = null, ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), @@ -66,6 +69,8 @@ fun InputRowImageInfo( subtitle = subtitle, caption = caption, imageUrl = imageUrl, + iconRes = iconRes, + iconTint = iconTint, subtitleColor = subtitleColor, captionColor = captionColor, isGrayscaleImage = isGrayscaleImage, diff --git a/core/ui/src/main/res/drawable/ic_connection_18.xml b/core/ui/src/main/res/drawable/ic_connection_18.xml new file mode 100644 index 0000000000..80d76d5158 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_connection_18.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + 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 24eae9474a..70577561ed 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 @@ -18,6 +18,7 @@ internal class YieldBalanceConverter : Converter BalanceItem( + id = item.groupId, type = BalanceType.valueOf(item.type.name), amount = item.amount, pricePerShare = item.pricePerShare, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 60cfb91e45..c789da63b3 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -34,6 +34,7 @@ data class YieldBalanceItem( ) data class BalanceItem( + val id: String, val type: BalanceType, val amount: BigDecimal, val pricePerShare: BigDecimal, @@ -76,16 +77,21 @@ data class PendingAction( } } -enum class BalanceType { - AVAILABLE, - STAKED, - PREPARING, - UNSTAKING, - UNLOCKING, - UNSTAKED, - LOCKED, - REWARDS, - UNKNOWN, +/** + * IMPORTANT!!! + * Order is used to sort balances. + */ +@Suppress("MagicNumber") +enum class BalanceType(val order: Int) { + AVAILABLE(1), + STAKED(2), + PREPARING(3), + LOCKED(4), + UNSTAKING(5), + UNLOCKING(6), + UNSTAKED(7), + REWARDS(8), + UNKNOWN(9), ; companion object { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 41fa1c3862..c989de7fb1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -9,37 +9,29 @@ import com.tangem.domain.staking.model.stakekit.Yield import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal -sealed class InnerYieldBalanceState { +internal sealed class InnerYieldBalanceState { data class Data( val rewardsCrypto: String, val rewardsFiat: String, val rewardBlockType: RewardBlockType, - val balance: ImmutableList, + val balance: ImmutableList, ) : InnerYieldBalanceState() data object Empty : InnerYieldBalanceState() } -// TODO staking get rid of unstable types @Immutable -data class BalanceGroupedState( - val items: ImmutableList, - val footer: TextReference?, +internal data class BalanceState( + val id: String, val title: TextReference, val type: BalanceType, + val subtitle: TextReference?, val isClickable: Boolean, -) - -@Immutable -data class BalanceState( - val validator: Yield.Validator?, - val title: TextReference, val cryptoValue: String, val cryptoDecimal: BigDecimal, val cryptoAmount: TextReference, val fiatAmount: TextReference, val rawCurrencyId: String?, - val unbondingPeriod: TextReference, - val warmupPeriod: TextReference, + val validator: Yield.Validator?, val pendingActions: ImmutableList, ) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt new file mode 100644 index 0000000000..c6317bf05d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -0,0 +1,125 @@ +package com.tangem.features.staking.impl.presentation.state.converters + +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 +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList +import org.joda.time.DateTime +import java.util.Calendar + +internal class BalanceItemConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val yield: Yield, +) : Converter { + override fun convert(value: BalanceItem): BalanceState? { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + val validator = yield.validators.firstOrNull { + value.validatorAddress?.contains(it.address, ignoreCase = true) == true + } + + val cryptoAmount = value.amount + val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) + + val title = value.type.getTitle(validator?.name) + return title?.let { + BalanceState( + id = value.id, + validator = validator, + title = title, + subtitle = getSubtitle(value), + type = value.type, + cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), + cryptoDecimal = cryptoAmount, + cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + fiatAmount = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + rawCurrencyId = value.rawCurrencyId, + pendingActions = value.pendingActions.toPersistentList(), + isClickable = value.type.isClickable(), + ) + } + } + + private fun BalanceType.getTitle(validatorName: String?) = when (this) { + BalanceType.STAKED -> validatorName?.let { stringReference(it) } + BalanceType.PREPARING -> resourceReference(R.string.staking_preparing) + BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) + BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) + BalanceType.LOCKED -> resourceReference(R.string.staking_locked) + BalanceType.AVAILABLE, + BalanceType.REWARDS, + BalanceType.UNLOCKING, + BalanceType.UNKNOWN, + -> null + } + + private fun getSubtitle(balance: BalanceItem) = when (balance.type) { + BalanceType.UNSTAKING -> getUnbondingDate(balance.date) + BalanceType.UNSTAKED -> resourceReference(R.string.staking_tap_to_withdraw) + BalanceType.LOCKED -> resourceReference(R.string.staking_tap_to_unlock) + BalanceType.AVAILABLE, + BalanceType.STAKED, + BalanceType.PREPARING, + BalanceType.UNLOCKING, + BalanceType.REWARDS, + BalanceType.UNKNOWN, + -> null + } + + 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) { + resourceReference( + R.string.common_left, + wrappedList( + pluralReference(R.plurals.common_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/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 987866f39b..e3cc2f8e07 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -1,19 +1,18 @@ package com.tangem.features.staking.impl.presentation.state.converters -import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -49,23 +48,21 @@ internal class RewardsValidatorStateConverter( val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) validator?.toBalanceState( + balance = balance, cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoValue = cryptoValue, fiatValue = fiatValue, - pendingActions = balance.pendingActions.toPersistentList(), ) } private fun Yield.Validator.toBalanceState( + balance: BalanceItem, cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoValue: BigDecimal, fiatValue: BigDecimal?, - pendingActions: ImmutableList, ): BalanceState { val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency - val unbondingPeriod = yield.metadata.cooldownPeriod.days - val warmupPeriod = yield.metadata.warmupPeriod.days val cryptoAmount = stringReference( BigDecimalFormatter.formatCryptoAmount( cryptoAmount = cryptoValue, @@ -81,24 +78,18 @@ internal class RewardsValidatorStateConverter( ) return BalanceState( + id = balance.id, validator = this, title = stringReference(this.name), + subtitle = null, cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), cryptoDecimal = cryptoValue, cryptoAmount = cryptoAmount, fiatAmount = fiatAmount, rawCurrencyId = cryptoCurrency.id.rawCurrencyId, - unbondingPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(unbondingPeriod), - ), - warmupPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(warmupPeriod), - ), - pendingActions = pendingActions, + pendingActions = balance.pendingActions.toPersistentList(), + isClickable = true, + type = balance.type, ) } } \ No newline at end of file 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 856bec78cc..d948580b77 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,29 +1,26 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.common.extensions.isZero -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 import com.tangem.domain.staking.model.stakekit.* -import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState -import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList -import org.joda.time.DateTime -import java.util.Calendar internal class YieldBalancesConverter( private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, private val yield: Yield, ) : Converter { + + private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) { + BalanceItemConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + } + override fun convert(value: Unit): InnerYieldBalanceState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val appCurrency = appCurrencyProvider() @@ -34,7 +31,6 @@ internal class YieldBalancesConverter( return if (yieldBalance is YieldBalance.Data) { val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) - val groupedBalances = getGroupedBalance(yieldBalance.balance) InnerYieldBalanceState.Data( rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( @@ -47,121 +43,18 @@ internal class YieldBalancesConverter( fiatCurrencySymbol = appCurrency.symbol, ), rewardBlockType = getRewardBlockType(), - balance = groupedBalances, + balance = yieldBalance.balance.items.mapBalances(), ) } else { InnerYieldBalanceState.Empty } } - private fun getGroupedBalance(balance: YieldBalanceItem) = balance.items - .sortedBy { it.type } - .groupBy { it.type.toGroup() } - .mapNotNull { item -> - val (title, footer) = getGroupTitle(item.key) - val isClickable = item.key.isClickable() - title?.let { - BalanceGroupedState( - items = item.value.mapBalances(item.key).toPersistentList(), - footer = footer, - title = it, - type = item.key, - isClickable = isClickable, - ) - } - } - .filterNot { it.items.isEmpty() } + private fun List.mapBalances() = sortedBy { it.type.order } + .filterNot { it.amount.isZero() || it.type == BalanceType.REWARDS } + .mapNotNull(balanceItemConverter::convert) .toPersistentList() - private fun List.mapBalances(balanceType: BalanceType): List { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val appCurrency = appCurrencyProvider() - val cryptoCurrency = cryptoCurrencyStatus.currency - var incrementingIndex = 1 - return this - .filterNot { it.amount.isZero() } - .mapNotNull { balance -> - val validator = yield.validators.firstOrNull { - balance.validatorAddress?.contains(it.address, ignoreCase = true) == true - } - - val cryptoAmount = balance.amount - val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - val unbonding = getUnbondingDate(balance.date) - val warmupPeriod = yield.metadata.warmupPeriod.days - val title = when { - validator != null -> stringReference(validator.name) - balanceType == BalanceType.UNSTAKING -> { - resourceReference(R.string.staking_unstaking_item_name, wrappedList(incrementingIndex++)) - } - else -> null - } - title?.let { - BalanceState( - validator = validator, - title = title, - cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), - cryptoDecimal = cryptoAmount, - cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoAmount, - cryptoCurrency = cryptoCurrency, - ), - ), - fiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - ), - rawCurrencyId = balance.rawCurrencyId, - unbondingPeriod = unbonding, - warmupPeriod = pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)), - pendingActions = balance.pendingActions.toPersistentList(), - ) - } - } - } - - private fun BalanceType.toGroup() = when (this) { - BalanceType.REWARDS, - BalanceType.UNKNOWN, - -> BalanceType.UNKNOWN - else -> this - } - - private fun getGroupTitle(type: BalanceType) = when (type) { - BalanceType.STAKED -> resourceReference(R.string.staking_active) to - resourceReference(R.string.staking_active_footer) - BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) to - resourceReference(R.string.staking_unstaked_footer) - BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null - BalanceType.LOCKED -> resourceReference(R.string.staking_locked) to null - BalanceType.AVAILABLE -> null to null - BalanceType.PREPARING -> resourceReference(R.string.staking_preparing) to null - BalanceType.REWARDS -> null to null - BalanceType.UNLOCKING -> null to null - BalanceType.UNKNOWN -> null to null - } - - 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 getRewardBlockType(): RewardBlockType { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data @@ -178,15 +71,4 @@ internal class YieldBalancesConverter( else -> RewardBlockType.NoRewards } } - - 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/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index a6ac4963e9..c23cb74fda 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -6,9 +6,9 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import kotlinx.collections.immutable.persistentListOf @@ -63,36 +63,30 @@ internal object InitialStakingStatePreview { rewardsCrypto = "100 SOL", rewardBlockType = RewardBlockType.RewardUnavailable, balance = persistentListOf( - BalanceGroupedState( - title = stringReference("Staked"), - footer = null, - type = BalanceType.STAKED, - isClickable = true, - items = persistentListOf( - BalanceState( - title = stringReference("Binance"), - cryptoValue = "100", - cryptoAmount = stringReference("100 SOL"), - cryptoDecimal = "100".toBigDecimal(), - fiatAmount = stringReference("100 $"), - rawCurrencyId = null, - validator = Yield.Validator( - address = "address", - status = ValidatorStatus.ACTIVE, - name = "Binance", - image = null, - website = null, - apr = "5".toBigDecimal(), - commission = null, - stakedBalance = null, - votingPower = null, - preferred = false, - ), - unbondingPeriod = stringReference("3 days"), - pendingActions = persistentListOf(), - warmupPeriod = stringReference("3 days"), - ), + BalanceState( + id = "id", + title = stringReference("Binance"), + cryptoValue = "100", + cryptoAmount = stringReference("100 SOL"), + cryptoDecimal = "100".toBigDecimal(), + fiatAmount = stringReference("100 $"), + rawCurrencyId = null, + validator = Yield.Validator( + address = "address", + status = Yield.Validator.ValidatorStatus.ACTIVE, + name = "Binance", + image = null, + website = null, + apr = "5".toBigDecimal(), + commission = null, + stakedBalance = null, + votingPower = null, + preferred = false, ), + pendingActions = persistentListOf(), + isClickable = true, + type = BalanceType.STAKED, + subtitle = null, ), ), ), 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 5e2f5c710b..530716b3fe 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 @@ -32,7 +32,6 @@ import androidx.compose.ui.unit.Density import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.list.roundedListWithDividersItems @@ -44,7 +43,6 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -198,53 +196,43 @@ private fun StakingRewardBlock( } @Composable -private fun ActiveStakingBlock(groups: ImmutableList, onClick: (BalanceState) -> Unit) { +private fun ActiveStakingBlock(balances: ImmutableList, onClick: (BalanceState) -> Unit) { Column( - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), ) { - groups.forEach { group -> - key(group.title) { - FooterContainer( - footer = group.footer?.resolveReference(), - modifier = Modifier, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action), - ) { - Text( - text = group.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding( - top = TangemTheme.dimens.spacing12, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - ) - group.items.forEachIndexed { index, balance -> - key(balance.title.resolveReference() + index) { - InputRowImageInfo( - subtitle = balance.title, - caption = getCaption(group.type, balance), - isGrayscaleImage = !group.isClickable, - infoTitle = balance.fiatAmount, - infoSubtitle = balance.cryptoAmount, - imageUrl = balance.validator?.image, - iconEndRes = R.drawable.ic_chevron_right_24.takeIf { group.isClickable }, - modifier = Modifier.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), - enabled = group.isClickable, - onClick = { onClick(balance) }, - ), - ) - } - } - } - } + Text( + text = stringResource(id = R.string.staking_your_stakes), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing4, + ), + ) + balances.forEach { balance -> + key(balance.id) { + val (icon, iconTint) = balance.type.getIcon() + InputRowImageInfo( + subtitle = balance.title, + caption = balance.subtitle ?: balance.getAprText(), + isGrayscaleImage = !balance.isClickable, + infoTitle = balance.fiatAmount, + infoSubtitle = balance.cryptoAmount, + imageUrl = balance.getImage(), + iconRes = icon, + iconTint = iconTint, + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + enabled = balance.isClickable, + onClick = { onClick(balance) }, + ), + ) } } } @@ -266,51 +254,35 @@ private fun StakeButtonBlock(buttonState: NavigationButtonsState) { } @Composable -private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextReference { - 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) - } - BalanceType.PREPARING -> { - combinedReference( - resourceReference(R.string.staking_details_warmup_period), - annotatedReference { - appendSpace() - appendColored( - text = balance.warmupPeriod.resolveReference(), - color = TangemTheme.colors.text.accent, - ) - }, - ) - } - else -> { - combinedReference( - resourceReference(R.string.staking_details_apr), - annotatedReference { - appendSpace() - appendColored( - text = BigDecimalFormatter.formatPercent( - percent = balance.validator?.apr.orZero(), - useAbsoluteValue = true, - ), - color = TangemTheme.colors.text.accent, - ) - }, - ) - } - } +private fun BalanceState.getAprText() = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent( + percent = validator?.apr.orZero(), + useAbsoluteValue = true, + ), + color = TangemTheme.colors.text.accent, + ) + }, +) + +@Composable +private fun BalanceType.getIcon() = when (this) { + BalanceType.UNSTAKING -> R.drawable.ic_connection_18 to TangemTheme.colors.icon.accent + BalanceType.UNSTAKED -> R.drawable.ic_connection_18 to TangemTheme.colors.icon.informative + BalanceType.LOCKED -> R.drawable.ic_lock_24 to TangemTheme.colors.icon.informative + else -> null to TangemTheme.colors.icon.informative +} + +@Composable +private fun BalanceState.getImage() = when (type) { + BalanceType.UNSTAKING, + BalanceType.UNSTAKED, + BalanceType.LOCKED, + -> null + else -> validator?.image } private val textGradientColors = listOf( From d82f8f61d4fe6a752d174d9b771a716edf8ce06d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 11:34:11 +0300 Subject: [PATCH 116/197] Updated on 2026-08-14 --- .../api/common/adapter/UnknownEnumMoshiAdapter.kt | 6 ++++++ .../models/response/model/error/StakeKitErrorResponse.kt | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index d594d91b51..2cc525ffd0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -11,6 +11,8 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.RewardT import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.error.AccessDeniedErrorTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorMessageDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO @@ -27,6 +29,7 @@ object UnknownEnumMoshiAdapter { fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { val map = mapOf( + // valid response enums BalanceTypeDTO::class.java to BalanceTypeDTO.UNKNOWN, NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN, RewardClaimingDTO::class.java to RewardClaimingDTO.UNKNOWN, @@ -37,6 +40,9 @@ fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, ValidatorStatusDTO::class.java to ValidatorStatusDTO.UNKNOWN, + // error enums + AccessDeniedErrorTypeDTO::class.java to AccessDeniedErrorTypeDTO.UNKNOWN, + StakeKitErrorMessageDTO::class.java to StakeKitErrorMessageDTO.UNKNOWN, ) return apply { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt index 9ad42062ca..fdb3436051 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt @@ -35,6 +35,8 @@ data class StakeKitErrorDetailsDTO( enum class AccessDeniedErrorTypeDTO { @Json(name = "GEO_LOCATION") GEO_LOCATION, + + UNKNOWN, } enum class StakeKitErrorMessageDTO { @@ -157,4 +159,6 @@ enum class StakeKitErrorMessageDTO { @Json(name = "GRTStakingDisabledLedgerLiveError") GRT_STAKING_DISABLED_LEDGER_LIVE_ERROR, + + UNKNOWN, } \ No newline at end of file From afdb81adac284b63b03e63cfcd57576f79885309 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 15:57:04 +0500 Subject: [PATCH 117/197] Updated on 2026-08-14 --- .../state/transformers/AddStakingNotificationsTransformer.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index f7738e94b5..3635796fe7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -222,6 +222,9 @@ internal class AddStakingNotificationsTransformer( StakingActionType.RESTAKE_REWARDS -> { R.string.staking_restake to R.string.staking_notification_restake_rewards_text } + StakingActionType.WITHDRAW -> { + R.string.staking_withdraw to R.string.staking_notification_withdraw_text + } else -> null to null } From 1bed77ffb1bb9ca3271c49337669b389a5493e02 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 15:57:31 +0500 Subject: [PATCH 118/197] Updated on 2026-08-14 --- .../com/tangem/common/ui/tokens/TokenItemStateConverter.kt | 2 +- .../com/tangem/domain/staking/model/stakekit/YieldBalance.kt | 4 ++++ .../tokens/operations/TokenListFiatBalanceOperations.kt | 5 +++-- .../state/factory/TokenDetailsLoadedBalanceConverter.kt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 3b3d14103d..1bae6dabaf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -91,7 +91,7 @@ class TokenItemStateConverter( } private fun CryptoCurrencyStatus.getStakedBalance() = - (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + (value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero() private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState(): TokenItemState.Unreachable { return TokenItemState.Unreachable( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index c789da63b3..c932b9f1d9 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -10,6 +10,10 @@ sealed class YieldBalance { val balance: YieldBalanceItem, val address: String, ) : YieldBalance() { + fun getTotalWithRewardsStakingBalance(): BigDecimal { + return balance.items.sumOf { it.amount } + } + fun getTotalStakingBalance(): BigDecimal { return balance.items .filterNot { it.type == BalanceType.REWARDS } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index e446ca55aa..de711b188f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -58,7 +58,8 @@ internal class TokenListFiatBalanceOperations( currentBalance: TotalFiatBalance, ): TotalFiatBalance { return with(currentBalance) { - val stakingBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val yieldBalance = status.yieldBalance as? YieldBalance.Data + val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance().orZero() val fiatStakingBalance = status.fiatRate.times(stakingBalance) (this as? TotalFiatBalance.Loaded)?.copy( @@ -76,7 +77,7 @@ internal class TokenListFiatBalanceOperations( ): TotalFiatBalance { return with(currentBalance) { val isTokenAmountCanBeSummarized = status.fiatAmount != null - val yieldBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val yieldBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero() val fiatYieldBalance = status.fiatRate?.times(yieldBalance).orZero() (this as? TotalFiatBalance.Loaded)?.copy( amount = this.amount + status.fiatAmount.orZero() + fiatYieldBalance, 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 594a80eeb7..2419e894d1 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 @@ -94,7 +94,7 @@ internal class TokenDetailsLoadedBalanceConverter( currentState: TokenDetailsBalanceBlockState, status: CryptoCurrencyStatus, ): TokenDetailsBalanceBlockState { - val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance() + val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance() val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } val isBalanceSelectorEnabled = stakingFeatureToggles.isStakingEnabled && !stakingCryptoAmount.isNullOrZero() return when (status.value) { From b4c656b3050dac15b83933a6f6fcbbaf4d81db1d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 15:57:48 +0500 Subject: [PATCH 119/197] Updated on 2026-08-14 --- .../AddStakingNotificationsTransformer.kt | 19 +++++++++++------- .../viewmodel/StakingViewModel.kt | 20 +++++++++++++++++-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 3635796fe7..f6c6eaac4c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -67,13 +67,18 @@ internal class AddStakingNotificationsTransformer( isSubtractAvailable = isSubtractAvailable, reduceAmountBy = reduceAmountBy, ) - val sendingAmount = checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable = isSubtractAvailable, - cryptoCurrencyStatus = cryptoCurrencyStatus, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = reduceAmountBy, - ) + val sendingAmount = if (isEnterAction) { + checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + } else { + // No amount is taken from account balance on exit or pending actions + BigDecimal.ZERO + } val notifications = buildList { // errors 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 1f1afe7e27..43da4818a4 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 @@ -56,9 +56,11 @@ import com.tangem.features.staking.impl.presentation.state.transformers.approval import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer +import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem +import com.tangem.utils.extensions.orZero import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -236,7 +238,7 @@ internal class StakingViewModel @Inject constructor( val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: error("No validator provided") 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") @@ -248,7 +250,7 @@ internal class StakingViewModel @Inject constructor( params = ActionParams( actionCommonType = value.actionType, integrationId = yield.id, - amount = amountValue, + amount = getAmount(amountState, fee, confirmationState.reduceAmountBy), address = defaultAddress, validatorAddress = validatorState.chosenValidator.address, token = yield.token, @@ -893,6 +895,20 @@ internal class StakingViewModel @Inject constructor( .getOrElse { false } } + private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal { + val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") + val feeValue = fee.amount.value ?: error("No fee value") + val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER + + return checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy.orZero(), + ) + } + private data class FullTransactionData( val stakeKitTransaction: StakingTransaction, val tangemTransaction: TransactionData.Compiled, From 0fff8339d4fe71ff592909e0649d8d3ac3fc5989 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 14:15:18 +0300 Subject: [PATCH 120/197] Updated on 2026-08-14 --- .../model/AddToPortfolioBSContentUMFactory.kt | 3 +- .../impl/model/MarketsPortfolioModel.kt | 26 ++---- .../impl/model/MyPortfolioUMFactory.kt | 10 ++- .../impl/model/PortfolioTokenUMConverter.kt | 1 + .../impl/model/TokenActionsHandler.kt | 14 +++- .../impl/model/TokensPortfolioUMConverter.kt | 83 +++++++++++++++++-- .../preview/PreviewMyPortfolioUMProvider.kt | 2 + .../impl/ui/state/PortfolioTokenUM.kt | 2 + 8 files changed, 106 insertions(+), 35 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 9e95fe3af0..761d567f41 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -86,7 +86,8 @@ internal class AddToPortfolioBSContentUMFactory( portfolioData = portfolioData, selectedWalletId = selectedWallet.walletId, ), - isWalletBlockVisible = portfolioData.walletsWithBalance.size > 1, + isWalletBlockVisible = portfolioData.walletsWithCurrencies + .filterKeys(UserWallet::isMultiCurrency).size > 1, ), ) } 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 6c6f15a0dd..03170e971d 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 @@ -10,7 +10,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase @@ -19,7 +18,6 @@ import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -62,7 +60,6 @@ internal class MarketsPortfolioModel @Inject constructor( private val factory = MyPortfolioUMFactory( onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) }, - onTokenItemClick = ::onTokenItemClick, addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( token = params.token, onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, @@ -71,6 +68,7 @@ internal class MarketsPortfolioModel @Inject constructor( onWalletSelect = ::onWalletSelect, onContinueClick = ::onContinueClick, ), + currentState = Provider { _state.value }, tokenActionsHandler = tokenActionsIntentsFactory.create( currentAppCurrency = Provider { currentAppCurrency.value }, updateTokenActionsBSConfig = { updateBlock -> @@ -80,6 +78,11 @@ internal class MarketsPortfolioModel @Inject constructor( updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } }, ), + updateTokens = { updateBlock -> + updateTokensState { state -> + state.copy(tokens = updateBlock(state.tokens)) + } + }, ) init { @@ -150,23 +153,6 @@ internal class MarketsPortfolioModel @Inject constructor( } } - private fun onTokenItemClick(index: Int, id: CryptoCurrency.ID) { - updateTokensState { state -> - state.copy( - tokens = state.tokens - .mapIndexed { i, tokenUM -> - // check by index and id because id can be the same - if (index == i && id.value == tokenUM.tokenItemState.id) { - tokenUM.copy(isQuickActionsShown = !tokenUM.isQuickActionsShown) - } else { - tokenUM.copy(isQuickActionsShown = false) - } - } - .toImmutableList(), - ) - } - } - private fun onNetworkSwitchClick(networkId: String, isChecked: Boolean) { val selectedWalletId = selectedMultiWalletIdFlow.value diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index c7e25a4287..e9b8527b12 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -2,11 +2,13 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.utils.Provider +import kotlinx.collections.immutable.ImmutableList /** * Factory for creating [MyPortfolioUM] @@ -18,9 +20,10 @@ import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM */ internal class MyPortfolioUMFactory( private val onAddClick: () -> Unit, - private val onTokenItemClick: (Int, CryptoCurrency.ID) -> Unit, private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, private val tokenActionsHandler: TokenActionsHandler, + private val currentState: Provider, + private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, ) { fun create(portfolioData: PortfolioData, portfolioUIData: PortfolioUIData): MyPortfolioUM { @@ -58,8 +61,9 @@ internal class MyPortfolioUMFactory( ), bsConfig = createAddToPortfolioBSConfig(portfolioData = portfolioData, portfolioUIData = portfolioUIData), onAddClick = onAddClick, - onTokenItemClick = onTokenItemClick, quickActionsIntents = tokenActionsHandler, + currentState = currentState, + updateTokens = updateTokens, ) .convert(walletsWithCurrencies) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index cf437c80c1..218d63c9ae 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -41,6 +41,7 @@ internal class PortfolioTokenUMConverter( return PortfolioTokenUM( tokenItemState = tokenItemStateConverter.convert(value = value.status), + walletId = value.userWallet.walletId, isBalanceHidden = isBalanceHidden, isQuickActionsShown = false, quickActions = quickActions(cryptoData = value), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 8dea5f71b6..b77101acb9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -53,6 +53,15 @@ internal class TokenActionsHandler @AssistedInject constructor( } fun onTokenLongClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val actions = cryptoCurrencyData.actions + .mapNotNull { it.toAction(cryptoCurrencyData) } + .sortedBy { it.order } + .toImmutableList() + + if (actions.isEmpty()) { + return + } + updateTokenActionsBSConfig { TangemBottomSheetConfig( isShow = true, @@ -63,10 +72,7 @@ internal class TokenActionsHandler @AssistedInject constructor( }, content = TokenActionsBSContentUM( title = cryptoCurrencyData.userWallet.name, - actions = cryptoCurrencyData.actions - .mapNotNull { it.toAction(cryptoCurrencyData) } - .sortedBy { it.order } - .toImmutableList(), + actions = actions, onActionClick = { action -> handle(action, cryptoCurrencyData) updateTokenActionsBSConfig { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index 1614ad4ebb..4dc478786c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -2,13 +2,16 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isZero +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList /** @@ -23,28 +26,94 @@ internal class TokensPortfolioUMConverter( private val isAllAvailableNetworksAdded: Boolean, private val bsConfig: TangemBottomSheetConfig, private val onAddClick: () -> Unit, - private val onTokenItemClick: (index: Int, id: CryptoCurrency.ID) -> Unit, private val quickActionsIntents: TokenActionsHandler, + private val currentState: Provider, + private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, ) : Converter>, MyPortfolioUM.Tokens> { override fun convert(value: Map>): MyPortfolioUM.Tokens { + val currentTokensState = currentState() as? MyPortfolioUM.Tokens + return MyPortfolioUM.Tokens( tokens = value .flatMap { entry -> entry.value } - .mapIndexed { index, cryptoData -> + .map { cryptoData -> PortfolioTokenUMConverter( appCurrency = appCurrency, isBalanceHidden = isBalanceHidden, - onTokenItemClick = { onTokenItemClick(index, it.currency.id) }, + onTokenItemClick = { toggleQuickActions(cryptoData) }, tokenActionsHandler = quickActionsIntents, - ).convert(cryptoData) + ).convert(value = cryptoData) to cryptoData } + .setQuickActionsVisibility(currentState = currentTokensState) .toImmutableList(), buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available, addToPortfolioBSConfig = bsConfig, onAddClick = onAddClick, - tokenReceiveBSConfig = TangemBottomSheetConfig.Empty, - tokenActionsBSConfig = TangemBottomSheetConfig.Empty, + tokenReceiveBSConfig = (currentState() as? MyPortfolioUM.Tokens) + ?.tokenReceiveBSConfig + ?: TangemBottomSheetConfig.Empty, + tokenActionsBSConfig = (currentState() as? MyPortfolioUM.Tokens) + ?.tokenActionsBSConfig + ?: TangemBottomSheetConfig.Empty, ) } + + private fun List>.setQuickActionsVisibility( + currentState: MyPortfolioUM.Tokens?, + ): List { + return when { + // if there is only one token and it has empty balance, show quick actions for it + currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> { + this.map { (token, _) -> + token.copy(isQuickActionsShown = true) + } + } + // if there is no previous state, hide quick actions for all tokens + currentState == null -> { + this.map { (token, _) -> + token.copy(isQuickActionsShown = false) + } + } + else -> { + val previousList = currentState.tokens + + // otherwise, keep previous state + this.map { (token, _) -> + token.copy( + isQuickActionsShown = previousList + .firstOrNull { it.matchWith(token) } + ?.isQuickActionsShown ?: false, + ) + } + } + } + } + + private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { + return cryptoData.status.value.amount?.isZero() ?: false + } + + private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) { + updateTokens { tokenList -> + tokenList.map { + it.copy( + isQuickActionsShown = if (it.matchWith(cryptoData)) { + !it.isQuickActionsShown + } else { + false + }, + ) + }.toImmutableList() + } + } + + private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean { + return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id + } + + private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { + return this.walletId == cryptoData.userWallet.walletId && + this.tokenItemState.id == cryptoData.status.currency.id.value + } } \ 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 247b98ce7d..3d027349ed 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 @@ -4,6 +4,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM @@ -64,5 +65,6 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider Date: Thu, 5 Sep 2024 09:18:09 +0300 Subject: [PATCH 121/197] Updated on 2026-08-14 --- .../core/ui/components/rows/BlockchainRow.kt | 24 +++++++++++--- .../core/ui/components/rows/RowComponents.kt | 13 ++++++-- .../components/rows/model/BlockchainRowUM.kt | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 32 ++++++++----------- .../impl/model/BlockchainRowUMConverter.kt | 17 ++++++++-- .../impl/model/MarketsPortfolioModel.kt | 8 ++--- .../impl/model/MyPortfolioUMFactory.kt | 6 +--- .../impl/model/SelectNetworkUMConverter.kt | 6 +++- .../impl/ui/AddToPortfolioBottomSheet.kt | 8 +++-- 9 files changed, 73 insertions(+), 42 deletions(-) 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 70150328ce..df2cdf37a9 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 @@ -10,6 +10,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -20,6 +21,8 @@ import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +private const val DISABLED_ICON_ALPHA = 0.4f + /** * [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2737-2800&t=ewlXfWwbDnRhjw4B-4) * */ @@ -36,8 +39,9 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni icon = { RowIcon( resId = model.iconResId, - isColored = model.isSelected, + isColored = model.isSelected && model.isEnabled, showAccentBadge = model.isMainNetwork, + isEnabled = model.isEnabled, ) }, text = { @@ -46,6 +50,7 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni secondText = model.type, accentMainText = model.isSelected, accentSecondText = model.isMainNetwork, + isEnabled = model.isEnabled, ) }, action = action, @@ -58,10 +63,9 @@ private fun RowIcon( isColored: Boolean, showAccentBadge: Boolean, modifier: Modifier = Modifier, + isEnabled: Boolean = true, ) { - Box( - modifier = modifier.size(TangemTheme.dimens.size24), - ) { + Box(modifier = modifier.size(TangemTheme.dimens.size24)) { if (isColored) { Image( modifier = Modifier @@ -78,6 +82,7 @@ private fun RowIcon( color = TangemTheme.colors.button.secondary, shape = CircleShape, ) + .alpha(if (isEnabled) 1f else DISABLED_ICON_ALPHA) .size(TangemTheme.dimens.size22), painter = painterResource(id = resId), tint = TangemTheme.colors.icon.informative, @@ -126,7 +131,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid BlockchainRow( model = state, action = { - TangemSwitch(onCheckedChange = { }, checked = true) + TangemSwitch(onCheckedChange = { }, checked = true, enabled = state.isEnabled) }, ) }, @@ -160,6 +165,15 @@ private class BlockchainRowParameterProvider : CollectionPreviewParameterProvide isMainNetwork = false, isSelected = false, ), + BlockchainRowUM( + id = "2", + name = "BNB BEACON CHAIN", + type = "1234567890111213141516171819", + iconResId = R.drawable.ic_bsc_16, + isMainNetwork = false, + isSelected = false, + isEnabled = false, + ), ), ) // endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt index ede73396a0..258a153de4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -53,6 +53,7 @@ internal fun RowText( accentSecondText: Boolean, modifier: Modifier = Modifier, subtitle: TextReference? = null, + isEnabled: Boolean = true, ) { Column( modifier = modifier, @@ -67,7 +68,11 @@ internal fun RowText( modifier = Modifier.weight(weight = 10f, fill = false), text = mainText, style = TangemTheme.typography.subtitle2, - color = if (accentMainText) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + color = if (isEnabled) { + if (accentMainText) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary + } else { + TangemTheme.colors.text.disabled + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -76,7 +81,11 @@ internal fun RowText( modifier = Modifier.weight(weight = 4f, fill = false), text = secondText, style = TangemTheme.typography.body2, - color = if (accentSecondText) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary, + color = if (isEnabled) { + if (accentSecondText) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary + } else { + TangemTheme.colors.text.disabled + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt index 700b4bc377..6cba8925ee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt @@ -10,4 +10,5 @@ data class BlockchainRowUM( val iconResId: Int, val isMainNetwork: Boolean, val isSelected: Boolean, + val isEnabled: Boolean = true, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index 761d567f41..c1b58bdbf6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -30,28 +30,22 @@ internal class AddToPortfolioBSContentUMFactory( private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, private val onNetworkSwitchClick: (String, Boolean) -> Unit, private val onWalletSelect: (UserWalletId) -> Unit, - private val onContinueClick: ( - selectedWalletId: UserWalletId, - addedNetworks: Set, - removedNetworks: Set, - ) -> Unit, + private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set) -> Unit, ) { /** * Create [TangemBottomSheetConfig] * - * @param portfolioData portfolio data - * @param portfolioUIData portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param networksWithToggle networks with toggle - * @param isUserChangedNetworks flag indicates if user changed networks + * @param portfolioData portfolio data + * @param portfolioUIData portfolio bottom sheet visibility model + * @param selectedWallet selected wallet + * @param alreadyAddedNetworks already added networks */ fun create( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, selectedWallet: UserWallet, - networksWithToggle: Map, - isUserChangedNetworks: Boolean, + alreadyAddedNetworks: Set, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( isShow = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, @@ -59,11 +53,17 @@ internal class AddToPortfolioBSContentUMFactory( content = AddToPortfolioBSContentUM( selectedWallet = selectedWallet.toSelectedUserWalletItemUM(), selectNetworkUM = SelectNetworkUMConverter( - networksWithToggle = networksWithToggle, + networksWithToggle = portfolioUIData.addToPortfolioData.associateWithToggle( + userWalletId = selectedWallet.walletId, + alreadyAddedNetworkIds = alreadyAddedNetworks, + ), + alreadyAddedNetworks = alreadyAddedNetworks, onNetworkSwitchClick = onNetworkSwitchClick, ).convert(value = token), isScanCardNotificationVisible = portfolioUIData.hasMissedDerivations, - continueButtonEnabled = isUserChangedNetworks, + continueButtonEnabled = portfolioUIData.addToPortfolioData.isUserChangedNetworks( + userWalletId = selectedWallet.walletId, + ), onContinueButtonClick = { val alreadyAddedNetworkIds = portfolioData.walletsWithCurrencies[selectedWallet].orEmpty() .map { it.status.currency.network.backendId } @@ -75,10 +75,6 @@ internal class AddToPortfolioBSContentUMFactory( userWalletId = selectedWallet.walletId, alreadyAddedNetworkIds = alreadyAddedNetworkIds, ), - portfolioUIData.addToPortfolioData.getRemovedNetworks( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworkIds, - ), ) }, walletSelectorConfig = crateWalletSelectorBSConfig( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt index b80d71c2d1..09be6a8976 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt @@ -10,9 +10,13 @@ import com.tangem.utils.converter.Converter /** * Converter from [TokenMarketInfo.Network] to [BlockchainRowUM] * + * @property alreadyAddedNetworks set of already added networks + * [REDACTED_AUTHOR] */ -internal object BlockchainRowUMConverter : Converter, BlockchainRowUM> { +internal class BlockchainRowUMConverter( + private val alreadyAddedNetworks: Set, +) : Converter, BlockchainRowUM> { override fun convert(value: Pair): BlockchainRowUM { val (network, isSelected) = value @@ -22,17 +26,24 @@ internal object BlockchainRowUMConverter : Converter, - removedNetworks: Set, - ) { + private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set) { modelScope.launch { saveMarketTokensUseCase( userWalletId = userWalletId, tokenMarketParams = params.token, addedNetworks = addedNetworks, - removedNetworks = removedNetworks, + removedNetworks = emptySet(), ) onAddToPortfolioBSVisibilityChange(isShow = false) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index e9b8527b12..7cc596fc95 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -95,11 +95,7 @@ internal class MyPortfolioUMFactory( portfolioData = portfolioData, portfolioUIData = portfolioUIData, selectedWallet = selectedWallet, - networksWithToggle = portfolioUIData.addToPortfolioData.associateWithToggle( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - ), - isUserChangedNetworks = portfolioUIData.addToPortfolioData.isUserChangedNetworks(selectedWallet.walletId), + alreadyAddedNetworks = alreadyAddedNetworks, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt index ea02d5c805..d7d84b6bcb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt @@ -10,12 +10,14 @@ import kotlinx.collections.immutable.toImmutableList * Converter from [TokenMarketParams] to [SelectNetworkUM] * * @property networksWithToggle map of networks with toggles + * @property alreadyAddedNetworks already added networks * @property onNetworkSwitchClick callback is called when network switch is clicked * [REDACTED_AUTHOR] */ internal class SelectNetworkUMConverter( private val networksWithToggle: Map, + private val alreadyAddedNetworks: Set, private val onNetworkSwitchClick: (String, Boolean) -> Unit, ) : Converter { @@ -25,7 +27,9 @@ internal class SelectNetworkUMConverter( iconUrl = value.imageUrl, tokenName = value.name, tokenCurrencySymbol = value.symbol, - networks = BlockchainRowUMConverter.convertList(networksWithToggle.toList()).toImmutableList(), + networks = BlockchainRowUMConverter(alreadyAddedNetworks) + .convertList(networksWithToggle.toList()) + .toImmutableList(), onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um.id, isChecked) }, ) } 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 19080cb275..524c8bfd4f 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 @@ -224,9 +224,13 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi action = { TangemSwitch( checked = network.isSelected, - onCheckedChange = { - state.onNetworkSwitchClick(network, it) + checkedColor = if (network.isEnabled) { + TangemTheme.colors.control.checked + } else { + TangemTheme.colors.icon.inactive }, + onCheckedChange = { state.onNetworkSwitchClick(network, it) }, + enabled = network.isEnabled, ) }, ) From 1006477fd77f16d2add0bdadf12085cc89433f7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 16:28:36 +0300 Subject: [PATCH 122/197] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 9 +++++- .../repository/DefaultCurrenciesRepository.kt | 2 ++ .../domain/markets/SaveMarketTokensUseCase.kt | 28 +++++++++++-------- .../impl/loader/PortfolioDataLoader.kt | 14 ++++++++++ .../impl/model/MyPortfolioUMFactory.kt | 15 ++++------ 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 293ec473c3..59b0f45dc5 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import dagger.Module import dagger.Provides @@ -53,7 +54,13 @@ object MarketsDomainModule { derivationsRepository: DerivationsRepository, marketsTokenRepository: MarketsTokenRepository, currenciesRepository: CurrenciesRepository, + networksRepository: NetworksRepository, ): SaveMarketTokensUseCase { - return SaveMarketTokensUseCase(derivationsRepository, marketsTokenRepository, currenciesRepository) + return SaveMarketTokensUseCase( + derivationsRepository = derivationsRepository, + marketsTokenRepository = marketsTokenRepository, + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index d66f067313..238150055c 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 @@ -481,6 +481,8 @@ internal class DefaultCurrenciesRepository( @OptIn(ExperimentalCoroutinesApi::class) override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { return userWalletsStore.userWallets.flatMapLatest { userWallets -> + userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } + val userWalletsWithCurrencies = userWallets .filterNot(UserWallet::isLocked) .map { userWallet -> diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index bc04fa27ec..d944b5f9de 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -3,8 +3,10 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId /** @@ -20,6 +22,7 @@ class SaveMarketTokensUseCase( private val derivationsRepository: DerivationsRepository, private val marketsTokenRepository: MarketsTokenRepository, private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, ) { suspend operator fun invoke( @@ -44,17 +47,20 @@ class SaveMarketTokensUseCase( networkIds = addedNetworks.map { Network.ID(it.networkId) }, ) - currenciesRepository.addCurrencies( - userWalletId = userWalletId, - currencies = addedNetworks.mapNotNull { - marketsTokenRepository.createCryptoCurrency( - userWalletId = userWalletId, - token = tokenMarketParams, - network = it, - ) - }, - ) + val addedCurrencies = addedNetworks.mapNotNull { + marketsTokenRepository.createCryptoCurrency( + userWalletId = userWalletId, + token = tokenMarketParams, + network = it, + ) + } - // TODO: [REDACTED_JIRA] + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = addedCurrencies) + + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = addedCurrencies.map(CryptoCurrency::network).toSet(), + refresh = true, + ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index db10d655ab..ac016c1bbf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -56,6 +56,7 @@ internal class PortfolioDataLoader @Inject constructor( ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), ) .map { portfolioData.copy(walletsWithBalance = it) } + .onEmpty { emit(portfolioData) } } } @@ -93,6 +94,19 @@ internal class PortfolioDataLoader @Inject constructor( } } } + .onEmpty { + emit( + walletsWithStatuses.mapValues { (wallet, statuses) -> + statuses.map { + PortfolioData.CryptoCurrencyData( + userWallet = wallet, + status = it, + actions = emptyList(), + ) + } + }, + ) + } } .distinctUntilChanged() } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 7cc596fc95..041c92c05f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -72,21 +72,16 @@ internal class MyPortfolioUMFactory( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, ): TangemBottomSheetConfig { - val walletId = portfolioUIData.selectedWalletId - ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }?.walletId - val selectedWallet = portfolioData.walletsWithCurrencies.keys - .firstOrNull { it.walletId == walletId } - ?: error("portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId") + .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } + ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency } + ?: error("walletsWithCurrencies don't contain selected wallet or any multi-currency wallet") val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty() val alreadyAddedNetworks = requireNotNull( - value = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(availableNetworks)[selectedWallet], - lazyMessage = { - "portfolioModel.walletsWithCurrencyStatuses doesn't contain selected wallet: $walletId" - }, + value = portfolioData.walletsWithCurrencies.filterAvailableNetworks(availableNetworks)[selectedWallet], + lazyMessage = { "walletsWithCurrencies don't contain ${selectedWallet.walletId}" }, ) .map { it.status.currency.network.backendId } .toSet() From 2d753e14af7d1ae17d11a8512112f25162aa6a78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 18:33:40 +0300 Subject: [PATCH 123/197] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 ++ .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 3 +- .../tangem/common/ui/charts/MarketChart.kt | 2 ++ .../com/tangem/core/ui/components/Shimmers.kt | 20 +++++++++++ .../src/main/res/drawable/ic_analytics_24.xml | 18 ++++++++++ .../repository/DefaultQuotesRepository.kt | 1 - .../domain/markets/TokenMarketParams.kt | 2 +- domain/tokens/build.gradle.kts | 1 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 8 +++++ .../domain/tokens/model/TokenActionsState.kt | 2 ++ .../managetokens/ui/ManageTokensScreen.kt | 13 ++++++- .../details/MarketsTokenDetailsComponent.kt | 1 + features/markets/impl/build.gradle.kts | 1 + .../DefaultMarketsTokenDetailsComponent.kt | 34 +++++++++++++------ .../impl/model/MarketsTokenDetailsModel.kt | 13 +++---- .../impl/ui/MarketsTokenDetailsContent.kt | 22 ++++++------ .../impl/ui/components/InfoBottomSheet.kt | 14 +++++--- .../details/impl/ui/components/LinksBlock.kt | 5 ++- .../ui/components/PricePerformanceBlock.kt | 2 +- .../ui/components/TokenMarketDetailsBody.kt | 14 +++++--- .../impl/ui/state/MarketsTokenDetailsUM.kt | 4 +-- .../impl/DefaultMarketsEntryComponent.kt | 1 + .../impl/model/TokenActionsHandler.kt | 7 ++-- .../impl/ui/AddToPortfolioBottomSheet.kt | 14 +++++++- .../block/impl/model/TokenMarketBlockModel.kt | 1 + features/wallet/impl/build.gradle.kts | 1 + .../MultiWalletCurrencyActionsConverter.kt | 5 +++ .../WalletCurrencyActionsClickIntents.kt | 29 ++++++++++++++++ gradle/dependencies.toml | 2 ++ 30 files changed, 189 insertions(+), 55 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_analytics_24.xml 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 8ec7199796..47d32be700 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 @@ -7,6 +7,7 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -230,6 +231,7 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, stakingFeatureToggles: StakingFeatureToggles, + marketsFeatureToggles: MarketsFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -241,6 +243,7 @@ internal object TokensDomainModule { networksRepository = networksRepository, stakingRepository = stakingRepository, stakingFeatureToggles = stakingFeatureToggles, + marketsFeatureToggles = marketsFeatureToggles, dispatchers = dispatchers, ) } 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 c2b8cf249c..f33cf4b1af 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 @@ -193,6 +193,7 @@ internal class ChildFactory @Inject constructor( params = MarketsTokenDetailsComponent.Params( token = route.token, appCurrency = route.appCurrency, + showPortfolio = route.showPortfolio, ), componentFactory = marketsTokenDetailsComponentFactory, ) 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 d8e383a18e..2362e15686 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 @@ -271,5 +271,6 @@ sealed class AppRoute(val path: String) : Route { data class MarketsTokenDetails( val token: TokenMarketParams, val appCurrency: AppCurrency, - ) : AppRoute(path = "/markets_token_details/${token.id}") + val showPortfolio: Boolean, + ) : AppRoute(path = "/markets_token_details/${token.id}/$showPortfolio") } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index cd14d8f8c8..992380b05e 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -125,6 +125,8 @@ fun MarketChart( // Sometimes the chart is not drawn correctly (ex. in LazyLayout), so we need to force the redraw .drawBehind { state.markerFraction + state.chartColor + state.markerHighlightRightSide }, chart = chart, modelProducer = state.modelProducer, 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 c0f97514dc..e613aa253a 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 @@ -22,10 +22,13 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.PrimarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.chip.Chip import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.* import com.valentinilk.shimmer.* @@ -123,6 +126,22 @@ fun SmallButtonShimmer( ) } +/** + * Shimmer for Chip + * Height and min width will be set automatically + */ +@Composable +fun ChipShimmer(modifier: Modifier = Modifier) { + Chip( + modifier = modifier + .clip(RoundedCornerShape(size = 100.dp)) + .shimmer(LocalTangemShimmer.current), + text = TextReference.EMPTY, + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ) +} + internal val TangemShimmer: Shimmer @Composable get() = rememberShimmer( @@ -195,6 +214,7 @@ private fun ShimmersPreview() { ) SmallButtonShimmer(withIcon = true) SmallButtonShimmer(withIcon = false) + ChipShimmer() } } } diff --git a/core/ui/src/main/res/drawable/ic_analytics_24.xml b/core/ui/src/main/res/drawable/ic_analytics_24.xml new file mode 100644 index 0000000000..743431578d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_analytics_24.xml @@ -0,0 +1,18 @@ + + + + diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index cc25c467e0..13e1c3e368 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -143,7 +143,6 @@ internal class DefaultQuotesRepository( block = { acc.add(rawCurrencyId) }, ) } - acc } } diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt index 643846c466..e14b1f417b 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt @@ -15,7 +15,7 @@ data class TokenMarketParams( @Serializable data class Quotes( val currentPrice: SerializedBigDecimal, - val h24Percent: SerializedBigDecimal, + val h24Percent: SerializedBigDecimal?, val weekPercent: SerializedBigDecimal?, val monthPercent: SerializedBigDecimal?, ) diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index bbe01868f9..a1c639300b 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { /** Project - Api */ implementation(projects.features.send.api) implementation(projects.features.staking.api) + implementation(projects.features.markets.api) /** Project - Other */ implementation(projects.core.utils) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index a1e112bb15..c1ac067bff 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -12,6 +12,7 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero @@ -33,6 +34,7 @@ class GetCryptoCurrencyActionsUseCase( private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val stakingFeatureToggles: StakingFeatureToggles, + private val marketsFeatureToggles: MarketsFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -112,6 +114,12 @@ class GetCryptoCurrencyActionsUseCase( val activeList = mutableListOf() val disabledList = mutableListOf() + // markets + // not a custom token + if (marketsFeatureToggles.isFeatureEnabled && cryptoCurrencyStatus.currency.id.rawCurrencyId != null) { + activeList.add(TokenActionsState.ActionState.Analytics(ScenarioUnavailabilityReason.None)) + } + // copy address if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index c6db8e28e2..2531bc3ee2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -30,6 +30,8 @@ data class TokenActionsState( data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Analytics(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class HideToken(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() } } \ No newline at end of file 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 d84cc432eb..3b4b3bb478 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 @@ -53,6 +53,8 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -369,6 +371,8 @@ private fun NetworksList( isEditable: Boolean, modifier: Modifier = Modifier, ) { + val hapticManager = LocalHapticManager.current + AnimatedVisibility( modifier = modifier, visible = networks is NetworksUM.Expanded, @@ -403,7 +407,14 @@ private fun NetworksList( if (isEditable) { TangemSwitch( checked = network.isSelected, - onCheckedChange = network.onSelectedStateChange, + onCheckedChange = { checked -> + if (checked) { + hapticManager.perform(TangemHapticEffect.View.ToggleOn) + } else { + hapticManager.perform(TangemHapticEffect.View.ToggleOff) + } + network.onSelectedStateChange(checked) + }, ) } }, diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt index b7c38d44bb..3a4b5e4b57 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt @@ -19,6 +19,7 @@ interface MarketsTokenDetailsComponent : ComposableContentComponent { data class Params( val token: TokenMarketParams, val appCurrency: AppCurrency, + val showPortfolio: Boolean, ) @Composable diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 8cccec2563..d356234b52 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(deps.compose.ui.utils) implementation(deps.lifecycle.compose) implementation(deps.androidx.activity.compose) + implementation(deps.markdown.composeview) /* DI */ implementation(deps.hilt.android) 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 0d758ce282..22c2c2b970 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 @@ -33,17 +33,21 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( private val model: MarketsTokenDetailsModel = getOrCreateModel(params) - private val portfolioComponent = portfolioComponentFactory.create( - context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params(params.token), - ) + private val portfolioComponent: MarketsPortfolioComponent? = if (params.showPortfolio) { + portfolioComponentFactory.create( + context = child("my_portfolio"), + params = MarketsPortfolioComponent.Params(params.token), + ) + } else { + null + } init { componentScope.launch { model.networksState.collectLatest { when (it) { - is TokenNetworksState.NetworksAvailable -> portfolioComponent.setTokenNetworks(it.networks) - TokenNetworksState.NoNetworksAvailable -> portfolioComponent.setNoNetworksAvailable() + is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks(it.networks) + TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable() else -> {} } } @@ -79,10 +83,16 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( backgroundColor = LocalMainBottomSheetColor.current.value, addTopBarStatusBarPadding = false, state = state, - onBackClick = ::navigateBack, + onBackClick = { + if (bsState == BottomSheetState.EXPANDED) { + navigateBack() + } + }, onHeaderSizeChange = onHeaderSizeChange, - portfolioBlock = { blockModifier -> - portfolioComponent.Content(blockModifier) + portfolioBlock = portfolioComponent?.let { component -> + { blockModifier -> + component.Content(blockModifier) + } }, ) } @@ -109,8 +119,10 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( state = state, onBackClick = ::navigateBack, onHeaderSizeChange = {}, - portfolioBlock = { blockModifier -> - portfolioComponent.Content(blockModifier) + portfolioBlock = portfolioComponent?.let { component -> + { blockModifier -> + component.Content(blockModifier) + } }, ) } 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 83b17519eb..c1f7cd2393 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 @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.sorted import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model @@ -135,15 +134,16 @@ internal class MarketsTokenDetailsModel @Inject constructor( fiatCurrencySymbol = currentAppCurrency.value.symbol, ), dateTimeText = resourceReference(R.string.common_today), - priceChangePercentText = BigDecimalFormatter.formatPercent( - percent = params.token.tokenQuotes.h24Percent, - useAbsoluteValue = true, - ), + priceChangePercentText = params.token.tokenQuotes.h24Percent?.let { + BigDecimalFormatter.formatPercent( + percent = it, + useAbsoluteValue = true, + ) + }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), iconUrl = params.token.imageUrl, chartState = MarketsTokenDetailsUM.ChartState( dataProducer = chartDataProducer, - chartLook = MarketChartLook(), onLoadRetryClick = ::onLoadRetryClicked, status = MarketsTokenDetailsUM.ChartState.Status.LOADING, onMarkerPointSelected = ::onMarkerPointSelected, @@ -320,6 +320,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( currentQuotes.value = newInfo.quotes val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval) + state.update { it.copy( priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), 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 9bbdfb559c..295497c053 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 @@ -18,7 +18,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM @@ -54,7 +53,7 @@ internal fun MarketsTokenDetailsContent( addTopBarStatusBarPadding: Boolean, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, - portfolioBlock: @Composable (Modifier) -> Unit, + portfolioBlock: @Composable ((Modifier) -> Unit)?, modifier: Modifier = Modifier, ) { Content( @@ -78,7 +77,7 @@ private fun Content( addTopBarStatusBarInsets: Boolean, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, - portfolioBlock: @Composable (Modifier) -> Unit, + portfolioBlock: @Composable ((Modifier) -> Unit)?, modifier: Modifier = Modifier, ) { val density = LocalDensity.current @@ -129,9 +128,7 @@ private fun Content( ) } item { SpacerH32() } - item( - contentType = "chart", - ) { + item("chart") { MarketTokenDetailsChart( modifier = Modifier.fillMaxWidth(), backgroundColor = backgroundColor, @@ -167,11 +164,13 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) - PriceChangeInPercent( - valueInPercent = state.priceChangePercentText, - type = state.priceChangeType, - textStyle = TangemTheme.typography.caption2, - ) + if (state.priceChangePercentText != null) { + PriceChangeInPercent( + valueInPercent = state.priceChangePercentText, + type = state.priceChangeType, + textStyle = TangemTheme.typography.caption2, + ) + } } } SpacerW4() @@ -290,7 +289,6 @@ private fun Preview() { priceChangeType = PriceChangeType.UP, chartState = MarketsTokenDetailsUM.ChartState( dataProducer = MarketChartDataProducer.build { }, - chartLook = MarketChartLook(), onLoadRetryClick = {}, status = MarketsTokenDetailsUM.ChartState.Status.LOADING, onMarkerPointSelected = { _, _ -> }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt index 0d44a41f91..d78f6b2d99 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity @@ -17,6 +16,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent +import dev.jeziellago.compose.markdowntext.MarkdownText @Composable internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { @@ -35,10 +35,14 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { .verticalScroll(rememberScrollState()) .padding(horizontal = TangemTheme.dimens.spacing28), ) { - Text( - text = it.body.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, + MarkdownText( + markdown = it.body.resolveReference(), + disableLinkMovementMethod = true, + linkifyMask = 0, + syntaxHighlightColor = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.secondary, + ), ) SpacerH(bottomBarHeight) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index cfbb51ec74..e08e4a3052 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.ChipShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.buttons.chip.Chip @@ -145,9 +145,8 @@ private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolea horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { repeat(times = 3) { - SmallButtonShimmer( + ChipShimmer( modifier = Modifier.weight(1f), - withIcon = true, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 057d1bbed6..4e947ff908 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -161,7 +161,7 @@ internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) { }, content = { Column( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { Row( 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 5d9e93fd4e..d7ae7737c6 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 @@ -13,7 +13,7 @@ import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData internal fun LazyListScope.tokenMarketDetailsBody( state: MarketsTokenDetailsUM.Body, - portfolioBlock: @Composable (Modifier) -> Unit, + portfolioBlock: @Composable ((Modifier) -> Unit)?, ) { when (state) { MarketsTokenDetailsUM.Body.Loading -> { @@ -21,8 +21,10 @@ internal fun LazyListScope.tokenMarketDetailsBody( DescriptionPlaceholder(modifier = Modifier.blockPaddings()) } - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) + if (portfolioBlock != null) { + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } } loadingInfoBlocks() @@ -32,8 +34,10 @@ internal fun LazyListScope.tokenMarketDetailsBody( description(state.description) } - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) + if (portfolioBlock != null) { + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } } infoBlocksList(state.infoBlocks) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt index 6023a1c8a2..cec823bccb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt @@ -2,7 +2,6 @@ package com.tangem.features.markets.details.impl.ui.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent @@ -15,7 +14,7 @@ internal data class MarketsTokenDetailsUM( val priceText: String, val iconUrl: String?, val dateTimeText: TextReference, - val priceChangePercentText: String, + val priceChangePercentText: String?, val priceChangeType: PriceChangeType, val selectedInterval: PriceChangeInterval, val markerSet: Boolean, @@ -29,7 +28,6 @@ internal data class MarketsTokenDetailsUM( data class ChartState( val status: Status, val dataProducer: MarketChartDataProducer, - val chartLook: MarketChartLook, val onLoadRetryClick: () -> Unit, val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, ) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index 1419aa7349..7f96268b4c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -70,6 +70,7 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( params = MarketsTokenDetailsComponent.Params( token = token.toSerializableParam(), appCurrency = appCurrency, + showPortfolio = true, ), ), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index b77101acb9..c134371f8e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -182,13 +182,12 @@ internal class TokenActionsHandler @AssistedInject constructor( is TokenActionsState.ActionState.HideToken -> null is TokenActionsState.ActionState.Receive -> TokenActionsBSContentUM.Action.Receive is TokenActionsState.ActionState.Sell -> TokenActionsBSContentUM.Action.Sell - is TokenActionsState.ActionState.Send -> { - TokenActionsBSContentUM.Action.Send.takeIf { - cryptoCurrencyData.status.value.amount.isNullOrZero().not() - } + is TokenActionsState.ActionState.Send -> TokenActionsBSContentUM.Action.Send.takeIf { + cryptoCurrencyData.status.value.amount.isNullOrZero().not() } is TokenActionsState.ActionState.Stake -> TokenActionsBSContentUM.Action.Stake is TokenActionsState.ActionState.Swap -> TokenActionsBSContentUM.Action.Exchange + is TokenActionsState.ActionState.Analytics -> null } .takeIf { this.unavailabilityReason == ScenarioUnavailabilityReason.None } 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 524c8bfd4f..b73ba99609 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 @@ -36,6 +36,8 @@ 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.extensions.resourceReference +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 @@ -168,6 +170,8 @@ private fun ContinueButton( @Suppress("LongMethod") @Composable private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { + val hapticManager = LocalHapticManager.current + InformationBlock( modifier = modifier, title = { @@ -229,7 +233,15 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi } else { TangemTheme.colors.icon.inactive }, - onCheckedChange = { state.onNetworkSwitchClick(network, it) }, + onCheckedChange = { checked -> + if (checked) { + hapticManager.perform(TangemHapticEffect.View.ToggleOn) + } else { + hapticManager.perform(TangemHapticEffect.View.ToggleOff) + } + + state.onNetworkSwitchClick(network, checked) + }, enabled = network.isEnabled, ) }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index bb584f1731..71c71a4d5e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -143,6 +143,7 @@ internal class TokenMarketBlockModel @Inject constructor( AppRoute.MarketsTokenDetails( token = tokenParam, appCurrency = currentAppCurrency.value, + showPortfolio = false, ), ) } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 5aa98a53a1..db8ba49b05 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -79,6 +79,7 @@ dependencies { implementation(projects.domain.analytics) implementation(projects.domain.visa) implementation(projects.domain.staking.models) + implementation(projects.domain.markets.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index f4009e9c73..0ff004ae48 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -95,6 +95,11 @@ internal class MultiWalletCurrencyActionsConverter( icon = R.drawable.ic_hide_24 action = { clickIntents.onHideTokensClick(cryptoCurrencyStatus) } } + is TokenActionsState.ActionState.Analytics -> { + title = resourceReference(R.string.common_analytics) + icon = R.drawable.ic_analytics_24 + action = { clickIntents.onAnalyticsClick(cryptoCurrencyStatus) } + } } return TokenActionButtonConfig( 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 e66128d2c8..1a067b02f7 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 @@ -20,6 +20,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.* @@ -48,6 +49,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject interface WalletCurrencyActionsClickIntents { @@ -77,6 +79,8 @@ interface WalletCurrencyActionsClickIntents { fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onExploreClick() + + fun onAnalyticsClick(cryptoCurrencyStatus: CryptoCurrencyStatus) } @Suppress("LongParameterList", "LargeClass") @@ -378,6 +382,31 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } + override fun onAnalyticsClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + viewModelScope.launch { + val rawId = cryptoCurrencyStatus.currency.id.rawCurrencyId ?: return@launch + + appRouter.push( + AppRoute.MarketsTokenDetails( + TokenMarketParams( + id = rawId, + name = cryptoCurrencyStatus.currency.name, + symbol = cryptoCurrencyStatus.currency.symbol, + tokenQuotes = TokenMarketParams.Quotes( + currentPrice = cryptoCurrencyStatus.value.fiatRate ?: BigDecimal.ZERO, + h24Percent = cryptoCurrencyStatus.value.priceChange, + weekPercent = null, + monthPercent = null, + ), + imageUrl = cryptoCurrencyStatus.currency.iconUrl, + ), + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + showPortfolio = true, + ), + ) + } + } + override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4b1fb256b2..9fc95abab5 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,6 +85,7 @@ leakcanary = "2.13" decompose = "2.2.2" room = "2.6.1" markdown = "0.7.2" +markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem @@ -270,4 +271,5 @@ room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } +markdown-composeview = { module = "com.github.jeziellago:compose-markdown", version.ref = "markdownComposeView" } # endregion Other From 9cb17e5cc38fb706ff14296d8a376ab2b1f6f520 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 15:54:02 +0200 Subject: [PATCH 124/197] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6c64f890e2..3999c3c8e3 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -413,6 +413,7 @@ The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market Pull this up or tap the search bar to add tokens directly from the market Add tokens + NFC is not available on your device You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later From 06eabf3699cd2a947e5441a4a33d8e6d1e37ba44 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 22:17:47 +0300 Subject: [PATCH 125/197] Updated on 2026-08-14 --- .../staking/converters/YieldBalanceConverter.kt | 6 +++++- .../ActionTypeActiveStakeTransformer.kt | 13 +++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) 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 70577561ed..90b35354b7 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 @@ -3,6 +3,7 @@ package com.tangem.data.staking.converters import com.tangem.data.staking.converters.action.PendingActionConverter import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.utils.converter.Converter internal class YieldBalanceConverter : Converter { @@ -27,7 +28,10 @@ internal class YieldBalanceConverter : Converter { override fun transform(prevState: StakingUiState): StakingUiState { - val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) - val actionType = if (activeStake.pendingActions.isEmpty() || isTron) { + val isTronStakedBalance = isTronStakedBalance( + networkId = cryptoCurrencyStatus.currency.network.id, + activeStake = activeStake, + ) + val actionType = if (activeStake.pendingActions.isEmpty() || isTronStakedBalance) { StakingActionCommonType.EXIT } else { StakingActionCommonType.PENDING_OTHER @@ -21,4 +26,8 @@ internal class ActionTypeActiveStakeTransformer( return prevState.copy(actionType = actionType) } + + private fun isTronStakedBalance(networkId: Network.ID, activeStake: BalanceState): Boolean { + return isTron(networkId.value) && activeStake.type == BalanceType.STAKED + } } \ No newline at end of file From 6045dce1df2ceda7e81e774d967b1d6e37687e56 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Sep 2024 11:35:11 +0300 Subject: [PATCH 126/197] Updated on 2026-08-14 --- .../SetInitialDataStateTransformer.kt | 18 ++++++++++-------- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 +++++ 2 files changed, 15 insertions(+), 8 deletions(-) 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 796723c946..f07d45346e 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 @@ -22,6 +22,7 @@ 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 +import com.tangem.lib.crypto.BlockchainUtils.isPolkadot import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer @@ -144,18 +145,19 @@ internal class SetInitialDataStateTransformer( cryptoCurrencyStatus: CryptoCurrencyStatus, minimumCryptoAmount: SerializedBigDecimal?, ): RoundedListWithDividersItemData? { - minimumCryptoAmount ?: return null + if (minimumCryptoAmount == null) return null + if (!isPolkadot(cryptoCurrencyStatus.currency.network.id.value)) return null + + val formattedAmount = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = minimumCryptoAmount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ) return RoundedListWithDividersItemData( id = R.string.staking_details_minimum_requirement, startText = TextReference.Res(R.string.staking_details_minimum_requirement), - endText = TextReference.Str( - value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = minimumCryptoAmount, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), - ), + endText = TextReference.Str(formattedAmount), ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index e3618d6cd8..ada8b81979 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -77,6 +77,11 @@ object BlockchainUtils { return blockchain == Blockchain.Solana } + fun isPolkadot(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Polkadot || blockchain == Blockchain.PolkadotTestnet + } + data class BlockchainInfo( val blockchainId: String, val name: String, From b535ca1558d9cca0be9ac381837258a8311a1a9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 16:25:24 +0300 Subject: [PATCH 127/197] Updated on 2026-08-14 --- .../tangem/tap/features/home/HomeFragment.kt | 38 ++----------------- .../tangem/tap/features/home/HomeViewModel.kt | 6 ++- .../home/featuretoggles/HomeFeatureToggles.kt | 12 ------ .../tap/features/home/redux/HomeAction.kt | 1 - .../tap/features/home/redux/HomeMiddleware.kt | 24 ++---------- .../configs/feature_toggles_config.json | 8 ---- .../WalletManagerFactoryCreator.kt | 6 +-- .../BlockchainSDKFeatureToggles.kt | 5 +-- .../DefaultBlockchainSDKFeatureToggles.kt | 8 +--- 9 files changed, 17 insertions(+), 91 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt 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 292b7b1bf6..2379b399a9 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 @@ -7,18 +7,11 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.Analytics import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.domain.tokens.TokensAction -import com.tangem.tap.common.analytics.events.IntroductionProcess -import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.compose.StoriesScreen -import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.store @@ -32,9 +25,6 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber { @Inject override lateinit var uiDependencies: UiDependencies - @Inject - lateinit var homeFeatureToggles: HomeFeatureToggles - private var homeState: MutableState = mutableStateOf(store.state.homeState) private val viewModel by viewModels() @@ -77,31 +67,9 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber { private fun ScreenContent() { StoriesScreen( homeState = homeState, - onScanButtonClick = { - if (homeFeatureToggles.isCallbacksRefactoringEnabled) { - viewModel.onScanClick() - } else { - Analytics.send(IntroductionProcess.ButtonScanCard()) - store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope)) - } - }, - onShopButtonClick = { - if (homeFeatureToggles.isCallbacksRefactoringEnabled) { - viewModel.onShopClick() - } else { - Analytics.send(IntroductionProcess.ButtonBuyCards()) - store.dispatch(HomeAction.GoToShop) - } - }, - onSearchTokensClick = { - if (homeFeatureToggles.isCallbacksRefactoringEnabled) { - viewModel.onSearchClick() - } else { - Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } - store.dispatch(TokensAction.SetArgs.ReadAccess) - } - }, + onScanButtonClick = viewModel::onScanClick, + onShopButtonClick = viewModel::onShopClick, + onSearchTokensClick = viewModel::onSearchClick, ) } } \ No newline at end of file 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 0299b8832d..ce18962585 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 @@ -2,6 +2,8 @@ package com.tangem.tap.features.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -54,7 +56,9 @@ internal class HomeViewModel @Inject constructor( analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) analyticsEventHandler.send(Shop.ScreenOpened()) - urlOpener.openUrl(NEW_BUY_WALLET_URL) + Firebase.analytics.appInstanceId + .addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") } + .addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) } } fun onSearchClick() { diff --git a/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt deleted file mode 100644 index 8d23463a53..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.features.home.featuretoggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import javax.inject.Inject - -internal class HomeFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) { - - val isCallbacksRefactoringEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index f0171194c2..e3f0375cc1 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -15,7 +15,6 @@ sealed class HomeAction : Action { data class ReadCard(val scope: CoroutineScope) : HomeAction() data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() - data object GoToShop : HomeAction() data class UpdateCountryCode(val userCountryCode: String) : HomeAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 9da71e64e8..87a615ef97 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -1,7 +1,5 @@ package com.tangem.tap.features.home.redux -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess @@ -15,11 +13,12 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess -import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.eraseContext +import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store @@ -61,21 +60,6 @@ private fun handleHomeAction(action: Action) { readCard() } } - is HomeAction.GoToShop -> { - Analytics.send(Shop.ScreenOpened()) - Firebase.analytics.appInstanceId - .addOnSuccessListener { - store.dispatchOpenUrl("$NEW_BUY_WALLET_URL&app_instance_id=$it") - } - .addOnFailureListener { - store.dispatchOpenUrl(NEW_BUY_WALLET_URL) - } - // disabled for now in task [REDACTED_JIRA] - // when (action.userCountryCode) { - // RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL) - // else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop)) - // } - } } } diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index f0c510874b..cd7e3a75f2 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -15,10 +15,6 @@ "name": "WC_SOLANA_TX_SIGN_ENABLED", "version": "undefined" }, - { - "name": "CARDANO_TOKENS_SUPPORT_ENABLED", - "version": "5.12.0" - }, { "name": "STAKING_ENABLED", "version": "undefined" @@ -35,10 +31,6 @@ "name": "MARKETS_ENABLED", "version": "undefined" }, - { - "name": "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED", - "version": "5.14.0" - }, { "name": "NEW_MANAGE_TOKENS", "version": "undefined" diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index e7224f8434..3e8ec9fb13 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -23,7 +23,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, + @Suppress("UnusedPrivateMember") private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -33,9 +33,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( config = config, blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, - featureToggles = BlockchainFeatureToggles( - isCardanoTokenSupport = blockchainSDKFeatureToggles.isCardanoTokensSupportEnabled, - ), + featureToggles = BlockchainFeatureToggles(isCardanoTokenSupport = true), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), ) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt index fcdc2099b3..de92518ae5 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt @@ -1,6 +1,3 @@ package com.tangem.blockchainsdk.featuretoggles -internal interface BlockchainSDKFeatureToggles { - - val isCardanoTokensSupportEnabled: Boolean -} \ No newline at end of file +internal interface BlockchainSDKFeatureToggles \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt index ce7241d7d5..3d2504e78c 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt @@ -3,9 +3,5 @@ package com.tangem.blockchainsdk.featuretoggles import com.tangem.core.featuretoggle.manager.FeatureTogglesManager internal class DefaultBlockchainSDKFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : BlockchainSDKFeatureToggles { - - override val isCardanoTokensSupportEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "CARDANO_TOKENS_SUPPORT_ENABLED") -} \ No newline at end of file + @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager, +) : BlockchainSDKFeatureToggles \ No newline at end of file From 9a574200b5db43f01a42d67a7ef452eda29705d6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Sep 2024 14:07:51 +0500 Subject: [PATCH 128/197] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 6 ++ .../DefaultManageTokensRepository.kt | 56 +++++++++++++++++-- .../CheckCurrencyUnsupportedUseCase.kt | 21 +++++++ .../model/CurrencyUnsupportedState.kt | 12 ++++ .../repository/ManageTokensRepository.kt | 6 ++ .../ui/dialog/CurrencyUnsupportedDialog.kt | 22 ++++++++ .../utils/list/ManageTokensListManager.kt | 35 ++++++++++-- .../utils/list/ManageTokensUiActions.kt | 6 ++ .../utils/list/ManageTokensUiManager.kt | 38 ++++++++++++- 9 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/CurrencyUnsupportedState.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CurrencyUnsupportedDialog.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 611bce95ac..e15eea4635 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -96,4 +96,10 @@ internal object ManageTokensDomainModule { fun provideCheckHasLinkedTokensUseCase(repository: ManageTokensRepository): CheckHasLinkedTokensUseCase { return CheckHasLinkedTokensUseCase(repository) } + + @Provides + @Singleton + fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase { + return CheckCurrencyUnsupportedUseCase(repository) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 90409a1f9d..6c0d025397 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -14,13 +14,13 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.canHandleBlockchain +import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.supportedBlockchains +import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow -import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext -import com.tangem.domain.managetokens.model.ManageTokensListConfig -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.* import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.tokens.model.Network @@ -175,7 +175,51 @@ internal class DefaultManageTokensRepository( key = PreferencesKeys.getUserTokensKey(key.stringValue), ) } - // endregion - data class TokenWithSourceNetwork(val token: ManagedCryptoCurrency.Token, val sourceNetwork: SourceNetwork) + override suspend fun checkCurrencyUnsupportedState( + userWalletId: UserWalletId, + sourceNetwork: SourceNetwork, + ): CurrencyUnsupportedState? { + val userWallet = getUserWallet(userWalletId = userWalletId) + val blockchain = getBlockchain(sourceNetwork.id) + return when (sourceNetwork) { + is SourceNetwork.Default -> checkTokenUnsupportedState(userWallet = userWallet, blockchain = blockchain) + is SourceNetwork.Main -> checkBlockchainUnsupportedState(userWallet = userWallet, blockchain = blockchain) + } + } + + private fun checkBlockchainUnsupportedState( + userWallet: UserWallet, + blockchain: Blockchain, + ): CurrencyUnsupportedState? { + val canHandleBlockchain = userWallet.scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = userWallet.cardTypesResolver, + ) + + return if (!canHandleBlockchain) { + CurrencyUnsupportedState.UnsupportedNetwork(networkName = blockchain.getNetworkName()) + } else { + null + } + } + + private fun checkTokenUnsupportedState( + userWallet: UserWallet, + blockchain: Blockchain, + ): CurrencyUnsupportedState.Token? { + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + val supportedTokens = userWallet.scanResponse.card.supportedTokens(cardTypesResolver) + + return when { + // refactor this later by moving all this logic in card config + blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana) -> { + CurrencyUnsupportedState.Token.NetworkTokensUnsupported(networkName = blockchain.getNetworkName()) + } + !userWallet.scanResponse.card.canHandleToken(supportedTokens, blockchain, cardTypesResolver) -> { + CurrencyUnsupportedState.Token.UnsupportedCurve(networkName = blockchain.getNetworkName()) + } + else -> null + } + } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt new file mode 100644 index 0000000000..40dbddf6fd --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.CurrencyUnsupportedState +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.wallets.models.UserWalletId + +class CheckCurrencyUnsupportedUseCase( + private val repository: ManageTokensRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + sourceNetwork: ManagedCryptoCurrency.SourceNetwork, + ): Either { + return Either.catch { + repository.checkCurrencyUnsupportedState(userWalletId, sourceNetwork) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/CurrencyUnsupportedState.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/CurrencyUnsupportedState.kt new file mode 100644 index 0000000000..af7a16af06 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/CurrencyUnsupportedState.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.managetokens.model + +sealed class CurrencyUnsupportedState { + + abstract val networkName: String + sealed class Token : CurrencyUnsupportedState() { + data class NetworkTokensUnsupported(override val networkName: String) : Token() + data class UnsupportedCurve(override val networkName: String) : Token() + } + + data class UnsupportedNetwork(override val networkName: String) : CurrencyUnsupportedState() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt index c977402d75..cfa44069fe 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.managetokens.repository +import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext import com.tangem.domain.managetokens.model.ManagedCryptoCurrency @@ -16,4 +17,9 @@ interface ManageTokensRepository { tempAddedTokens: Map>, tempRemovedTokens: Map>, ): Boolean + + suspend fun checkCurrencyUnsupportedState( + userWalletId: UserWalletId, + sourceNetwork: ManagedCryptoCurrency.SourceNetwork, + ): CurrencyUnsupportedState? } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CurrencyUnsupportedDialog.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CurrencyUnsupportedDialog.kt new file mode 100644 index 0000000000..659b007c29 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/CurrencyUnsupportedDialog.kt @@ -0,0 +1,22 @@ +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.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun CurrencyUnsupportedDialog(title: TextReference, message: TextReference, onDismiss: () -> Unit) { + BasicDialog( + title = title.resolveReference(), + message = message.resolveReference(), + 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/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 3282da26fe..835316aa30 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 @@ -6,13 +6,11 @@ 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.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase -import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext -import com.tangem.domain.managetokens.model.ManageTokensListConfig -import com.tangem.domain.managetokens.model.ManageTokensUpdateAction -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase +import com.tangem.domain.managetokens.model.* import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -39,6 +37,7 @@ internal class ManageTokensListManager @Inject constructor( private val getManagedTokensUseCase: GetManagedTokensUseCase, private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, + private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, ) : ManageTokensUiActions { @@ -223,4 +222,32 @@ internal class ManageTokensListManager @Inject constructor( false } } + + override suspend fun checkCurrencyUnsupportedState( + userWalletId: UserWalletId, + sourceNetwork: ManagedCryptoCurrency.SourceNetwork, + ): CurrencyUnsupportedState? { + return checkCurrencyUnsupportedUseCase( + userWalletId = userWalletId, + sourceNetwork = sourceNetwork, + ).getOrElse { + Timber.e( + it, + """ + Failed to check currency unsupported state + |- User wallet ID: $userWalletId + |- Source Network: $sourceNetwork + """.trimIndent(), + ) + + val message = SnackbarMessage( + message = it.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + null + } + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt index 7b7548289a..d5e583482e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -1,5 +1,6 @@ package com.tangem.features.managetokens.utils.list +import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -15,4 +16,9 @@ internal interface ManageTokensUiActions { fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean + + suspend fun checkCurrencyUnsupportedState( + userWalletId: UserWalletId, + sourceNetwork: ManagedCryptoCurrency.SourceNetwork, + ): CurrencyUnsupportedState? } \ 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 1cc07f7286..b43eb3542c 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 @@ -1,10 +1,15 @@ package com.tangem.features.managetokens.utils.list import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.dialog.CurrencyUnsupportedDialog import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning import com.tangem.features.managetokens.ui.dialog.HideTokenWarning import com.tangem.features.managetokens.utils.mapper.toUiModel @@ -143,7 +148,13 @@ internal class ManageTokensUiManager( if (currency !is ManagedCryptoCurrency.Token) return@launch if (isSelected) { - actions.addCurrency(batchKey, currency, source.network) + val userWalletId = state.value.userWalletId + val unsupportedState = userWalletId?.let { actions.checkCurrencyUnsupportedState(it, source) } + if (unsupportedState != null) { + showUnsupportedWarning(unsupportedState) + } else { + actions.addCurrency(batchKey, currency, source.network) + } } else { if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) { showRemoveNetworkWarning( @@ -160,6 +171,31 @@ internal class ManageTokensUiManager( } } + private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { + val message = ContentMessage { onDismiss -> + CurrencyUnsupportedDialog( + title = resourceReference(R.string.common_warning), + message = when (unsupportedState) { + is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + }, + onDismiss = onDismiss, + ) + } + + messageSender.send(message) + } + private suspend fun showRemoveNetworkWarning( currency: ManagedCryptoCurrency, network: Network, From b9d30a4d55e99e483b11be3981a657518578a017 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 17:31:16 +0300 Subject: [PATCH 129/197] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 ++- .../domain/tokens/FetchTokenListUseCase.kt | 24 ++++++++++++++++++- .../intents/WalletWarningsClickIntents.kt | 5 +++- 3 files changed, 29 insertions(+), 3 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 47d32be700..63b8864a62 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 @@ -39,8 +39,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): FetchTokenListUseCase { - return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) + return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) } @Provides diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 0fe7e8b345..394cd7c1dc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -24,12 +25,14 @@ import kotlinx.coroutines.coroutineScope * @param currenciesRepository The repository for retrieving currency-related data. * @param networksRepository The repository for retrieving network-related data. * @param quotesRepository The repository for retrieving cryptocurrency quotes. + * @param stakingRepository The repository for retrieving staking-related data. */ // TODO: Add tests class FetchTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, + private val stakingRepository: StakingRepository, ) { /** @@ -59,7 +62,15 @@ class FetchTokenListUseCase( ) } - awaitAll(fetchStatuses, fetchQuotes) + val yieldBalances = async { + fetchYieldBalances( + userWalletId = userWalletId, + currencies = currencies, + refresh = refresh, + ) + } + + awaitAll(fetchStatuses, fetchQuotes, yieldBalances) } } } @@ -98,4 +109,15 @@ class FetchTokenListUseCase( /* Ignore error */ } } + + private suspend fun fetchYieldBalances( + userWalletId: UserWalletId, + currencies: List, + refresh: Boolean, + ) { + catch( + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, + catch = { /* Ignore error */ }, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 0e557c8e46..6798d988ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -126,7 +126,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( userWalletId = userWallet.walletId, currencies = missedAddressCurrencies, ) - .onRight { fetchTokenListUseCase(userWalletId = userWallet.walletId) } + .onRight { + // Refresh must be set to true to ensure that yield balances are updated + fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } .onLeft { Timber.e("Failed to derive public keys: $it") } } } From f209babd03cbf3d2420dc886ac63910b294b9dd9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 00:23:39 +0500 Subject: [PATCH 130/197] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 12 ++++++++++++ .../data/staking/DefaultStakingRepository.kt | 15 +++++++++++---- .../domain/staking/IsAnyTokenStakedUseCase.kt | 18 ++++++++++++++++++ .../staking/repositories/StakingRepository.kt | 2 ++ .../tokens/repository/MockStakingRepository.kt | 2 ++ .../impl/presentation/state/StakingUiState.kt | 1 + .../previewdata/InitialStakingStatePreview.kt | 1 + .../SetInitialDataStateTransformer.kt | 5 ++++- .../ui/StakingInitialInfoContent.kt | 2 +- .../presentation/viewmodel/StakingViewModel.kt | 3 +++ 10 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/IsAnyTokenStakedUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index a4b7ff17e4..388a7df7b7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -168,4 +168,16 @@ internal object StakingDomainModule { stakingErrorResolver = stakingErrorResolver, ) } + + @Provides + @Singleton + fun provideIsAnyTokenStakedUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): IsAnyTokenStakedUseCase { + return IsAnyTokenStakedUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } } \ 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 c17d722c51..65b43686b3 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 @@ -37,10 +37,7 @@ import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata -import com.tangem.domain.staking.model.stakekit.NetworkType -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionType @@ -516,6 +513,16 @@ internal class DefaultStakingRepository( } } + override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { + return withContext(dispatchers.io) { + stakingBalanceStore.getSyncOrNull(userWalletId) + ?.let { + it.isNotEmpty() && it.any { yieldBalance -> yieldBalance.balances.isNotEmpty() } + } + ?: false + } + } + private suspend fun createActionRequestBody( userWalletId: UserWalletId, network: Network, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsAnyTokenStakedUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsAnyTokenStakedUseCase.kt new file mode 100644 index 0000000000..33f4c0bc1d --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/IsAnyTokenStakedUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking + +import arrow.core.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.wallets.models.UserWalletId + +class IsAnyTokenStakedUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + suspend operator fun invoke(userWalletId: UserWalletId): Either { + return Either + .catch { stakingRepository.isAnyTokenStaked(userWalletId) } + .mapLeft { stakingErrorResolver.resolve(it) } + } +} \ No newline at end of file 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 f95af53048..aa35ac2b58 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 @@ -83,4 +83,6 @@ interface StakingRepository { /** Returns staking approval */ fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval + + suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean } \ 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 3000900a10..f9a6856ac9 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 @@ -252,4 +252,6 @@ class MockStakingRepository : StakingRepository { } override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty + + override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean = false } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index bdf349c499..ca9f2e192b 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 @@ -57,6 +57,7 @@ internal sealed class StakingStates { sealed class InitialInfoState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val showBanner: Boolean, val infoItems: ImmutableList, val aprRange: TextReference, val onInfoClick: (InfoType) -> Unit, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index c23cb74fda..72872ce0da 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -15,6 +15,7 @@ import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { val defaultState = StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = true, + showBanner = true, aprRange = stringReference("2.54-5.12%"), infoItems = persistentListOf( RoundedListWithDividersItemData( 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 f07d45346e..1156fc73a4 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 @@ -36,6 +36,7 @@ internal class SetInitialDataStateTransformer( private val clickIntents: StakingClickIntents, private val yield: Yield, private val isApprovalNeeded: Boolean, + private val isAnyTokenStaked: Boolean, private val cryptoCurrencyStatusProvider: Provider, private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, @@ -79,12 +80,14 @@ internal class SetInitialDataStateTransformer( } private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { + val yieldBalance = yieldBalancesConverter.convert(Unit) return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = !cryptoCurrencyStatusProvider().value.amount.isNullOrZero(), + showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, aprRange = getAprRange(yield.validators), infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, - yieldBalance = yieldBalancesConverter.convert(Unit), + yieldBalance = yieldBalance, ) } 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 530716b3fe..c4dbbd814e 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 @@ -75,7 +75,7 @@ internal fun StakingInitialInfoContent( .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - if (state.yieldBalance == InnerYieldBalanceState.Empty) { + if (state.showBanner) { item(key = BANNER_BLOCK_KEY) { Column( modifier = Modifier.animateItemPlacement(), 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 43da4818a4..6e833d5ebb 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 @@ -105,6 +105,7 @@ internal class StakingViewModel @Inject constructor( private val validateTransactionUseCase: ValidateTransactionUseCase, private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -713,6 +714,7 @@ internal class StakingViewModel @Inject constructor( getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( ifRight = { feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, it).getOrNull() + val isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false cryptoCurrencyStatus = it setupApprovalNeeded() @@ -723,6 +725,7 @@ internal class StakingViewModel @Inject constructor( clickIntents = this@StakingViewModel, yield = yield, isApprovalNeeded = stakingApproval is StakingApproval.Needed, + isAnyTokenStaked = isAnyTokenStaked, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider { appCurrency }, From 42a1d544f53caac04463ba52704631f7c2a2021e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Sep 2024 12:54:55 +0300 Subject: [PATCH 131/197] Updated on 2026-08-14 --- .../impl/presentation/ui/StakingConfirmationContent.kt | 9 +++++---- .../impl/presentation/ui/block/StakingFeeBlock.kt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 128ff53e50..701e97a0ce 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -37,6 +37,7 @@ internal fun StakingConfirmationContent( type: StakingActionCommonType, ) { if (state !is StakingStates.ConfirmationState.Data) return + val isEnterAction = type == StakingActionCommonType.ENTER Column( modifier = Modifier @@ -58,11 +59,11 @@ internal fun StakingConfirmationContent( } AmountBlock( amountState = amountState, - isClickDisabled = true, - isEditingDisabled = true, - onClick = {}, + isClickDisabled = !isEnterAction, + isEditingDisabled = !isEnterAction, + onClick = clickIntents::onPrevClick, ) - if (type == StakingActionCommonType.ENTER) { + if (isEnterAction) { ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators) } StakingFeeBlock(feeState = state.feeState) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index fdca1be81a..2efa78b2d0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -34,7 +34,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) + .background(TangemTheme.colors.button.disabled) .padding(TangemTheme.dimens.spacing12), ) { Text( From c5fdc17dd4cc8fea2212fc242c31ba331aacc4de Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 16:03:47 +0300 Subject: [PATCH 132/197] Updated on 2026-08-14 --- .../features/details/ui/cardsettings/CardSettingsViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) 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 4abe4b37cc..9af2855279 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 @@ -160,6 +160,8 @@ internal class CardSettingsViewModel @Inject constructor( } if (scanResponse.cardTypesResolver.isTangemTwins()) { + // needs to prepare twin state if it's twin cards (depends on onboarding refactoring) + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) cardSettingsInteractor.clear() From de1acdf0eb947658636ac7dd0e9684daeca96e80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 16:04:06 +0300 Subject: [PATCH 133/197] Updated on 2026-08-14 --- .../products/twins/ui/OnboardingTwinsFragment.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index eda118c3dc..76880fb5e1 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -317,16 +317,16 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { when (previousStep) { - TwinCardsStep.None -> { + TwinCardsStep.None, TwinCardsStep.Welcome -> { when (state.cardNumber) { TwinCardNumber.First -> { - twinsWidget.leapfrogWidget.unfold(false) { - twinsWidget.toActivate(false) + twinsWidget.leapfrogWidget.unfold(true) { + twinsWidget.toActivate(true) } } TwinCardNumber.Second -> { - switchToCard(state.cardNumber, false) { - twinsWidget.toActivate(false) + switchToCard(state.cardNumber, true) { + twinsWidget.toActivate(true) } } else -> {} From a74c3787e7f3ec1b504a62aa89c5de6b8b1893c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 11:25:19 +0300 Subject: [PATCH 134/197] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 9 +++++++- .../com/tangem/core/analytics/models/Basic.kt | 2 +- .../analytics/TokenExchangeAnalyticsEvent.kt | 23 +++++++++++++------ .../analytics/SendAnalyticEvents.kt | 10 ++++---- .../viewmodels/ExchangeStatusFactory.kt | 15 +++++------- .../analytics/WalletScreenAnalyticsEvent.kt | 2 +- 6 files changed, 37 insertions(+), 24 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 093769910b..fd070196c9 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -131,7 +131,7 @@ sealed class AnalyticsParam { companion object Key { const val BLOCKCHAIN = "blockchain" - const val TOKEN = "Token" + const val TOKEN_PARAM = "Token" const val SOURCE = "Source" const val BALANCE = "Balance" const val STATE = "State" @@ -152,5 +152,12 @@ sealed class AnalyticsParam { const val VALIDATION = "Validation" const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" const val BLOCKCHAIN_SELECTED_HOST = "selected_host" + + // region swap + const val TOKEN_CATEGORY = "Token" + const val STATUS = "Status" + const val PROVIDER = "Provider" + const val PLACE = "Place" + // } } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 9f39ae5bdd..6fb52dfe29 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -55,7 +55,7 @@ sealed class Basic( this[AnalyticsParam.SOURCE] = sentFrom.value if (sentFrom is AnalyticsParam.TxData) { this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain - this[AnalyticsParam.TOKEN] = sentFrom.token + this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value } if (sentFrom is AnalyticsParam.TxSentFrom.Approve) { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt index b89306aded..ae26e772f5 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt @@ -1,34 +1,43 @@ package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.PLACE +import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER +import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_CATEGORY +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM class TokenExchangeAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Token", event, params, null) { +) : AnalyticsEvent(TOKEN_CATEGORY, event, params, null) { class CexTxStatusOpened(token: String) : TokenScreenAnalyticsEvent( event = "Swap Status Opened", - params = mapOf("Token" to token), + params = mapOf(TOKEN_PARAM to token), ) - class CexTxStatusChanged(token: String, status: String) : TokenScreenAnalyticsEvent( + class CexTxStatusChanged(token: String, status: String, provider: String) : TokenScreenAnalyticsEvent( event = "Swap Status", - params = mapOf("Token" to token, "Status" to status), + params = mapOf( + TOKEN_PARAM to token, + STATUS to status, + PROVIDER to provider, + ), ) class GoToProviderStatus(token: String) : TokenScreenAnalyticsEvent( event = "Button - Go To Provider", - params = mapOf("Token" to token, "Place" to "Status"), + params = mapOf(TOKEN_PARAM to token, PLACE to "Status"), ) class GoToProviderKYC(token: String) : TokenScreenAnalyticsEvent( event = "Button - Go To Provider", - params = mapOf("Token" to token, "Place" to "KYC"), + params = mapOf(TOKEN_PARAM to token, PLACE to "KYC"), ) class GoToProviderFail(token: String) : TokenScreenAnalyticsEvent( event = "Button - Go To Provider", - params = mapOf("Token" to token, "Place" to "Fail"), + params = mapOf(TOKEN_PARAM to token, PLACE to "Fail"), ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt index c4ce202599..03fb73beab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt @@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE -import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION import com.tangem.core.analytics.models.AnalyticsParam.OnOffState @@ -105,7 +105,7 @@ internal sealed class SendAnalyticEvents( ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", params = mapOf( - TOKEN to token, + TOKEN_PARAM to token, FEE_TYPE to feeType.value, ), ) @@ -119,13 +119,13 @@ internal sealed class SendAnalyticEvents( /** If not enough fee notification is present */ data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SendAnalyticEvents( event = "Notice - Not Enough Fee", - params = mapOf(TOKEN to token, BLOCKCHAIN to blockchain), + params = mapOf(TOKEN_PARAM to token, BLOCKCHAIN to blockchain), ) /** If transaction delays notification is present */ data class NoticeTransactionDelays(val token: String) : SendAnalyticEvents( event = "Notice - Transaction Delays Are Possible", - params = mapOf(TOKEN to token), + params = mapOf(TOKEN_PARAM to token), ) data object NoticeFeeCoverage : SendAnalyticEvents( @@ -135,7 +135,7 @@ internal sealed class SendAnalyticEvents( /** If error occurs during send transactions */ data class TransactionError(val token: String) : SendAnalyticEvents( event = "Error - Transaction Rejected", - params = mapOf(TOKEN to token), + params = mapOf(TOKEN_PARAM to token), ) // endregion } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 67920c922a..aa63ffd144 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -13,10 +13,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.ExchangeStatus -import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel -import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter @@ -109,7 +106,7 @@ internal class ExchangeStatusFactory( suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) { swapTxList.map { tx -> async { - val statusModel = getExchangeStatus(tx.txId) + val statusModel = getExchangeStatus(tx.txId, tx.provider) val isRefundTerminalStatus = statusModel?.refundNetwork == null && statusModel?.refundContractAddress == null && tx.provider.type != ExchangeProviderType.DEX_BRIDGE @@ -130,26 +127,26 @@ internal class ExchangeStatusFactory( .toPersistentList() } - private suspend fun getExchangeStatus(txId: String): ExchangeStatusModel? { + private suspend fun getExchangeStatus(txId: String, provider: SwapProvider): ExchangeStatusModel? { return swapRepository.getExchangeStatus(txId) .fold( ifLeft = { null }, ifRight = { statusModel -> - sendStatusUpdateAnalytics(statusModel) + sendStatusUpdateAnalytics(statusModel, provider) swapTransactionRepository.storeTransactionState(txId, statusModel) statusModel }, ) } - private suspend fun sendStatusUpdateAnalytics(statusModel: ExchangeStatusModel) { + private suspend fun sendStatusUpdateAnalytics(statusModel: ExchangeStatusModel, provider: SwapProvider) { val txId = statusModel.txId ?: return val status = toAnalyticStatus(statusModel.status) ?: return val savedStatus = swapTransactionStatusStore.getTransactionStatus(txId) if (savedStatus != status) { analyticsEventsHandlerProvider().send( - TokenExchangeAnalyticsEvent.CexTxStatusChanged(cryptoCurrency.symbol, status.value), + TokenExchangeAnalyticsEvent.CexTxStatusChanged(cryptoCurrency.symbol, status.value, provider.name), ) swapTransactionStatusStore.setTransactionStatus(txId, status) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index de6ddf8b1b..c0021f5d7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -43,7 +43,7 @@ sealed class WalletScreenAnalyticsEvent { event = "Token Balance", params = mapOf( AnalyticsParam.STATE to balance.value, - AnalyticsParam.TOKEN to token, + AnalyticsParam.TOKEN_PARAM to token, ), ) } From e49a9befc7d7d282f724b759b17ef65320b58c3e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 10:10:35 +0300 Subject: [PATCH 135/197] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 2 +- .../tap/di/domain/StakingDomainModule.kt | 13 +-- data/staking/build.gradle.kts | 1 + .../data/staking/DefaultStakingRepository.kt | 59 +------------ ...DefaultStakingTransactionHashRepository.kt | 83 +++++++++++++++++++ .../data/staking/di/StakingDataModule.kt | 26 ++++-- .../staking/SaveUnsubmittedHashUseCase.kt | 6 +- .../staking/SendUnsubmittedHashesUseCase.kt | 6 +- .../domain/staking/SubmitHashUseCase.kt | 6 +- .../staking/repositories/StakingRepository.kt | 7 -- .../StakingTransactionHashRepository.kt | 12 +++ .../repository/MockStakingRepository.kt | 13 --- 12 files changed, 137 insertions(+), 97 deletions(-) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/DefaultStakingTransactionHashRepository.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingTransactionHashRepository.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index f9888910fa..7735974a6d 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -583,8 +583,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun sendStakingUnsubmittedHashes() { lifecycleScope.launch { sendUnsubmittedHashesUseCase.invoke() - .onRight { Timber.d("Submitting hashes succeeded") } .onLeft { Timber.e(it.toString()) } + .onRight { Timber.d("Submitting hashes succeeded") } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 388a7df7b7..2c64513e3b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.repositories.StakingTransactionHashRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -112,11 +113,11 @@ internal object StakingDomainModule { @Provides @Singleton fun provideSubmitHashUseCase( - stakingRepository: StakingRepository, + stakingTransactionHashRepository: StakingTransactionHashRepository, stakingErrorResolver: StakingErrorResolver, ): SubmitHashUseCase { return SubmitHashUseCase( - stakingRepository = stakingRepository, + stakingTransactionHashRepository = stakingTransactionHashRepository, stakingErrorResolver = stakingErrorResolver, ) } @@ -124,11 +125,11 @@ internal object StakingDomainModule { @Provides @Singleton fun provideSaveUnsubmittedHashUseCase( - stakingRepository: StakingRepository, + stakingTransactionHashRepository: StakingTransactionHashRepository, stakingErrorResolver: StakingErrorResolver, ): SaveUnsubmittedHashUseCase { return SaveUnsubmittedHashUseCase( - stakingRepository = stakingRepository, + stakingTransactionHashRepository = stakingTransactionHashRepository, stakingErrorResolver = stakingErrorResolver, ) } @@ -136,11 +137,11 @@ internal object StakingDomainModule { @Provides @Singleton fun provideSendUnsubmittedHashesUseCase( - stakingRepository: StakingRepository, + stakingTransactionHashRepository: StakingTransactionHashRepository, stakingErrorResolver: StakingErrorResolver, ): SendUnsubmittedHashesUseCase { return SendUnsubmittedHashesUseCase( - stakingRepository = stakingRepository, + stakingTransactionHashRepository = stakingTransactionHashRepository, stakingErrorResolver = stakingErrorResolver, ) } diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 4a9ba3d819..f79c7c6109 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(deps.timber) implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) 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 65b43686b3..f1c4732032 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 @@ -26,9 +26,6 @@ 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 import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow @@ -36,7 +33,10 @@ import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata +import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -54,7 +54,6 @@ import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.lib.crypto.BlockchainUtils.isSolana -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -62,7 +61,6 @@ import kotlinx.coroutines.withContext @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, - private val appPreferencesStore: AppPreferencesStore, private val stakingYieldsStore: StakingYieldsStore, private val stakingBalanceStore: StakingBalanceStore, private val cacheRegistry: CacheRegistry, @@ -464,55 +462,6 @@ internal class DefaultStakingRepository( } } - override suspend fun submitHash(transactionId: String, transactionHash: String) { - withContext(dispatchers.io) { - stakeKitApi.submitTransactionHash( - transactionId = transactionId, - body = SubmitTransactionHashRequestBody( - hash = transactionHash, - ), - ) - } - } - - override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) { - withContext(dispatchers.io) { - appPreferencesStore.editData { preferences -> - val savedTransactions = preferences.getObjectListOrDefault( - key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, - default = emptyList(), - ) - - preferences.setObjectList( - key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, - value = savedTransactions + unsubmittedTransactionMetadata, - ) - } - } - } - - override suspend fun sendUnsubmittedHashes() { - withContext(NonCancellable) { - val savedTransactions = appPreferencesStore.getObjectListSync( - key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, - ) - - savedTransactions.forEach { - stakeKitApi.submitTransactionHash( - transactionId = it.transactionId, - body = SubmitTransactionHashRequestBody(hash = it.transactionHash), - ) - } - - appPreferencesStore.editData { mutablePreferences -> - mutablePreferences.setObjectList( - key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, - value = emptyList(), - ) - } - } - } - override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { return withContext(dispatchers.io) { stakingBalanceStore.getSyncOrNull(userWalletId) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingTransactionHashRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingTransactionHashRepository.kt new file mode 100644 index 0000000000..0d12489648 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingTransactionHashRepository.kt @@ -0,0 +1,83 @@ +package com.tangem.data.staking + +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.request.* +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectListSync +import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata +import com.tangem.domain.staking.repositories.StakingTransactionHashRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber + +internal class DefaultStakingTransactionHashRepository( + private val stakeKitApi: StakeKitApi, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : StakingTransactionHashRepository { + + override suspend fun submitHash(transactionId: String, transactionHash: String) { + withContext(dispatchers.io) { + stakeKitApi.submitTransactionHash( + transactionId = transactionId, + body = SubmitTransactionHashRequestBody( + hash = transactionHash, + ), + ) + } + } + + override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) { + withContext(dispatchers.io) { + appPreferencesStore.editData { preferences -> + val savedTransactions = preferences.getObjectListOrDefault( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + default = emptyList(), + ) + + preferences.setObjectList( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + value = savedTransactions + unsubmittedTransactionMetadata, + ) + } + } + } + + override suspend fun sendUnsubmittedHashes() { + withContext(dispatchers.io) { + val savedTransactions = appPreferencesStore.getObjectListSync( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + ) + + savedTransactions.forEach { transaction -> + try { + stakeKitApi.submitTransactionHash( + transactionId = transaction.transactionId, + body = SubmitTransactionHashRequestBody(hash = transaction.transactionHash), + ) + + appPreferencesStore.editData { mutablePreferences -> + val updatedTransactions = + mutablePreferences.getObjectListOrDefault( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + default = emptyList(), + ).filterNot { it.transactionId == transaction.transactionId } + + mutablePreferences.setObjectList( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + value = updatedTransactions, + ) + } + } catch (e: Exception) { + val logMessage = buildString { + append("Error while submitting transaction with\n") + append("StakeKit id = ${transaction.transactionId} and\n") + append("transaction hash = ${transaction.transactionHash}") + } + Timber.e(logMessage) + } + } + } + } +} \ No newline at end of file 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 fc2baff6dd..cb8d518895 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 @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.DefaultStakingErrorResolver import com.tangem.data.staking.DefaultStakingRepository +import com.tangem.data.staking.DefaultStakingTransactionHashRepository import com.tangem.data.staking.converters.error.StakeKitErrorConverter import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse @@ -13,6 +14,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.repositories.StakingTransactionHashRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles @@ -31,23 +33,21 @@ internal object StakingDataModule { @Singleton fun provideStakingRepository( stakeKitApi: StakeKitApi, - appPreferencesStore: AppPreferencesStore, - stakingTokenStore: StakingYieldsStore, + stakingYieldsStore: StakingYieldsStore, stakingBalanceStore: StakingBalanceStore, + cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, stakingFeatureToggle: StakingFeatureToggles, - cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, getUserWalletUseCase: GetUserWalletUseCase, @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, - appPreferencesStore = appPreferencesStore, - stakingYieldsStore = stakingTokenStore, + stakingYieldsStore = stakingYieldsStore, stakingBalanceStore = stakingBalanceStore, - dispatchers = dispatchers, cacheRegistry = cacheRegistry, + dispatchers = dispatchers, stakingFeatureToggle = stakingFeatureToggle, walletManagersFacade = walletManagersFacade, getUserWalletUseCase = getUserWalletUseCase, @@ -55,6 +55,20 @@ internal object StakingDataModule { ) } + @Provides + @Singleton + fun StakingTransactionHashRepository( + stakeKitApi: StakeKitApi, + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): StakingTransactionHashRepository { + return DefaultStakingTransactionHashRepository( + stakeKitApi = stakeKitApi, + appPreferencesStore = appPreferencesStore, + dispatchers = dispatchers, + ) + } + @Provides @Singleton internal fun provideStakingErrorResolver(@NetworkMoshi moshi: Moshi): StakingErrorResolver { diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt index 0a83c395cb..abfeab0d56 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt @@ -4,19 +4,19 @@ import arrow.core.Either import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.repositories.StakingTransactionHashRepository /** * Use case for saving hash that failed to submit during staking confirmation */ class SaveUnsubmittedHashUseCase( - private val stakingRepository: StakingRepository, + private val stakingTransactionHashRepository: StakingTransactionHashRepository, private val stakingErrorResolver: StakingErrorResolver, ) { suspend operator fun invoke(transactionId: String, transactionHash: String): Either { return Either.catch { - stakingRepository.storeUnsubmittedHash( + stakingTransactionHashRepository.storeUnsubmittedHash( unsubmittedTransactionMetadata = UnsubmittedTransactionMetadata( transactionId = transactionId, transactionHash = transactionHash, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt index 95f3ad9eea..3564651d9f 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt @@ -3,19 +3,19 @@ package com.tangem.domain.staking import arrow.core.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.staking.repositories.StakingTransactionHashRepository /** * Use case for commiting hashes that failed to submit during staking confirmation */ class SendUnsubmittedHashesUseCase( - private val stakingRepository: StakingRepository, + private val stakingTransactionHashRepository: StakingTransactionHashRepository, private val stakingErrorResolver: StakingErrorResolver, ) { suspend operator fun invoke(): Either { return Either - .catch { stakingRepository.sendUnsubmittedHashes() } + .catch { stakingTransactionHashRepository.sendUnsubmittedHashes() } .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt index d810cfac8c..83d1b151b7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt @@ -3,20 +3,20 @@ package com.tangem.domain.staking import arrow.core.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.staking.repositories.StakingTransactionHashRepository /** * Use case for submitting transaction hash to stakekit */ class SubmitHashUseCase( - private val stakingRepository: StakingRepository, + private val stakingTransactionHashRepository: StakingTransactionHashRepository, private val stakingErrorResolver: StakingErrorResolver, ) { suspend fun submitHash(transactionId: String, transactionHash: String): Either { return Either .catch { - stakingRepository.submitHash( + stakingTransactionHashRepository.submitHash( transactionId = transactionId, transactionHash = transactionHash, ) 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 aa35ac2b58..ba93d3e3cb 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 @@ -6,7 +6,6 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList @@ -75,12 +74,6 @@ interface StakingRepository { transactionId: String, ): Pair - suspend fun submitHash(transactionId: String, transactionHash: String) - - suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) - - suspend fun sendUnsubmittedHashes() - /** Returns staking approval */ fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingTransactionHashRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingTransactionHashRepository.kt new file mode 100644 index 0000000000..be75380a84 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingTransactionHashRepository.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.staking.repositories + +import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata + +interface StakingTransactionHashRepository { + + suspend fun submitHash(transactionId: String, transactionHash: String) + + suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) + + suspend fun sendUnsubmittedHashes() +} \ 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 f9a6856ac9..cc3fc8d8b2 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 @@ -8,7 +8,6 @@ import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus @@ -239,18 +238,6 @@ class MockStakingRepository : StakingRepository { status = TransactionStatus.Unconfirmed, ) - override suspend fun submitHash(transactionId: String, transactionHash: String) { - /* no-op */ - } - - override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) { - /* no-op */ - } - - override suspend fun sendUnsubmittedHashes() { - /* no-op */ - } - override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean = false From e6dc5b37a2a14154efc8238354792b692af0181d Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 7 Sep 2024 11:46:07 +0300 Subject: [PATCH 136/197] Updated on 2026-08-14 --- .../tangem/common/ui/amountScreen/AmountScreenContent.kt | 8 ++++---- .../common/ui/amountScreen/ui/AmountFieldContainer.kt | 4 ++-- .../features/send/impl/presentation/ui/SendScreen.kt | 4 ++-- .../impl/presentation/ui/StakingInitialInfoContent.kt | 2 ++ .../staking/impl/presentation/ui/StakingScreen.kt | 3 ++- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index 64615bc792..4f90184a2b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -20,11 +20,11 @@ import com.tangem.core.ui.res.TangemThemePreview /** * Amount screen with field * @param amountState amount state - * @param isBalanceHiding flag hidden balances + * @param isBalanceHidden flag hidden balances * @param clickIntents amount screen clicks */ @Composable -fun AmountScreenContent(amountState: AmountState, isBalanceHiding: Boolean, clickIntents: AmountScreenClickIntents) { +fun AmountScreenContent(amountState: AmountState, isBalanceHidden: Boolean, clickIntents: AmountScreenClickIntents) { if (amountState !is AmountState.Data) return // Do not put fillMaxSize() in here @@ -37,7 +37,7 @@ fun AmountScreenContent(amountState: AmountState, isBalanceHiding: Boolean, clic bottom = TangemTheme.dimens.spacing16, ), ) { - amountField(amountState = amountState, isBalanceHiding = isBalanceHiding) + amountField(amountState = amountState, isBalanceHidden = isBalanceHidden) buttons( segmentedButtonConfig = amountState.segmentedButtonConfig, clickIntents = clickIntents, @@ -57,7 +57,7 @@ private fun SendAmountContentPreview( TangemThemePreview { AmountScreenContent( amountState = amountState, - isBalanceHiding = false, + isBalanceHidden = false, clickIntents = AmountScreenClickIntentsStub, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 146c491074..a5017f62a1 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -22,7 +22,7 @@ private const val AMOUNT_FIELD_KEY = "amountFieldKey" internal fun LazyListScope.amountField( amountState: AmountState.Data, - isBalanceHiding: Boolean, + isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { item(key = AMOUNT_FIELD_KEY) { @@ -41,7 +41,7 @@ internal fun LazyListScope.amountField( .padding(top = TangemTheme.dimens.spacing14), ) - val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference() + val balance = if (isBalanceHidden) STARS else amountState.walletBalance.resolveReference() AnimatedContent( targetState = balance, label = "Hide Balance Animation", diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 992da60b9f..2c4304444f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -163,12 +163,12 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS when (state.type) { SendUiStateType.Amount -> AmountScreenContent( amountState = uiState.amountState, - isBalanceHiding = uiState.isBalanceHidden, + isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) SendUiStateType.EditAmount -> AmountScreenContent( amountState = uiState.editAmountState!!, - isBalanceHiding = uiState.isBalanceHidden, + isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) SendUiStateType.Recipient -> SendRecipientContent( 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 c4dbbd814e..c536e37fad 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 @@ -65,6 +65,7 @@ internal fun StakingInitialInfoContent( state: StakingStates.InitialInfoState, buttonState: NavigationButtonsState, clickIntents: StakingClickIntents, + isBalanceHidden: Boolean, ) { if (state !is StakingStates.InitialInfoState.Data) return @@ -321,6 +322,7 @@ private fun StakingInitialInfoContent_Preview( state = feeState, buttonState = NavigationButtonsState.Empty, clickIntents = StakingClickIntentsStub, + isBalanceHidden = false, ) } } 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 48b48cd7a0..55ea17c9a9 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 @@ -157,6 +157,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M state = uiState.initialInfoState, buttonState = uiState.buttonsState, clickIntents = uiState.clickIntents, + isBalanceHidden = uiState.isBalanceHidden, ) StakingStep.RewardsValidators -> { StakingClaimRewardsValidatorContent( @@ -166,7 +167,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M } StakingStep.Amount -> AmountScreenContent( amountState = uiState.amountState, - isBalanceHiding = uiState.isBalanceHidden, + isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) StakingStep.Confirmation -> StakingConfirmationContent( From 0ad4588d2ce9326fab53c6ebf75e1b6aea4ed52e Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 7 Sep 2024 17:36:07 +0300 Subject: [PATCH 137/197] Updated on 2026-08-14 --- .../list/RoundedListWithDividers.kt | 5 +++- .../core/ui/extensions/TextReference.kt | 16 ++++++++++++- .../java/com/tangem/utils/StringsSigns.kt | 2 ++ .../com/tangem/utils/extensions/String.kt | 16 +++++++++++++ .../previewdata/InitialStakingStatePreview.kt | 1 + .../SetInitialDataStateTransformer.kt | 1 + .../ui/StakingInitialInfoContent.kt | 24 ++++++++++++++----- .../tokendetails/ui/TokenDetailsScreen.kt | 3 ++- .../components/staking/StakingBalanceBlock.kt | 14 +++++++---- .../components/staking/TokenStakingBlock.kt | 19 +++++++++++---- 10 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/String.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt index 336f35d454..641044c813 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -16,6 +16,7 @@ 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.utils.extensions.orHide import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -42,6 +43,7 @@ fun LazyListScope.roundedListWithDividersItems( rows: ImmutableList, headerContent: (@Composable () -> Unit)? = null, footerContent: (@Composable () -> Unit)? = null, + hideEndText: Boolean = false, ) { if (headerContent != null) { item(key = ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY) { @@ -55,7 +57,7 @@ fun LazyListScope.roundedListWithDividersItems( ) { index, row -> InitialInfoContentRow( startText = row.startText.resolveReference(), - endText = row.endText.resolveReference(), + endText = row.endText.resolveReference().orHide(hideEndText && row.isEndTextHideable), cornersToRound = getCornersToRound(index, rows.size), iconClick = row.iconClick, ) @@ -126,6 +128,7 @@ data class RoundedListWithDividersItemData( val startText: TextReference, val endText: TextReference, val iconClick: (() -> Unit)? = null, + val isEndTextHideable: Boolean = false, ) @Composable diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 4aab281036..02c18f0152 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString.Builder import androidx.compose.ui.text.buildAnnotatedString +import com.tangem.utils.StringsSigns.STARS import org.intellij.markdown.MarkdownElementTypes import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -250,4 +251,17 @@ private fun formatAnnotated(rawString: String): AnnotatedString { return buildAnnotatedString { appendMarkdown(markdownText = rawString, node = parsedTree) } -} \ No newline at end of file +} + +/** + * Returns the TextReference itself if hide is false, otherwise returns a reference with STARS. + * + * @param mask A boolean flag that determines whether to hide the string. + * If true, the original TextReference will be replaced by reference with STARS. + * If false, the original TextReference will be returned. + * + * @return The original reference if hide is false, or reference with STARS if hide is true. + */ +fun TextReference.orMaskWithStars(mask: Boolean) : TextReference { + return if (mask) stringReference(STARS) else this +} diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index b2d8906fb0..dce38bdc29 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -1,5 +1,7 @@ package com.tangem.utils +import com.tangem.utils.StringsSigns.STARS + object StringsSigns { const val STARS = "\u2217\u2217\u2217" diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/String.kt b/core/utils/src/main/java/com/tangem/utils/extensions/String.kt new file mode 100644 index 0000000000..9b2e8052f2 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/String.kt @@ -0,0 +1,16 @@ +package com.tangem.utils.extensions + +import com.tangem.utils.StringsSigns.STARS + +/** + * Returns the string itself if hide is false, otherwise returns a string of stars. + * + * @param hide A boolean flag that determines whether to hide the string. + * If true, the string will be hidden (replaced by `STARS`). + * If false, the original string will be returned. + * + * @return The original string if hide is false, or STARS if hide is true. + */ +fun String.orHide(hide: Boolean) : String { + return if (hide) STARS else this +} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 72872ce0da..77d30e2fb2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -22,6 +22,7 @@ internal object InitialStakingStatePreview { id = R.string.staking_details_available, startText = TextReference.Res(R.string.staking_details_available), endText = TextReference.Str("15 SOL"), + isEndTextHideable = true, ), RoundedListWithDividersItemData( id = R.string.staking_details_annual_percentage_rate, 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 1156fc73a4..35b98d0107 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 @@ -128,6 +128,7 @@ internal class SetInitialDataStateTransformer( decimals = cryptoCurrencyStatus.currency.decimals, ), ), + isEndTextHideable = true, ) } 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 c536e37fad..5701a7ed0c 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 @@ -51,6 +51,7 @@ import com.tangem.features.staking.impl.presentation.state.stub.StakingClickInte import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.PLUS +import com.tangem.utils.extensions.orHide import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList @@ -90,6 +91,7 @@ internal fun StakingInitialInfoContent( this.roundedListWithDividersItems( rows = state.infoItems, footerContent = { SpacerH12() }, + hideEndText = isBalanceHidden, ) if (state.yieldBalance is InnerYieldBalanceState.Data) { @@ -100,6 +102,7 @@ internal fun StakingInitialInfoContent( rewardFiat = state.yieldBalance.rewardsFiat, rewardBlockType = state.yieldBalance.rewardBlockType, onRewardsClick = clickIntents::openRewardsValidators, + isBalanceHidden = isBalanceHidden, ) SpacerH12() } @@ -109,7 +112,11 @@ internal fun StakingInitialInfoContent( if (state.yieldBalance is InnerYieldBalanceState.Data) { item(key = ACTIVE_STAKING_BLOCK_KEY) { Column(modifier = Modifier.animateItemPlacement()) { - ActiveStakingBlock(state.yieldBalance.balance, clickIntents::onActiveStake) + ActiveStakingBlock( + balances = state.yieldBalance.balance, + onClick = clickIntents::onActiveStake, + isBalanceHidden = isBalanceHidden, + ) SpacerH12() } } @@ -158,17 +165,18 @@ private fun StakingRewardBlock( rewardFiat: String, rewardBlockType: RewardBlockType, onRewardsClick: () -> Unit, + isBalanceHidden: Boolean, ) { val (text, textColor) = when (rewardBlockType) { RewardBlockType.Rewards -> { annotatedReference { append(PLUS) appendSpace() - append(rewardFiat) + append(rewardFiat.orHide(isBalanceHidden)) appendSpace() append(DOT) appendSpace() - append(rewardCrypto) + append(rewardCrypto.orHide(isBalanceHidden)) } to TangemTheme.colors.text.primary1 } RewardBlockType.RewardUnavailable -> { @@ -197,7 +205,11 @@ private fun StakingRewardBlock( } @Composable -private fun ActiveStakingBlock(balances: ImmutableList, onClick: (BalanceState) -> Unit) { +private fun ActiveStakingBlock( + balances: ImmutableList, + onClick: (BalanceState) -> Unit, + isBalanceHidden: Boolean, +) { Column( modifier = Modifier .fillMaxWidth() @@ -222,8 +234,8 @@ private fun ActiveStakingBlock(balances: ImmutableList, onClick: ( subtitle = balance.title, caption = balance.subtitle ?: balance.getAprText(), isGrayscaleImage = !balance.isClickable, - infoTitle = balance.fiatAmount, - infoSubtitle = balance.cryptoAmount, + infoTitle = balance.fiatAmount.orMaskWithStars(isBalanceHidden), + infoSubtitle = balance.cryptoAmount.orMaskWithStars(isBalanceHidden), imageUrl = balance.getImage(), iconRes = icon, iconTint = iconTint, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 8ced7dd13a..65fb047b37 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -169,8 +169,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon contentType = StakingBlockUM::class.java, content = { TokenStakingBlock( - modifier = itemModifier, state = state.stakingBlocksState, + isBalanceHidden = state.isBalanceHidden, + modifier = itemModifier, ) }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 8d40a79e42..50ef80a078 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -28,7 +29,11 @@ import com.tangem.features.tokendetails.impl.R import com.tangem.utils.StringsSigns @Composable -internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifier = Modifier) { +internal fun StakingBalanceBlock( + state: StakingBlockUM.Staked, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -54,7 +59,7 @@ internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifie ) Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { Text( - text = state.fiatValue.resolveReference(), + text = state.fiatValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) @@ -64,14 +69,14 @@ internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifie color = TangemTheme.colors.text.primary1, ) Text( - text = state.cryptoValue.resolveReference(), + text = state.cryptoValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) } if (state.rewardValue != TextReference.EMPTY) { Text( - text = state.rewardValue.resolveReference(), + text = state.rewardValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -95,6 +100,7 @@ private fun StakingBalanceBlock_Preview( TangemThemePreview { StakingBalanceBlock( state = data, + isBalanceHidden = false, modifier = Modifier.padding(TangemTheme.dimens.spacing16), ) } 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 9d0087292d..366021ea3f 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 @@ -33,11 +33,16 @@ import com.tangem.features.tokendetails.impl.R /** * Token staking block * - * @param state component state - * @param modifier modifier + * @param state component state + * @param isBalanceHidden whether to hide balance + * @param modifier modifier */ @Composable -internal fun TokenStakingBlock(state: StakingBlockUM, modifier: Modifier = Modifier) { +internal fun TokenStakingBlock( + state: StakingBlockUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { AnimatedContent( targetState = state, contentAlignment = Alignment.CenterStart, @@ -51,6 +56,7 @@ internal fun TokenStakingBlock(state: StakingBlockUM, modifier: Modifier = Modif ) is StakingBlockUM.Staked -> StakingBalanceBlock( state = it, + isBalanceHidden = isBalanceHidden, modifier = modifier, ) is StakingBlockUM.StakeAvailable -> StakingAvailableContent( @@ -124,7 +130,7 @@ private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) .heightIn(min = TangemTheme.dimens.size72) .padding(all = TangemTheme.dimens.spacing12), - ) { + ) { Row { val (alpha, colorFilter) = remember(iconState.isGrayscale) { getGreyScaleColorFilter(iconState.isGrayscale) @@ -172,7 +178,10 @@ private fun Preview_TokenStakingBlock( state: StakingBlockUM, ) { TangemThemePreview { - TokenStakingBlock(state = state) + TokenStakingBlock( + state = state, + isBalanceHidden = false + ) } } From fbf91e4c6dd107dffc8b7bf6d4ab4cdec3bdaed8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 7 Sep 2024 17:37:25 +0300 Subject: [PATCH 138/197] Updated on 2026-08-14 --- .../com/tangem/core/ui/extensions/TextReference.kt | 4 ++-- .../src/main/java/com/tangem/utils/StringsSigns.kt | 2 -- .../main/java/com/tangem/utils/extensions/String.kt | 4 ++-- .../ui/components/staking/TokenStakingBlock.kt | 10 +++------- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 02c18f0152..d29cce8cf4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -262,6 +262,6 @@ private fun formatAnnotated(rawString: String): AnnotatedString { * * @return The original reference if hide is false, or reference with STARS if hide is true. */ -fun TextReference.orMaskWithStars(mask: Boolean) : TextReference { +fun TextReference.orMaskWithStars(mask: Boolean): TextReference { return if (mask) stringReference(STARS) else this -} +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index dce38bdc29..b2d8906fb0 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -1,7 +1,5 @@ package com.tangem.utils -import com.tangem.utils.StringsSigns.STARS - object StringsSigns { const val STARS = "\u2217\u2217\u2217" diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/String.kt b/core/utils/src/main/java/com/tangem/utils/extensions/String.kt index 9b2e8052f2..d8ad9d24a8 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/String.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/String.kt @@ -11,6 +11,6 @@ import com.tangem.utils.StringsSigns.STARS * * @return The original string if hide is false, or STARS if hide is true. */ -fun String.orHide(hide: Boolean) : String { +fun String.orHide(hide: Boolean): String { return if (hide) STARS else this -} +} \ 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 366021ea3f..8abb138685 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 @@ -38,11 +38,7 @@ import com.tangem.features.tokendetails.impl.R * @param modifier modifier */ @Composable -internal fun TokenStakingBlock( - state: StakingBlockUM, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { +internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { AnimatedContent( targetState = state, contentAlignment = Alignment.CenterStart, @@ -130,7 +126,7 @@ private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) .heightIn(min = TangemTheme.dimens.size72) .padding(all = TangemTheme.dimens.spacing12), - ) { + ) { Row { val (alpha, colorFilter) = remember(iconState.isGrayscale) { getGreyScaleColorFilter(iconState.isGrayscale) @@ -180,7 +176,7 @@ private fun Preview_TokenStakingBlock( TangemThemePreview { TokenStakingBlock( state = state, - isBalanceHidden = false + isBalanceHidden = false, ) } } From 3e7108d41770f09959039460baed0ac7f616f879 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 7 Sep 2024 18:07:12 +0300 Subject: [PATCH 139/197] Updated on 2026-08-14 --- .../list/RoundedListWithDividers.kt | 4 +-- .../com/tangem/core/ui/extensions/String.kt | 7 ++++- .../core/ui/extensions/TextReference.kt | 28 ++++++++++--------- .../com/tangem/utils/extensions/String.kt | 16 ----------- .../ui/StakingInitialInfoContent.kt | 5 ++-- 5 files changed, 25 insertions(+), 35 deletions(-) delete mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/String.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt index 641044c813..5f85437fb2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -13,10 +13,10 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.rows.CornersToRound import com.tangem.core.ui.components.rows.RoundableCornersRow import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.utils.extensions.orHide import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -57,7 +57,7 @@ fun LazyListScope.roundedListWithDividersItems( ) { index, row -> InitialInfoContentRow( startText = row.startText.resolveReference(), - endText = row.endText.resolveReference().orHide(hideEndText && row.isEndTextHideable), + endText = row.endText.resolveReference().orMaskWithStars(hideEndText && row.isEndTextHideable), cornersToRound = getCornersToRound(index, rows.size), iconClick = row.iconClick, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt index be264409bc..f227e8b1cb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt @@ -6,6 +6,7 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import com.tangem.utils.StringsSigns.STARS import java.util.Hashtable @Suppress("MagicNumber") @@ -31,4 +32,8 @@ fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap { return bmp } -fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file +fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + +fun String.orMaskWithStars(maskWithStars: Boolean): String { + return if (maskWithStars) STARS else this +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index d29cce8cf4..3f1d59e115 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -233,6 +233,21 @@ operator fun TextReference.plus(ref: TextReference): TextReference { } } + +/** + * Returns the TextReference itself if hide is false, otherwise returns a reference with STARS. + * + * @param mask A boolean flag that determines whether to hide the string. + * If true, the original TextReference will be replaced by reference with STARS. + * If false, the original TextReference will be returned. + * + * @return The original reference if hide is false, or reference with STARS if hide is true. + */ +fun TextReference.orMaskWithStars(mask: Boolean): TextReference { + return if (mask) stringReference(STARS) else this +} + + @Suppress("NOTHING_TO_INLINE") @OptIn(ExperimentalContracts::class) inline fun TextReference?.isNullOrEmpty(): Boolean { @@ -251,17 +266,4 @@ private fun formatAnnotated(rawString: String): AnnotatedString { return buildAnnotatedString { appendMarkdown(markdownText = rawString, node = parsedTree) } -} - -/** - * Returns the TextReference itself if hide is false, otherwise returns a reference with STARS. - * - * @param mask A boolean flag that determines whether to hide the string. - * If true, the original TextReference will be replaced by reference with STARS. - * If false, the original TextReference will be returned. - * - * @return The original reference if hide is false, or reference with STARS if hide is true. - */ -fun TextReference.orMaskWithStars(mask: Boolean): TextReference { - return if (mask) stringReference(STARS) else this } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/String.kt b/core/utils/src/main/java/com/tangem/utils/extensions/String.kt deleted file mode 100644 index d8ad9d24a8..0000000000 --- a/core/utils/src/main/java/com/tangem/utils/extensions/String.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.utils.extensions - -import com.tangem.utils.StringsSigns.STARS - -/** - * Returns the string itself if hide is false, otherwise returns a string of stars. - * - * @param hide A boolean flag that determines whether to hide the string. - * If true, the string will be hidden (replaced by `STARS`). - * If false, the original string will be returned. - * - * @return The original string if hide is false, or STARS if hide is true. - */ -fun String.orHide(hide: Boolean): String { - return if (hide) STARS else this -} \ 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 5701a7ed0c..96a17749b0 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 @@ -51,7 +51,6 @@ import com.tangem.features.staking.impl.presentation.state.stub.StakingClickInte import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.PLUS -import com.tangem.utils.extensions.orHide import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList @@ -172,11 +171,11 @@ private fun StakingRewardBlock( annotatedReference { append(PLUS) appendSpace() - append(rewardFiat.orHide(isBalanceHidden)) + append(rewardFiat.orMaskWithStars(isBalanceHidden)) appendSpace() append(DOT) appendSpace() - append(rewardCrypto.orHide(isBalanceHidden)) + append(rewardCrypto.orMaskWithStars(isBalanceHidden)) } to TangemTheme.colors.text.primary1 } RewardBlockType.RewardUnavailable -> { From 451410346ca193b7f7cc7dd1fcc5c716cd896fed Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 8 Sep 2024 08:24:11 +0300 Subject: [PATCH 140/197] Updated on 2026-08-14 --- .../amountScreen/ui/AmountFieldContainer.kt | 4 +-- .../list/RoundedListWithDividers.kt | 2 +- .../core/ui/extensions/TextReference.kt | 28 +++++++++---------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index a5017f62a1..caee01e48c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -14,9 +14,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.utils.StringsSigns.STARS private const val AMOUNT_FIELD_KEY = "amountFieldKey" @@ -41,7 +41,7 @@ internal fun LazyListScope.amountField( .padding(top = TangemTheme.dimens.spacing14), ) - val balance = if (isBalanceHidden) STARS else amountState.walletBalance.resolveReference() + val balance = amountState.walletBalance.orMaskWithStars(isBalanceHidden).resolveReference() AnimatedContent( targetState = balance, label = "Hide Balance Animation", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt index 5f85437fb2..8ace2f0b02 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -57,7 +57,7 @@ fun LazyListScope.roundedListWithDividersItems( ) { index, row -> InitialInfoContentRow( startText = row.startText.resolveReference(), - endText = row.endText.resolveReference().orMaskWithStars(hideEndText && row.isEndTextHideable), + endText = row.endText.orMaskWithStars(hideEndText && row.isEndTextHideable).resolveReference(), cornersToRound = getCornersToRound(index, rows.size), iconClick = row.iconClick, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 3f1d59e115..7d941398ac 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -233,21 +233,6 @@ operator fun TextReference.plus(ref: TextReference): TextReference { } } - -/** - * Returns the TextReference itself if hide is false, otherwise returns a reference with STARS. - * - * @param mask A boolean flag that determines whether to hide the string. - * If true, the original TextReference will be replaced by reference with STARS. - * If false, the original TextReference will be returned. - * - * @return The original reference if hide is false, or reference with STARS if hide is true. - */ -fun TextReference.orMaskWithStars(mask: Boolean): TextReference { - return if (mask) stringReference(STARS) else this -} - - @Suppress("NOTHING_TO_INLINE") @OptIn(ExperimentalContracts::class) inline fun TextReference?.isNullOrEmpty(): Boolean { @@ -266,4 +251,17 @@ private fun formatAnnotated(rawString: String): AnnotatedString { return buildAnnotatedString { appendMarkdown(markdownText = rawString, node = parsedTree) } +} + +/** + * Returns the TextReference itself if hide is false, otherwise returns a reference with STARS. + * + * @param maskWithStars A boolean flag that determines whether to hide the string. + * If true, the original TextReference will be replaced by reference with STARS. + * If false, the original TextReference will be returned. + * + * @return The original reference if hide is false, or reference with STARS if hide is true. + */ +fun TextReference.orMaskWithStars(maskWithStars: Boolean): TextReference { + return if (maskWithStars) stringReference(STARS) else this } \ No newline at end of file From ad014930b2d901632ccbbafb678ab674477d9a35 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 8 Sep 2024 11:14:50 +0500 Subject: [PATCH 141/197] Updated on 2026-08-14 --- .../state/converters/BalanceItemConverter.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index c6317bf05d..9fd91d3cb8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -65,8 +65,9 @@ internal class BalanceItemConverter( } private fun BalanceType.getTitle(validatorName: String?) = when (this) { - BalanceType.STAKED -> validatorName?.let { stringReference(it) } - BalanceType.PREPARING -> resourceReference(R.string.staking_preparing) + BalanceType.PREPARING, + BalanceType.STAKED, + -> validatorName?.let { stringReference(it) } BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) BalanceType.LOCKED -> resourceReference(R.string.staking_locked) @@ -81,9 +82,16 @@ internal class BalanceItemConverter( BalanceType.UNSTAKING -> getUnbondingDate(balance.date) BalanceType.UNSTAKED -> resourceReference(R.string.staking_tap_to_withdraw) BalanceType.LOCKED -> resourceReference(R.string.staking_tap_to_unlock) + BalanceType.PREPARING -> { + val warmupPeriod = yield.metadata.warmupPeriod.days + combinedReference( + resourceReference(R.string.staking_details_warmup_period), + stringReference(" "), + pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)), + ) + } BalanceType.AVAILABLE, BalanceType.STAKED, - BalanceType.PREPARING, BalanceType.UNLOCKING, BalanceType.REWARDS, BalanceType.UNKNOWN, @@ -91,12 +99,20 @@ internal class BalanceItemConverter( } private fun getUnbondingDate(date: DateTime?): TextReference { - val now = DateTime.now().millis + val unbondingPeriod = yield.metadata.cooldownPeriod.days + if (date == null) { + return combinedReference( + resourceReference(R.string.staking_details_unbonding_period), + stringReference(" "), + pluralReference(R.plurals.common_days, unbondingPeriod, wrappedList(unbondingPeriod)), + ) + } + val nowCalendar = Calendar.getInstance() nowCalendar.resetHours() val endDate = Calendar.getInstance() - endDate.timeInMillis = date?.millis ?: now + endDate.timeInMillis = date.millis endDate.resetHours() val days = ((endDate.timeInMillis - nowCalendar.timeInMillis) / DAY_IN_MILLIS).toInt() From db9990920e3bc7f98ed6eb48c088f01995ae9d08 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 8 Sep 2024 11:16:10 +0500 Subject: [PATCH 142/197] Updated on 2026-08-14 --- .../core/ui/pullToRefresh/PullToRefreshConfig.kt | 15 +++++++++++++++ .../tokendetails/TokenDetailsPreviewData.kt | 4 ++-- .../tokendetails/state/TokenDetailsState.kt | 4 ++-- .../components/TokenDetailsPullToRefreshConfig.kt | 3 --- .../factory/TokenDetailsSkeletonStateConverter.kt | 6 +++--- .../tokendetails/ui/TokenDetailsScreen.kt | 3 ++- .../viewmodels/TokenDetailsClickIntents.kt | 2 +- .../viewmodels/TokenDetailsViewModel.kt | 2 +- .../common/preview/WalletScreenPreviewData.kt | 3 ++- .../state/model/WalletPullToRefreshConfig.kt | 15 --------------- .../wallet/state/model/WalletState.kt | 7 ++++--- .../state/model/holder/LockedWalletStateHolder.kt | 8 ++++---- .../transformers/SetRefreshStateTransformer.kt | 3 ++- .../state/utils/WalletLoadingStateFactory.kt | 5 +++-- .../wallet/presentation/wallet/ui/WalletScreen.kt | 5 +++-- 15 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/pullToRefresh/PullToRefreshConfig.kt delete mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/pullToRefresh/PullToRefreshConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/pullToRefresh/PullToRefreshConfig.kt new file mode 100644 index 0000000000..b6c6aea4b9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/pullToRefresh/PullToRefreshConfig.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.pullToRefresh + +/** + * Pull to refresh config data + * + * @property isRefreshing state is indicator visible + * @property onRefresh lambda be invoked when pulled to refresh + */ +data class PullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: (ShowRefreshState) -> Unit) { + + @JvmInline + value class ShowRefreshState( + val value: Boolean = true, + ) +} \ No newline at end of file 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 9c69cd0b55..c75f89ca04 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 @@ -12,10 +12,10 @@ 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.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton -import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -164,7 +164,7 @@ internal object TokenDetailsPreviewData { onStakeClicked = {}, ) - private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( + private val pullToRefreshConfig = PullToRefreshConfig( isRefreshing = false, onRefresh = {}, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index f6b3d59719..ffc5d6f5c1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -6,9 +6,9 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification -import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -23,7 +23,7 @@ internal data class TokenDetailsState( val swapTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, - val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, + val pullToRefreshConfig: PullToRefreshConfig, val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt deleted file mode 100644 index 5015995638..0000000000 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.state.components - -data class TokenDetailsPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 8339f1f69b..a87e9b10a4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -15,7 +16,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton -import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R @@ -137,8 +137,8 @@ internal class TokenDetailsSkeletonStateConverter( ) } - private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig( + private fun createPullToRefresh(): PullToRefreshConfig = PullToRefreshConfig( isRefreshing = false, - onRefresh = clickIntents::onRefreshSwipe, + onRefresh = { clickIntents.onRefreshSwipe(it.value) } , ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 65fb047b37..9ad67ab5bf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -38,6 +38,7 @@ import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData @@ -80,7 +81,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon ) { scaffoldPaddings -> val pullRefreshState = rememberPullRefreshState( refreshing = state.pullToRefreshConfig.isRefreshing, - onRefresh = state.pullToRefreshConfig.onRefresh, + onRefresh = { state.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) } , ) val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 833a3a3f0b..ec142bf0ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -29,7 +29,7 @@ interface TokenDetailsClickIntents { fun onHideConfirmed() - fun onRefreshSwipe() + fun onRefreshSwipe(isRefreshing: Boolean) fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 949d9deaa0..2157443000 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -711,7 +711,7 @@ internal class TokenDetailsViewModel @Inject constructor( ) } - override fun onRefreshSwipe() { + override fun onRefreshSwipe(isRefreshing: Boolean) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Refreshed(cryptoCurrency.symbol)) internalUiState.value = stateFactory.getRefreshingState() 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 77ae8c206f..5bd058642b 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 @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.impl.R @@ -112,7 +113,7 @@ internal object WalletScreenPreviewData { } private val multiWalletState by lazy { WalletState.MultiCurrency.Content( - pullToRefreshConfig = WalletPullToRefreshConfig( + pullToRefreshConfig = PullToRefreshConfig( isRefreshing = false, onRefresh = {}, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt deleted file mode 100644 index 8bf0e4ea63..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -/** - * Wallet screen top bar config - * - * @property isRefreshing state is indicator visible - * @property onRefresh lambda be invoked when pulled to refresh - */ -data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: (ShowRefreshState) -> Unit) { - - @JvmInline - value class ShowRefreshState( - val value: Boolean, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 81c9017dee..0f48485b21 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedTxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder @@ -22,7 +23,7 @@ internal sealed interface WalletState : WalletStateHolder { abstract val manageTokensButtonConfig: ManageTokensButtonConfig? data class Content( - override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val pullToRefreshConfig: PullToRefreshConfig, override val walletCardState: WalletCardState, override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, @@ -52,7 +53,7 @@ internal sealed interface WalletState : WalletStateHolder { abstract val marketPriceBlockState: MarketPriceBlockState? data class Content( - override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val pullToRefreshConfig: PullToRefreshConfig, override val walletCardState: WalletCardState, override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, @@ -84,7 +85,7 @@ internal sealed interface WalletState : WalletStateHolder { abstract val balancesAndLimitBlockState: BalancesAndLimitsBlockState? data class Content( - override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val pullToRefreshConfig: PullToRefreshConfig, override val walletCardState: WalletCardState, override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt index 96bef56446..518723c898 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.state.model.holder import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal interface WalletStateHolder { - val pullToRefreshConfig: WalletPullToRefreshConfig + val pullToRefreshConfig: PullToRefreshConfig val walletCardState: WalletCardState val warnings: ImmutableList val bottomSheetConfig: TangemBottomSheetConfig? @@ -21,8 +21,8 @@ internal class LockedWalletStateHolder( onUnlockNotificationClick: () -> Unit, ) : WalletStateHolder { - override val pullToRefreshConfig: WalletPullToRefreshConfig - get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}) + override val pullToRefreshConfig: PullToRefreshConfig + get() = PullToRefreshConfig(isRefreshing = false, onRefresh = {}) override val warnings: ImmutableList = persistentListOf( WalletNotification.UnlockWallets(onUnlockNotificationClick), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 58ceb2ac53..68e485e6eb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.* import kotlinx.collections.immutable.PersistentList @@ -38,7 +39,7 @@ internal class SetRefreshStateTransformer( } } - private fun WalletPullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): WalletPullToRefreshConfig { + private fun PullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): PullToRefreshConfig { return copy(isRefreshing = isRefreshing) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index feeb07e488..924cd9e0c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory @@ -71,8 +72,8 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn ) } - private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { - return WalletPullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) + private fun createPullToRefreshConfig(): PullToRefreshConfig { + return PullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) } private fun UserWallet.toLoadingWalletCardState(): WalletCardState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 1991328d2c..1250ea7853 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -57,6 +57,7 @@ import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme @@ -305,7 +306,7 @@ private fun BaseScaffold( val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + selectedWallet.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) }, ) @@ -426,7 +427,7 @@ private inline fun BaseScaffoldWithMarkets( val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + selectedWallet.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) }, ) From 4dc40538692ae0a482080290a42c2f1ae0977bf0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 8 Sep 2024 11:16:31 +0500 Subject: [PATCH 143/197] Updated on 2026-08-14 --- .../ui/components/rows/RoundableCornersRow.kt | 4 +- .../state/StakingStateController.kt | 2 +- .../impl/presentation/state/StakingUiState.kt | 2 + .../previewdata/InitialStakingStatePreview.kt | 2 + .../state/stub/StakingClickIntentsStub.kt | 5 +- .../SetInitialDataStateTransformer.kt | 9 +- .../SetInitialLoadingStateTransformer.kt | 20 ++ .../ui/StakingInitialInfoContent.kt | 220 +++++++++++------- .../viewmodel/StakingClickIntents.kt | 2 + .../viewmodel/StakingViewModel.kt | 88 ++++--- .../TokenDetailsSkeletonStateConverter.kt | 2 +- .../tokendetails/ui/TokenDetailsScreen.kt | 2 +- 12 files changed, 231 insertions(+), 127 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialLoadingStateTransformer.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index c2b1e97286..ac525e0fb1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme @@ -95,8 +96,7 @@ enum class CornersToRound { @Suppress("TopLevelComposableFunctions") @Composable - fun getShape(): RoundedCornerShape { - val radius = TangemTheme.dimens.radius12 + fun getShape(radius: Dp = TangemTheme.dimens.radius12): RoundedCornerShape { return when (this) { ALL_4 -> RoundedCornerShape(radius) TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) 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 8a47bfe35d..4c450c78a2 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 @@ -21,7 +21,7 @@ import javax.inject.Singleton @Singleton internal class StakingStateController @Inject constructor( - private val urlOpener: UrlOpener, + urlOpener: UrlOpener, ) { val value: StakingUiState get() = uiState.value 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 ca9f2e192b..1a276f55e0 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 @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig 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 @@ -62,6 +63,7 @@ internal sealed class StakingStates { val aprRange: TextReference, val onInfoClick: (InfoType) -> Unit, val yieldBalance: InnerYieldBalanceState, + val pullToRefreshConfig: PullToRefreshConfig, ) : InitialInfoState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 77d30e2fb2..6ad1e50645 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield @@ -57,6 +58,7 @@ internal object InitialStakingStatePreview { ), onInfoClick = {}, yieldBalance = InnerYieldBalanceState.Empty, + pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}), ) val stateWithYield = defaultState.copy( 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 4697e7fe88..a13675c8bf 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 @@ -21,6 +21,8 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onPrevClick() {} + override fun onRefreshSwipe(isRefreshing: Boolean) {} + override fun onInitialInfoBannerClick() {} override fun onInfoClick(infoType: InfoType) {} @@ -59,7 +61,8 @@ internal object StakingClickIntentsStub : StakingClickIntents { reduceAmountBy: BigDecimal, reduceAmountByDiff: BigDecimal, notification: Class, - ) {} + ) { + } override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) {} 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 35b98d0107..13c748e962 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 @@ -6,6 +6,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.serialization.SerializedBigDecimal @@ -14,10 +15,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.TransactionDoneState -import com.tangem.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 @@ -88,6 +85,10 @@ internal class SetInitialDataStateTransformer( infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalance, + pullToRefreshConfig = PullToRefreshConfig( + onRefresh = { clickIntents.onRefreshSwipe(it.value) }, + isRefreshing = false, + ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialLoadingStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialLoadingStateTransformer.kt new file mode 100644 index 0000000000..416ec78676 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialLoadingStateTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class SetInitialLoadingStateTransformer( + private val isRefreshing: Boolean, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val initialState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data + return prevState.copy( + initialInfoState = initialState?.copy( + pullToRefreshConfig = prevState.initialInfoState.pullToRefreshConfig.copy( + isRefreshing = isRefreshing, + ), + ) ?: prevState.initialInfoState, + ) + } +} \ 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 96a17749b0..6ffdee416f 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 @@ -8,11 +8,16 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,7 +40,9 @@ import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.list.roundedListWithDividersItems +import com.tangem.core.ui.components.rows.CornersToRound import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -52,14 +59,13 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.extensions.orZero -import kotlinx.collections.immutable.ImmutableList private const val BANNER_BLOCK_KEY = "BannerBlock" private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock" private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock" private const val STAKE_PRIMARY_BUTTON_KEY = "StakePrimaryButton" -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Composable internal fun StakingInitialInfoContent( state: StakingStates.InitialInfoState, @@ -69,60 +75,116 @@ internal fun StakingInitialInfoContent( ) { if (state !is StakingStates.InitialInfoState.Data) return - LazyColumn( - verticalArrangement = alignLastToBottom(), - modifier = Modifier - .fillMaxSize() - .background(TangemTheme.colors.background.secondary) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - if (state.showBanner) { - item(key = BANNER_BLOCK_KEY) { - Column( - modifier = Modifier.animateItemPlacement(), - ) { - BannerBlock(onClick = clickIntents::onInitialInfoBannerClick) - SpacerH12() + val pullRefreshState = rememberPullRefreshState( + refreshing = state.pullToRefreshConfig.isRefreshing, + onRefresh = { state.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) }, + ) + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + LazyColumn( + verticalArrangement = alignLastToBottom(), + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + if (state.showBanner) { + item(key = BANNER_BLOCK_KEY) { + Column( + modifier = Modifier.animateItemPlacement(), + ) { + BannerBlock(onClick = clickIntents::onInitialInfoBannerClick) + SpacerH12() + } } } + + this.roundedListWithDividersItems( + rows = state.infoItems, + footerContent = { SpacerH12() }, + hideEndText = isBalanceHidden, + ) + + if (state.yieldBalance is InnerYieldBalanceState.Data) { + item(key = STAKING_REWARD_BLOCK_KEY) { + Column(modifier = Modifier.animateItemPlacement()) { + StakingRewardBlock( + rewardCrypto = state.yieldBalance.rewardsCrypto, + rewardFiat = state.yieldBalance.rewardsFiat, + rewardBlockType = state.yieldBalance.rewardBlockType, + onRewardsClick = clickIntents::openRewardsValidators, + isBalanceHidden = isBalanceHidden, + ) + SpacerH12() + } + } + } + + activeStakingBlock( + state = state, + clickIntents = clickIntents, + isBalanceHidden = isBalanceHidden, + ) + + item(STAKE_PRIMARY_BUTTON_KEY) { + SpacerH12() + StakeButtonBlock(buttonState) + } } - this.roundedListWithDividersItems( - rows = state.infoItems, - footerContent = { SpacerH12() }, - hideEndText = isBalanceHidden, + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + refreshing = state.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, ) + } +} - if (state.yieldBalance is InnerYieldBalanceState.Data) { - item(key = STAKING_REWARD_BLOCK_KEY) { - Column(modifier = Modifier.animateItemPlacement()) { - StakingRewardBlock( - rewardCrypto = state.yieldBalance.rewardsCrypto, - rewardFiat = state.yieldBalance.rewardsFiat, - rewardBlockType = state.yieldBalance.rewardBlockType, - onRewardsClick = clickIntents::openRewardsValidators, - isBalanceHidden = isBalanceHidden, - ) - SpacerH12() - } - } +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.activeStakingBlock( + state: StakingStates.InitialInfoState.Data, + clickIntents: StakingClickIntents, + isBalanceHidden: Boolean, +) { + if (state.yieldBalance is InnerYieldBalanceState.Data) { + item(ACTIVE_STAKING_BLOCK_KEY) { + Text( + text = stringResource(id = R.string.staking_your_stakes), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .clip(CornersToRound.TOP_2.getShape()) + .background(TangemTheme.colors.background.action) + .padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing4, + ), + ) } - - if (state.yieldBalance is InnerYieldBalanceState.Data) { - item(key = ACTIVE_STAKING_BLOCK_KEY) { - Column(modifier = Modifier.animateItemPlacement()) { - ActiveStakingBlock( - balances = state.yieldBalance.balance, - onClick = clickIntents::onActiveStake, - isBalanceHidden = isBalanceHidden, - ) - SpacerH12() - } - } - } - - item(STAKE_PRIMARY_BUTTON_KEY) { - StakeButtonBlock(buttonState) + items( + items = state.yieldBalance.balance, + key = { + // Staked balance does not have unique identifier. + // Balance type and group id is used to identify item. + it.id + it.type + }, + ) { balance -> + ActiveStakingBlock( + balance = balance, + onClick = clickIntents::onActiveStake, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItemPlacement() + .then( + if (state.yieldBalance.balance.last() == balance) { + Modifier.clip(CornersToRound.BOTTOM_2.getShape()) + } else { + Modifier + }, + ), + ) } } } @@ -205,49 +267,29 @@ private fun StakingRewardBlock( @Composable private fun ActiveStakingBlock( - balances: ImmutableList, + balance: BalanceState, onClick: (BalanceState) -> Unit, isBalanceHidden: Boolean, + modifier: Modifier = Modifier, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action), - ) { - Text( - text = stringResource(id = R.string.staking_your_stakes), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding( - top = TangemTheme.dimens.spacing12, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing4, + val (icon, iconTint) = balance.type.getIcon() + InputRowImageInfo( + subtitle = balance.title, + caption = balance.subtitle ?: balance.getAprText(), + infoTitle = balance.fiatAmount.orMaskWithStars(isBalanceHidden), + infoSubtitle = balance.cryptoAmount.orMaskWithStars(isBalanceHidden), + imageUrl = balance.getImage(), + iconRes = icon, + iconTint = iconTint, + modifier = modifier + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + enabled = balance.isClickable, + onClick = { onClick(balance) }, ), - ) - balances.forEach { balance -> - key(balance.id) { - val (icon, iconTint) = balance.type.getIcon() - InputRowImageInfo( - subtitle = balance.title, - caption = balance.subtitle ?: balance.getAprText(), - isGrayscaleImage = !balance.isClickable, - infoTitle = balance.fiatAmount.orMaskWithStars(isBalanceHidden), - infoSubtitle = balance.cryptoAmount.orMaskWithStars(isBalanceHidden), - imageUrl = balance.getImage(), - iconRes = icon, - iconTint = iconTint, - modifier = Modifier.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), - enabled = balance.isClickable, - onClick = { onClick(balance) }, - ), - ) - } - } - } + ) } @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 076d61b47d..b9743fb560 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 @@ -21,6 +21,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onPrevClick() + fun onRefreshSwipe(isRefreshing: Boolean) + fun onInitialInfoBannerClick() fun onInfoClick(infoType: InfoType) 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 6e833d5ebb..ce71e2b75b 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 @@ -76,7 +76,7 @@ internal class StakingViewModel @Inject constructor( private val stateController: StakingStateController, private val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, @@ -149,6 +149,7 @@ internal class StakingViewModel @Inject constructor( private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() private var approvalJobHolder: JobHolder = JobHolder() + private var updateBalancesJobHolder: JobHolder = JobHolder() init { subscribeOnSelectedAppCurrency() @@ -156,6 +157,12 @@ internal class StakingViewModel @Inject constructor( subscribeOnCurrencyStatusUpdates() } + override fun onCleared() { + super.onCleared() + updateBalancesJobHolder.cancel() + approvalJobHolder.cancel() + } + override fun onBackClick() { stakingStateRouter.onBackClick() } @@ -173,7 +180,10 @@ internal class StakingViewModel @Inject constructor( } stakingStateRouter.onNextClick() when { - isInitState() -> stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) + isInitState() -> { + stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) + onRefreshSwipe(isRefreshing = false) + } isAssentState() -> getFee(pendingAction) } } @@ -364,6 +374,19 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onPrevClick() } + override fun onRefreshSwipe(isRefreshing: Boolean) { + stateController.update(SetInitialLoadingStateTransformer(isRefreshing)) + viewModelScope.launch { + val balancesUpdate = async { updateStakeBalance() } + val networkUpdate = async { updateNetworkStatuses(0) } + + awaitAll(balancesUpdate, networkUpdate) + }.saveIn(updateBalancesJobHolder) + .invokeOnCompletion { + stateController.update(SetInitialLoadingStateTransformer(false)) + } + } + override fun onInitialInfoBannerClick() { innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } @@ -711,33 +734,42 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateResetAssentTransformer) }, ) - getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( - ifRight = { - feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, it).getOrNull() - val isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false - cryptoCurrencyStatus = it + getCurrencyStatusUpdatesUseCase(userWalletId, cryptoCurrencyId, false) + .conflate() + .distinctUntilChanged() + .filter { value.currentStep == StakingStep.InitialInfo } + .onEach { maybeStatus -> + maybeStatus.fold( + ifRight = { status -> + feeCryptoCurrencyStatus = + getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() + cryptoCurrencyStatus = status + val isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false - setupApprovalNeeded() - checkIfSubtractAvailable() + setupApprovalNeeded() + checkIfSubtractAvailable() - stateController.update( - transformer = SetInitialDataStateTransformer( - clickIntents = this@StakingViewModel, - yield = yield, - isApprovalNeeded = stakingApproval is StakingApproval.Needed, - isAnyTokenStaked = isAnyTokenStaked, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, - ), + stateController.update( + transformer = SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + isAnyTokenStaked = isAnyTokenStaked, + isApprovalNeeded = stakingApproval is StakingApproval.Needed, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + ), + ) + }, + ifLeft = { error -> + Timber.e(error.toString()) + stakingEventFactory.createGenericErrorAlert(error.toString()) + stateController.update(SetConfirmationStateResetAssentTransformer) + }, ) - }, - ifLeft = { - Timber.e(it.toString()) - stakingEventFactory.createGenericErrorAlert(it.toString()) - stateController.update(SetConfirmationStateResetAssentTransformer) - }, - ) + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) } } @@ -850,11 +882,11 @@ internal class StakingViewModel @Inject constructor( } } - private suspend fun updateNetworkStatuses() { + private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) { updateDelayedNetworkStatusUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, - delayMillis = BALANCE_UPDATE_DELAY, + delayMillis = delay, refresh = true, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index a87e9b10a4..4aeac3dd4b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -139,6 +139,6 @@ internal class TokenDetailsSkeletonStateConverter( private fun createPullToRefresh(): PullToRefreshConfig = PullToRefreshConfig( isRefreshing = false, - onRefresh = { clickIntents.onRefreshSwipe(it.value) } , + onRefresh = { clickIntents.onRefreshSwipe(it.value) }, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 9ad67ab5bf..0cda424f1a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -81,7 +81,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon ) { scaffoldPaddings -> val pullRefreshState = rememberPullRefreshState( refreshing = state.pullToRefreshConfig.isRefreshing, - onRefresh = { state.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) } , + onRefresh = { state.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) }, ) val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { From 72cf995bd63a35aa965ac2937d7562fe00951169 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 14:25:49 +0500 Subject: [PATCH 144/197] Updated on 2026-08-14 --- .../ui/amountScreen/AmountScreenContent.kt | 11 ++-- .../list/RoundedListWithDividers.kt | 65 +++++++++---------- .../ui/components/rows/RoundableCornersRow.kt | 2 +- .../send/impl/presentation/ui/SendScreen.kt | 4 +- .../AddStakingNotificationsTransformer.kt | 2 +- .../ui/StakingConfirmationContent.kt | 7 +- .../impl/presentation/ui/StakingScreen.kt | 5 +- .../ui/StakingValidatorListContent.kt | 4 +- .../presentation/ui/block/StakingFeeBlock.kt | 11 +++- .../viewmodel/StakingViewModel.kt | 3 +- 10 files changed, 64 insertions(+), 50 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index 4f90184a2b..cfd16d8ee2 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -1,7 +1,6 @@ package com.tangem.common.ui.amountScreen import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable @@ -24,13 +23,17 @@ import com.tangem.core.ui.res.TangemThemePreview * @param clickIntents amount screen clicks */ @Composable -fun AmountScreenContent(amountState: AmountState, isBalanceHidden: Boolean, clickIntents: AmountScreenClickIntents) { +fun AmountScreenContent( + amountState: AmountState, + isBalanceHidden: Boolean, + clickIntents: AmountScreenClickIntents, + modifier: Modifier = Modifier, +) { if (amountState !is AmountState.Data) return // Do not put fillMaxSize() in here LazyColumn( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) + modifier = modifier .padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt index 8ace2f0b02..87640bcc2a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -2,11 +2,15 @@ package com.tangem.core.ui.components.list import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R @@ -60,10 +64,8 @@ fun LazyListScope.roundedListWithDividersItems( endText = row.endText.orMaskWithStars(hideEndText && row.isEndTextHideable).resolveReference(), cornersToRound = getCornersToRound(index, rows.size), iconClick = row.iconClick, + showDivider = index < rows.lastIndex, ) - if (index < rows.lastIndex) { - RoundedListDivider() - } } if (footerContent != null) { @@ -78,41 +80,38 @@ private fun InitialInfoContentRow( startText: String, endText: String, cornersToRound: CornersToRound, + showDivider: Boolean, iconClick: (() -> Unit)? = null, ) { - RoundableCornersRow( - startText = startText, - startTextColor = TangemTheme.colors.text.primary1, - startTextStyle = TangemTheme.typography.body2, - endText = endText, - endTextColor = TangemTheme.colors.text.tertiary, - endTextStyle = TangemTheme.typography.body2, - cornersToRound = cornersToRound, - iconResId = R.drawable.ic_information_24, - iconClick = iconClick, - ) + Box { + RoundableCornersRow( + startText = startText, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.body2, + endText = endText, + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = cornersToRound, + iconResId = R.drawable.ic_information_24, + iconClick = iconClick, + ) + if (showDivider) { + RoundedListDivider( + modifier = Modifier.align(Alignment.BottomEnd), + ) + } + } } @Composable -fun RoundedListDivider() { - Row( - modifier = Modifier +fun RoundedListDivider(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .padding(start = TangemTheme.dimens.spacing16) .fillMaxWidth() - .height(TangemTheme.dimens.size0_5), - ) { - Box( - modifier = Modifier - .width(TangemTheme.dimens.size16) - .height(TangemTheme.dimens.size0_5) - .background(TangemTheme.colors.background.primary), - ) - Box( - modifier = Modifier - .weight(1f) - .height(TangemTheme.dimens.size0_5) - .background(TangemTheme.colors.background.tertiary), - ) - } + .height(TangemTheme.dimens.size0_5) + .background(TangemTheme.colors.stroke.primary), + ) } private fun getCornersToRound(currentIndex: Int, listSize: Int): CornersToRound { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index ac525e0fb1..df80c7d5b3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -41,7 +41,7 @@ fun RoundableCornersRow( ) { Surface( shape = cornersToRound.getShape(), - color = TangemTheme.colors.background.primary, + color = TangemTheme.colors.background.action, ) { Row( modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 2c4304444f..3324d89836 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -165,11 +165,13 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS amountState = uiState.amountState, isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, + modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) SendUiStateType.EditAmount -> AmountScreenContent( - amountState = uiState.editAmountState!!, + amountState = uiState.editAmountState, isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, + modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) SendUiStateType.Recipient -> SendRecipientContent( uiState = uiState.recipientState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index f6c6eaac4c..be78b5321c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -104,7 +104,7 @@ internal class AddStakingNotificationsTransformer( return prevState.copy( confirmationState = confirmationState.copy( notifications = notifications.toImmutableList(), - isPrimaryButtonEnabled = !notifications.any { it is StakingNotification.Error }, + isPrimaryButtonEnabled = notifications.any { it is StakingNotification.Error }, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 701e97a0ce..8071e92164 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData @@ -38,7 +39,7 @@ internal fun StakingConfirmationContent( ) { if (state !is StakingStates.ConfirmationState.Data) return val isEnterAction = type == StakingActionCommonType.ENTER - + val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED Column( modifier = Modifier .background(TangemTheme.colors.background.secondary) @@ -60,13 +61,13 @@ internal fun StakingConfirmationContent( AmountBlock( amountState = amountState, isClickDisabled = !isEnterAction, - isEditingDisabled = !isEnterAction, + isEditingDisabled = !isEnterAction || isTransactionSent, onClick = clickIntents::onPrevClick, ) if (isEnterAction) { ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators) } - StakingFeeBlock(feeState = state.feeState) + StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent) NotificationsBlock(notifications = state.notifications) } } 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 55ea17c9a9..d5ae448888 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 @@ -121,9 +121,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M snapshotFlow { isTransitionAnimationRunning } .withIndex() .map { (index, running) -> - if (running && index != 0) { - delay(timeMillis = 200) - } + if (running && index != 0) delay(timeMillis = 200) running } .first { !it } @@ -169,6 +167,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M amountState = uiState.amountState, isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) StakingStep.Confirmation -> StakingConfirmationContent( amountState = uiState.amountState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 57ad4dd680..9f8db40d8e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -44,7 +44,9 @@ internal fun StakingValidatorListContent( LazyColumn( contentPadding = PaddingValues(bottom = bottomBarHeight), - modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .padding(horizontal = TangemTheme.dimens.spacing16), ) { if (state is ValidatorState.Content) { val validators = state.availableValidators diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 2efa78b2d0..93802cd66d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -29,12 +29,18 @@ import com.tangem.features.staking.impl.presentation.state.FeeState import java.math.BigDecimal @Composable -internal fun StakingFeeBlock(feeState: FeeState) { +internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.button.disabled) + .background( + if (isTransactionSent) { + TangemTheme.colors.background.action + } else { + TangemTheme.colors.button.disabled + }, + ) .padding(TangemTheme.dimens.spacing12), ) { Text( @@ -128,6 +134,7 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va TangemThemePreview { StakingFeeBlock( feeState = value, + isTransactionSent = false, ) } } 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 ce71e2b75b..e99d719256 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 @@ -326,7 +326,8 @@ internal class StakingViewModel @Inject constructor( type = pendingAction?.type, ), ).getOrElse { - stateController.update(AddStakingErrorTransformer(it)) + stateController.update(AddStakingErrorTransformer()) + updateNotifications(GetFeeError.UnknownError) return } stateController.update( From 69627166941f56bfb9c4f3eb2a54c54769a05c1d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 13:59:33 +0300 Subject: [PATCH 145/197] Updated on 2026-08-14 --- .../converter/UserWalletItemUMConverter.kt | 3 +-- .../token/internal/TokenCryptoAmount.kt | 4 ++-- .../token/internal/TokenFiatAmount.kt | 4 ++-- .../ui/components/transactions/Transaction.kt | 8 ++----- .../com/tangem/core/ui/extensions/String.kt | 7 +++--- .../core/ui/extensions/TextReference.kt | 6 +++-- .../java/com/tangem/utils/StringsSigns.kt | 1 - .../ui/recipient/SendRecipientContent.kt | 4 ++-- .../feature/swap/ui/SwapScreenContent.kt | 8 ++----- .../feature/swap/ui/SwapSelectTokenScreen.kt | 24 +++++++------------ .../ui/components/TokenDetailsBalanceBlock.kt | 10 ++++---- .../wallet/state/model/WalletCardState.kt | 2 -- .../wallet/ui/components/common/WalletCard.kt | 14 +++++------ 13 files changed, 39 insertions(+), 56 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index 163168be81..34a65207e3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -10,7 +10,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.utils.StringsSigns.DOT -import com.tangem.utils.StringsSigns.STARS import com.tangem.utils.converter.Converter /** @@ -68,7 +67,7 @@ class UserWalletItemUMConverter( ) return when { - isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) + isBalanceHidden -> combinedReference(cardCountRef, dividerRef, TextReference.STARS) isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) isLoading -> cardCountRef else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt index bc87d3d276..aa7e254a1b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenCryptoAmount.kt @@ -10,8 +10,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.res.TangemTheme -import com.tangem.utils.StringsSigns import com.tangem.core.ui.components.token.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState @Composable @@ -23,7 +23,7 @@ internal fun TokenCryptoAmount( when (state) { is TokenCryptoAmountState.Content -> { CryptoAmountText( - amount = if (isBalanceHidden) StringsSigns.STARS else state.text, + amount = state.text.orMaskWithStars(isBalanceHidden), modifier = modifier, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt index 2f33bf4019..e8317b0c26 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt @@ -16,8 +16,8 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.res.TangemTheme -import com.tangem.utils.StringsSigns import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState @Composable @@ -25,7 +25,7 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool when (state) { is TokenFiatAmountState.Content -> { FiatAmountText( - text = if (isBalanceHidden) StringsSigns.STARS else state.text, + text = state.text.orMaskWithStars(isBalanceHidden), hasStaked = state.hasStaked, modifier = modifier, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 6a906295de..91efa949e6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -28,13 +28,9 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Direction import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Status -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.utils.StringsSigns import java.util.UUID /** @@ -258,7 +254,7 @@ private fun Amount(state: TransactionState, isBalanceHidden: Boolean, modifier: when (state) { is TransactionState.Content -> { Text( - text = if (isBalanceHidden) StringsSigns.STARS else state.amount, + text = state.amount.orMaskWithStars(isBalanceHidden), modifier = modifier, textAlign = TextAlign.End, color = when (state.status) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt index f227e8b1cb..63fb8f00bd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt @@ -6,7 +6,6 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import com.tangem.utils.StringsSigns.STARS import java.util.Hashtable @Suppress("MagicNumber") @@ -35,5 +34,7 @@ fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap { fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } fun String.orMaskWithStars(maskWithStars: Boolean): String { - return if (maskWithStars) STARS else this -} \ No newline at end of file + return if (maskWithStars) THREE_STARS else this +} + +internal const val THREE_STARS = "\u2217\u2217\u2217" \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 7d941398ac..c7a5af394e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString.Builder import androidx.compose.ui.text.buildAnnotatedString -import com.tangem.utils.StringsSigns.STARS import org.intellij.markdown.MarkdownElementTypes import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -68,6 +67,9 @@ sealed interface TextReference { /** Empty string as [TextReference] */ val EMPTY: TextReference by lazy(mode = LazyThreadSafetyMode.NONE) { Str(value = "") } + + /** Stars string as [TextReference] */ + val STARS: TextReference by lazy(mode = LazyThreadSafetyMode.NONE) { Str(value = THREE_STARS) } } } @@ -263,5 +265,5 @@ private fun formatAnnotated(rawString: String): AnnotatedString { * @return The original reference if hide is false, or reference with STARS if hide is true. */ fun TextReference.orMaskWithStars(maskWithStars: Boolean): TextReference { - return if (maskWithStars) stringReference(STARS) else this + return if (maskWithStars) stringReference(THREE_STARS) else this } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index b2d8906fb0..c563b399a8 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -2,7 +2,6 @@ package com.tangem.utils object StringsSigns { - const val STARS = "\u2217\u2217\u2217" const val DOT = "•" const val PLUS = "+" const val MINUS = "-" diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 75729e23b1..013b9e7258 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -33,8 +33,8 @@ import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.ImmutableList private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" @@ -205,7 +205,7 @@ private fun LazyListScope.listItem( AnimateRecentAppearance(item.isVisible) { ListItemWithIcon( title = title, - subtitle = if (isBalanceHidden) STARS else item.subtitle.resolveReference(), + subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(), info = item.timestamp?.resolveReference(), subtitleEndOffset = item.subtitleEndOffset, subtitleIconRes = item.subtitleIconRes, 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 ea5ce81e01..f64eb3344e 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 @@ -28,6 +28,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.getActiveIconResByCoinId +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -38,7 +39,6 @@ import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R -import com.tangem.utils.StringsSigns.STARS @Suppress("LongMethod") @Composable @@ -176,11 +176,7 @@ private fun TransactionCardData( is SwapCardState.SwapCardData -> { TransactionCard( type = swapCardState.type, - balance = if (swapCardState.isBalanceHidden) { - STARS - } else { - swapCardState.balance - }, + balance = swapCardState.balance.orMaskWithStars(swapCardState.isBalanceHidden), textFieldValue = swapCardState.amountTextFieldValue, amountEquivalent = swapCardState.amountEquivalent, tokenIconUrl = swapCardState.tokenIconUrl ?: "", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 5066dc9be6..a0d643fffd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -24,6 +24,7 @@ import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -32,7 +33,6 @@ import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.presentation.R -import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -251,13 +251,10 @@ private fun TokenItem( modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), ) { Text( - text = if (token.addedTokenBalanceData.isBalanceHidden && - !token.addedTokenBalanceData.amountEquivalent.isNullOrEmpty() - ) { - StringsSigns.STARS - } else { - token.addedTokenBalanceData.amountEquivalent.orEmpty() - }, + text = token.addedTokenBalanceData.amountEquivalent.orEmpty().orMaskWithStars( + maskWithStars = token.addedTokenBalanceData.isBalanceHidden && + !token.addedTokenBalanceData.amountEquivalent.isNullOrEmpty(), + ), style = TangemTheme.typography.subtitle1, color = if (token.available) { TangemTheme.colors.text.primary1 @@ -267,13 +264,10 @@ private fun TokenItem( ) SpacerW2() Text( - text = if (token.addedTokenBalanceData.isBalanceHidden && - !token.addedTokenBalanceData.amount.isNullOrEmpty() - ) { - StringsSigns.STARS - } else { - token.addedTokenBalanceData.amount.orEmpty() - }, + text = token.addedTokenBalanceData.amount.orEmpty().orMaskWithStars( + maskWithStars = token.addedTokenBalanceData.isBalanceHidden && + !token.addedTokenBalanceData.amount.isNullOrEmpty(), + ), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index b814e9e554..74ab07f465 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -22,7 +23,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPre import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R -import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.toImmutableList @Composable @@ -94,13 +94,13 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else state.displayFiatBalance, + text = state.displayFiatBalance.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -122,13 +122,13 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else state.displayCryptoBalance, + text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt index d2ffe9d0d1..69b9b48fb1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt @@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.StringsSigns import com.tangem.utils.StringsSigns.DASH_SIGN /** Wallet card state */ @@ -124,7 +123,6 @@ internal sealed interface WalletCardState { } companion object { - val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = StringsSigns.STARS) } val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = DASH_SIGN) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 07aaba8fe5..6ceb82e75c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -46,6 +46,7 @@ import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme @@ -53,7 +54,6 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -import com.tangem.utils.StringsSigns private const val HALF_OF_ITEM_WIDTH = 0.5 @@ -104,11 +104,9 @@ internal fun WalletCard(state: WalletCardState, isBalanceHidden: Boolean, modifi val additionalText by remember(state.additionalInfo, isBalanceHidden) { mutableStateOf( - if (state.additionalInfo?.hideable == true && isBalanceHidden) { - WalletCardState.HIDDEN_BALANCE_TEXT - } else { - state.additionalInfo?.content - }, + state.additionalInfo?.content?.orMaskWithStars( + maskWithStars = state.additionalInfo?.hideable == true && isBalanceHidden, + ), ) } AdditionalInfo( @@ -291,7 +289,7 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: when (walletCardState) { is WalletCardState.Content -> { ResizableText( - text = if (isBalanceHidden) StringsSigns.STARS else walletCardState.balance, + text = walletCardState.balance.orMaskWithStars(isBalanceHidden), fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), color = TangemTheme.colors.text.primary1, @@ -301,7 +299,7 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: ) } is WalletCardState.Error -> NonContentBalanceText( - text = if (isBalanceHidden) WalletCardState.HIDDEN_BALANCE_TEXT else WalletCardState.EMPTY_BALANCE_TEXT, + text = WalletCardState.EMPTY_BALANCE_TEXT.orMaskWithStars(isBalanceHidden), ) is WalletCardState.Loading -> { RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) From d543112e99d366f8f4a20ddefc62f5c501845b85 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 14:01:00 +0300 Subject: [PATCH 146/197] Updated on 2026-08-14 --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 16ef03c0ac..8c32b86fe1 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.14.0 \ No newline at end of file +versionName=5.15.0 \ No newline at end of file From beda6bca51296cd6d44b0ef1474966cf0341f124 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 14:02:20 +0300 Subject: [PATCH 147/197] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index cd7e3a75f2..b8b2cfbf61 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -17,7 +17,7 @@ }, { "name": "STAKING_ENABLED", - "version": "undefined" + "version": "5.15.0" }, { "name": "DETAILS_REDESIGN_ENABLED", @@ -29,10 +29,10 @@ }, { "name": "MARKETS_ENABLED", - "version": "undefined" + "version": "5.15.0" }, { "name": "NEW_MANAGE_TOKENS", - "version": "undefined" + "version": "5.15.0" } ] From 104333fe4c0f76b7c67d8af703cbfb33848b2a8d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 16:40:11 +0500 Subject: [PATCH 148/197] Updated on 2026-08-14 --- .../presentation/viewmodel/StakingViewModel.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 e99d719256..1988d8a119 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 @@ -372,7 +372,23 @@ internal class StakingViewModel @Inject constructor( } override fun onPrevClick() { - stakingStateRouter.onPrevClick() + if (value.currentStep == StakingStep.Confirmation) { + when ((value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState) { + InnerConfirmationStakingState.ASSENT -> { + stakingStateRouter.onPrevClick() + } + null, + InnerConfirmationStakingState.IN_PROGRESS, + -> { + // do nothing while transaction is in progress + } + InnerConfirmationStakingState.COMPLETED -> { + onNextClick() + } + } + } else { + stakingStateRouter.onPrevClick() + } } override fun onRefreshSwipe(isRefreshing: Boolean) { From 9925da286456ac95f8c6606d5fd7ad4560593bf8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Aug 2024 20:49:02 +0500 Subject: [PATCH 149/197] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 6 + .../staking/model/stakekit/YieldBalance.kt | 7 + .../analytics/TokenScreenAnalyticsEvent.kt | 5 + .../impl/analytics/StakingAnalyticsEvents.kt | 157 ++++++++++++++++++ .../analytics/utils/StakingAnalyticSender.kt | 104 ++++++++++++ .../impl/presentation/StakingFragment.kt | 1 + .../state/StakingStateController.kt | 1 + .../presentation/state/StakingStateRouter.kt | 21 ++- .../impl/presentation/state/StakingUiState.kt | 1 + .../state/stub/StakingClickIntentsStub.kt | 2 + .../SetInitialDataStateTransformer.kt | 13 +- .../ui/StakingInitialInfoContent.kt | 11 +- .../viewmodel/StakingClickIntents.kt | 2 + .../viewmodel/StakingViewModel.kt | 96 ++++++++++- .../viewmodels/TokenDetailsViewModel.kt | 1 + 15 files changed, 410 insertions(+), 18 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/StakingAnalyticsEvents.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index fd070196c9..f9911c81f8 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -81,6 +81,12 @@ sealed class AnalyticsParam { override val feeType: FeeType, ) : TxSentFrom("Swap"), TxData + data class Staking( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + ) : TxSentFrom("Staking"), TxData + data class Approve( override val blockchain: String, override val token: String, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index c932b9f1d9..6c93dfccee 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -25,6 +25,13 @@ sealed class YieldBalance { .filter { it.type == BalanceType.REWARDS } .sumOf { it.amount } } + + fun getValidatorsCount(): Int { + return balance.items + .filterNot { it.validatorAddress.isNullOrBlank() } + .distinctBy { it.validatorAddress } + .size + } } data object Empty : YieldBalance() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 1896d2c250..bea218c179 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -77,4 +77,9 @@ sealed class TokenScreenAnalyticsEvent( event = "Button - Token Trustline", params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), ) + + data class StakingClicked(val token: String) : TokenScreenAnalyticsEvent( + event = "Staking Clicked", + params = mapOf("Token" to token), + ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/StakingAnalyticsEvents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/StakingAnalyticsEvents.kt new file mode 100644 index 0000000000..336486156f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/StakingAnalyticsEvents.kt @@ -0,0 +1,157 @@ +package com.tangem.features.staking.impl.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.staking.model.stakekit.action.StakingActionType + +internal sealed class StakingAnalyticsEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent( + category = "Staking", + event = event, + params = params, +) { + + data class StakingInfoScreenOpened( + val validatorsCount: Int, + val token: String, + ) : StakingAnalyticsEvents( + event = "Staking Info Screen Opened", + params = mapOf( + "Validators Count" to validatorsCount.toString(), + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class WhatIsStaking(val token: String) : StakingAnalyticsEvents( + event = "Link - What Is Staking", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class AmountScreenOpened(val token: String) : StakingAnalyticsEvents( + event = "Amount Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class ConfirmationScreenOpened( + val token: String, + val validator: String, + val action: StakingActionType, + ) : StakingAnalyticsEvents( + event = "Confirmation Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + "Validator" to validator, + "Action" to action.name, + ), + ) + + data class StakeInProgressScreenOpened( + val validator: String, + val token: String, + ) : StakingAnalyticsEvents( + event = "Stake In Progress Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + "Validator" to validator, + ), + ) + + data class RewardScreenOpened(val token: String) : StakingAnalyticsEvents( + event = "Reward Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class AmountSelectCurrency( + val token: String, + val isAppCurrency: Boolean, + ) : StakingAnalyticsEvents( + event = "Selected Currency", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + AnalyticsParam.TYPE to if (isAppCurrency) "App Currency" else "Token", + ), + ) + + data class ButtonMax(val token: String) : StakingAnalyticsEvents( + event = "Button - Max", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class ButtonCancel( + val source: StakeScreenSource, + val token: String, + ) : StakingAnalyticsEvents( + event = "Button - Cancel", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + AnalyticsParam.SOURCE to source.name, + ), + ) + + data class ButtonValidator( + val source: StakeScreenSource, + val token: String, + ) : StakingAnalyticsEvents( + event = "Button - Validator", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + AnalyticsParam.SOURCE to source.name, + ), + ) + + data class ButtonRewards( + val token: String, + ) : StakingAnalyticsEvents( + event = "Button - Rewards", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + ), + ) + + data class ButtonAction( + val action: String, + val token: String, + val validator: String, + ) : StakingAnalyticsEvents( + event = "Button - $action", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to token, + "Validator" to validator, + ), + ) + + data object ButtonShare : StakingAnalyticsEvents(event = "Button - Share") + + data object ButtonExplore : StakingAnalyticsEvents(event = "Button - Explore") + + data class StakingError( + val token: String, + ) : StakingAnalyticsEvents( + event = "Errors", + params = mapOf(AnalyticsParam.TOKEN_PARAM to token), + ) + + data class TransactionError( + val token: String, + ) : StakingAnalyticsEvents( + event = "Error - Transaction Rejected", + params = mapOf(AnalyticsParam.TOKEN_PARAM to token), + ) +} + +enum class StakeScreenSource { + Info, + Amount, + Confirmation, + Validators, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt new file mode 100644 index 0000000000..42dc192fbb --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -0,0 +1,104 @@ +package com.tangem.features.staking.impl.analytics.utils + +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.staking.impl.analytics.StakeScreenSource +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.presentation.state.* + +internal class StakingAnalyticSender( + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun initialInfoScreen(value: StakingUiState) { + val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data + val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data + val validatorCount = validatorState?.balance + ?.filterNot { it.validator?.address.isNullOrBlank() } + ?.distinctBy { it.validator?.address } + ?.size ?: 0 + + analyticsEventHandler.send( + StakingAnalyticsEvents.StakingInfoScreenOpened( + validatorsCount = validatorCount, + token = value.cryptoCurrencySymbol, + ), + ) + } + + fun confirmationScreen(value: StakingUiState) { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorName = validatorState?.chosenValidator?.name ?: return + + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) return + + analyticsEventHandler.send( + StakingAnalyticsEvents.ConfirmationScreenOpened( + token = value.cryptoCurrencySymbol, + validator = validatorName, + action = when (value.actionType) { + StakingActionCommonType.ENTER -> StakingActionType.STAKE + StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE + StakingActionCommonType.PENDING_REWARDS, + StakingActionCommonType.PENDING_OTHER, + -> confirmationState.pendingAction?.type ?: StakingActionType.UNKNOWN + }, + ), + ) + } + + fun screenCancel(value: StakingUiState) { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonCancel( + source = when (value.currentStep) { + StakingStep.InitialInfo -> StakeScreenSource.Info + StakingStep.Amount -> StakeScreenSource.Amount + StakingStep.Confirmation -> StakeScreenSource.Confirmation + StakingStep.Validators, + StakingStep.RewardsValidators, + -> StakeScreenSource.Validators + }, + token = value.cryptoCurrencyName, + ), + ) + } + + fun sendTransactionApprovalAnalytics(tokenCryptoCurrency: CryptoCurrency) { + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.Approve( + blockchain = tokenCryptoCurrency.network.name, + token = tokenCryptoCurrency.symbol, + feeType = AnalyticsParam.FeeType.Normal, + permissionType = ApproveType.LIMITED.name, + ), + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + } + + fun sendTransactionStakingAnalytics(cryptoCurrency: CryptoCurrency, validator: String) { + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.Staking( + blockchain = cryptoCurrency.network.name, + token = cryptoCurrency.symbol, + feeType = AnalyticsParam.FeeType.Normal, + ), + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + analyticsEventHandler.send( + StakingAnalyticsEvents.StakeInProgressScreenOpened( + validator = validator, + token = cryptoCurrency.symbol, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index cb590bcb4c..1740d65084 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -54,6 +54,7 @@ internal class StakingFragment : ComposeFragment() { StakingStateRouter( appRouter = appRouter, stateController = stateController, + analyticsEventsHandler = analyticsEventsHandler, ), ) } 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 4c450c78a2..0b04c60e11 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 @@ -68,6 +68,7 @@ internal class StakingStateController @Inject constructor( clickIntents = StakingClickIntentsStub, walletName = "", cryptoCurrencyName = "", + cryptoCurrencySymbol = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 5dd1f3de27..69cca72194 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -1,14 +1,21 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender internal class StakingStateRouter( private val appRouter: AppRouter, private val stateController: StakingStateController, + private val analyticsEventsHandler: AnalyticsEventHandler, ) { + private val analyticSender = StakingAnalyticSender(analyticsEventsHandler) + fun onBackClick() { + analyticSender.screenCancel(stateController.value) appRouter.pop() stateController.clear() } @@ -48,14 +55,21 @@ internal class StakingStateRouter( } private fun showInitial() { + analyticSender.initialInfoScreen(stateController.value) stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } - fun showRewardsValidators() { + private fun showRewardsValidators() { + analyticsEventsHandler.send( + StakingAnalyticsEvents.RewardScreenOpened(stateController.value.cryptoCurrencySymbol), + ) stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } } - fun showAmount() { + private fun showAmount() { + analyticsEventsHandler.send( + StakingAnalyticsEvents.AmountScreenOpened(stateController.value.cryptoCurrencySymbol), + ) stateController.update { it.copy(currentStep = StakingStep.Amount) } } @@ -63,7 +77,8 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.Validators) } } - fun showConfirmation() { + private fun showConfirmation() { + analyticSender.confirmationScreen(stateController.value) stateController.update { it.copy(currentStep = StakingStep.Confirmation) } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 1a276f55e0..202219cb3b 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 @@ -27,6 +27,7 @@ internal data class StakingUiState( val clickIntents: StakingClickIntents, val walletName: String, val cryptoCurrencyName: String, + val cryptoCurrencySymbol: String, val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, 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 a13675c8bf..4cc05fa7f2 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 @@ -69,4 +69,6 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onNotificationCancel(notification: Class) {} override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {} + + override fun onActiveStakeAnalytic() {} } \ No newline at end of file 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 13c748e962..8779bf824e 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 @@ -56,16 +56,19 @@ internal class SetInitialDataStateTransformer( } private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) { - YieldBalancesConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + YieldBalancesConverter( + cryptoCurrencyStatusProvider, + appCurrencyProvider, + yield, + ) } override fun transform(prevState: StakingUiState): StakingUiState { - val cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name - + val cryptoCurrency = cryptoCurrencyStatusProvider().currency return prevState.copy( title = TextReference.EMPTY, - walletName = userWalletProvider.invoke().name, - cryptoCurrencyName = cryptoCurrencyName, + cryptoCurrencyName = cryptoCurrency.name, + cryptoCurrencySymbol = cryptoCurrency.symbol, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), 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 6ffdee416f..15aeef6efe 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 @@ -173,8 +173,9 @@ private fun LazyListScope.activeStakingBlock( ) { balance -> ActiveStakingBlock( balance = balance, - onClick = clickIntents::onActiveStake, isBalanceHidden = isBalanceHidden, + onClick = clickIntents::onActiveStake, + onAnalytic = clickIntents::onActiveStakeAnalytic, modifier = Modifier .animateItemPlacement() .then( @@ -268,8 +269,9 @@ private fun StakingRewardBlock( @Composable private fun ActiveStakingBlock( balance: BalanceState, - onClick: (BalanceState) -> Unit, isBalanceHidden: Boolean, + onClick: (BalanceState) -> Unit, + onAnalytic: () -> Unit, modifier: Modifier = Modifier, ) { val (icon, iconTint) = balance.type.getIcon() @@ -287,7 +289,10 @@ private fun ActiveStakingBlock( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), enabled = balance.isClickable, - onClick = { onClick(balance) }, + onClick = { + onAnalytic() + onClick(balance) + }, ), ) } 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 b9743fb560..ed0cfbffc4 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 @@ -39,6 +39,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onActiveStake(activeStake: BalanceState) + fun onActiveStakeAnalytic() + fun showApprovalBottomSheet() fun onApprovalClick() 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 e99d719256..b17622c312 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 @@ -15,6 +15,7 @@ import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -29,7 +30,9 @@ import com.tangem.domain.staking.* import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType @@ -45,6 +48,9 @@ import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.staking.impl.analytics.StakeScreenSource +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender 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 @@ -106,6 +112,7 @@ internal class StakingViewModel @Inject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -135,6 +142,8 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() + private var isInitialInfoAnalyticSent: Boolean = false + private val stakingEventFactory: StakingEventFactory get() = StakingEventFactory( stateController = stateController, @@ -142,6 +151,10 @@ internal class StakingViewModel @Inject constructor( onFailedTxEmailClick = ::onFailedTxEmailClick, ) + private val stakingAnalyticSender = StakingAnalyticSender( + analyticsEventHandler = analyticsEventHandler, + ) + private var stakingApproval: StakingApproval = StakingApproval.Empty private var isAmountSubtractAvailable: Boolean = false private val allowanceTaskScheduler = SingleTaskScheduler() @@ -270,6 +283,7 @@ internal class StakingViewModel @Inject constructor( ), ).getOrElse { Timber.e(it.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stakingEventFactory.createStakingErrorAlert(it) stateController.update(SetConfirmationStateResetAssentTransformer) return@launch @@ -284,6 +298,7 @@ internal class StakingViewModel @Inject constructor( transactionId = transaction.id, ).getOrElse { Timber.e(it.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stakingEventFactory.createStakingErrorAlert(it) stateController.update(SetConfirmationStateResetAssentTransformer) return@launch @@ -301,6 +316,7 @@ internal class StakingViewModel @Inject constructor( fullTransactionsData = fullTransactionsData, fee = fee, pendingAction = confirmationState.pendingAction, + validator = validatorState.chosenValidator.name, ) } } @@ -326,6 +342,7 @@ internal class StakingViewModel @Inject constructor( type = pendingAction?.type, ), ).getOrElse { + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stateController.update(AddStakingErrorTransformer()) updateNotifications(GetFeeError.UnknownError) return @@ -365,6 +382,7 @@ internal class StakingViewModel @Inject constructor( updateNotifications() }, ifLeft = { + analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) stateController.update(AddStakingErrorTransformer()) updateNotifications(it) }, @@ -389,6 +407,7 @@ internal class StakingViewModel @Inject constructor( } override fun onInitialInfoBannerClick() { + analyticsEventHandler.send(StakingAnalyticsEvents.WhatIsStaking(cryptoCurrencyStatus.currency.symbol)) innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } @@ -409,26 +428,47 @@ internal class StakingViewModel @Inject constructor( } override fun onMaxValueClick() { + analyticsEventHandler.send(StakingAnalyticsEvents.ButtonMax(cryptoCurrencyStatus.currency.symbol)) stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield)) } override fun onCurrencyChangeClick(isFiat: Boolean) { + analyticsEventHandler.send( + StakingAnalyticsEvents.AmountSelectCurrency(cryptoCurrencyStatus.currency.symbol, isFiat), + ) stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat)) } - override fun openValidators() = stakingStateRouter.showValidators() + override fun openValidators() { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonValidator( + source = StakeScreenSource.Confirmation, + token = cryptoCurrencyStatus.currency.symbol, + ), + ) + stakingStateRouter.showValidators() + } override fun onValidatorSelect(validator: Yield.Validator) { stateController.update(ValidatorSelectChangeTransformer(validator)) } override fun openRewardsValidators() { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonRewards(value.cryptoCurrencyName), + ) val rewardsValidators = stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data val rewards = rewardsValidators?.rewards if (rewards != null && rewards.isSingleItem()) { onActiveStake(rewards.first()) } else { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonValidator( + source = StakeScreenSource.Info, + token = cryptoCurrencyStatus.currency.symbol, + ), + ) onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS) } } @@ -463,6 +503,15 @@ internal class StakingViewModel @Inject constructor( } } + override fun onActiveStakeAnalytic() { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonValidator( + source = StakeScreenSource.Info, + token = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + override fun showApprovalBottomSheet() { stateController.update( ShowApprovalBottomSheetTransformer( @@ -503,6 +552,7 @@ internal class StakingViewModel @Inject constructor( ).fold( ifLeft = { error -> Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) stateController.update( SetConfirmationStateAssentApprovalTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -524,6 +574,7 @@ internal class StakingViewModel @Inject constructor( ).fold( ifLeft = { error -> Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) stateController.update( SetConfirmationStateAssentApprovalTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -535,6 +586,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateResetAssentTransformer) }, ifRight = { + stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency) stateController.update(SetApprovalInProgressTransformer) stateController.update(DismissBottomSheetStateTransformer()) awaitForAllowance(confirmationState.pendingAction) @@ -652,10 +704,10 @@ internal class StakingViewModel @Inject constructor( } override fun onExploreClick() { + analyticsEventHandler.send(StakingAnalyticsEvents.ButtonExplore) val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl - if (txUrl != null) { innerRouter.openUrl(txUrl) } @@ -666,12 +718,11 @@ internal class StakingViewModel @Inject constructor( val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl + analyticsEventHandler.send(StakingAnalyticsEvents.ButtonShare) if (txUrl != null) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) stakingEventFactory.createShareDialog(txUrl = txUrl) } - - // TODO staking [REDACTED_TASK_KEY] } override fun onFailedTxEmailClick(errorMessage: String) { @@ -742,6 +793,17 @@ internal class StakingViewModel @Inject constructor( .onEach { maybeStatus -> maybeStatus.fold( ifRight = { status -> + if (!isInitialInfoAnalyticSent) { + isInitialInfoAnalyticSent = true + val balances = status.value.yieldBalance as? YieldBalance.Data + analyticsEventHandler.send( + StakingAnalyticsEvents.StakingInfoScreenOpened( + validatorsCount = balances?.getValidatorsCount() ?: 0, + token = status.currency.symbol, + ), + ) + } + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() cryptoCurrencyStatus = status @@ -800,14 +862,33 @@ internal class StakingViewModel @Inject constructor( fullTransactionsData: List, fee: Fee, pendingAction: PendingAction?, + validator: String, ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val action = when (value.actionType) { + StakingActionCommonType.ENTER -> StakingActionType.STAKE + StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE + StakingActionCommonType.PENDING_REWARDS, + StakingActionCommonType.PENDING_OTHER, + -> pendingAction?.type + } + action?.let { + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonAction( + action = it.name, + token = value.cryptoCurrencyName, + validator = validator, + ), + ) + } sendMultipleTransactionUseCase( txsData = fullTransactionsData.map { it.tangemTransaction }, userWallet = userWallet, - network = cryptoCurrencyStatus.currency.network, + network = cryptoCurrency.network, ).fold( ifLeft = { error -> Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -820,6 +901,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateResetAssentTransformer) }, ifRight = { transactionHashes -> + stakingAnalyticSender.sendTransactionStakingAnalytics(cryptoCurrency, validator) transactionsInProgress.clear() submitHash( transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, @@ -828,9 +910,8 @@ internal class StakingViewModel @Inject constructor( scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), - networkId = cryptoCurrencyStatus.currency.network.id, + networkId = cryptoCurrency.network.id, ).getOrElse { "" } - stateController.update( SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -852,6 +933,7 @@ internal class StakingViewModel @Inject constructor( transactionHash = transactionHash, ) .onLeft { + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) saveUnsubmittedHashUseCase.invoke( transactionId = transactionId, transactionHash = transactionHash, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 2157443000..5d65adaf1c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -466,6 +466,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onStakeBannerClick() { + analyticsEventsHandler.send(TokenScreenAnalyticsEvent.StakingClicked(cryptoCurrency.symbol)) openStaking() } From a16a9e80bac77cc419437b5de9edc2b8bd8af809 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 17:16:46 +0500 Subject: [PATCH 150/197] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 17 ++++++++-- core/res/src/main/res/values-es/strings.xml | 17 ++++------ core/res/src/main/res/values-fr/strings.xml | 6 ++-- core/res/src/main/res/values-ja/strings.xml | 9 +++++- core/res/src/main/res/values-ru/strings.xml | 32 ++++++++++++------- .../src/main/res/values-uk-rUA/strings.xml | 4 +-- .../src/main/res/values-zh-rTW/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 11 ++++--- .../transactions/TransactionDoneTitle.kt | 30 ++++++++--------- .../impl/presentation/ui/send/SendContent.kt | 14 ++++++-- .../AddStakingNotificationsTransformer.kt | 4 ++- .../ui/StakingConfirmationContent.kt | 7 ++-- .../presentation/ui/block/StakingFeeBlock.kt | 15 +++++++-- .../feature/swap/ui/SwapSuccessScreen.kt | 15 ++++++++- 14 files changed, 121 insertions(+), 62 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index a50e97f246..73dbc1635e 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -84,6 +84,7 @@ D hast keinen Zugang zur Kamera erteilt, bitte passe deine Datenschutzeinstellungen an Abbrechen Aktion wählen + Beanspruchen Stakingbelohnungen beanstpruchen Schließen Weiter @@ -115,7 +116,9 @@ Zum Anbieter gehen Zum Token Importieren + In Arbeit Später + %1$s übrig Gesperrt Hauptnetz Netzgebühr @@ -413,6 +416,9 @@ Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können Handelsvolumen (24h) Der Gesamtbetrag einer Kryptowährung, der innerhalb der letzten 24 Stunden gehandelt wurde, wobei das Aktivitäts- und Liquiditätsniveau auf dem Markt angegeben wird + Rufe dies auf oder tippe auf die Suchleiste, um Token direkt vom Markt hinzuzufügen + Token hinzufügen + NFC ist auf deinem Gerät nicht verfügbar Du musst einen einzigen Zugangscode einrichten, um alle deine Karten zu schützen Schützen Du kannst später auf jeder Karte einen individuellen Zugangscode einrichten @@ -661,15 +667,18 @@ Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. Aufwärmphase Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. + Indem Du die Staking-Funktionalität nutzen, stimmst Du den %1$s und %2$s des Anbieters zu. Gesperrt Migrieren Natives Staking + Verdiente Prämien werden direkt an Deine Adresse gesendet. Sobald Du diese eingefordert hast, stehen Dir die Mittel zur Verfügung. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen - Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. + Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. + Die Belohnungen werden sofort nach dem start des unstaken gestoppt. Der unstakingprozess dauert %s. Dein Guthaben wurde freigegeben. Du kannst es jetzt zusammen mit deinen verdienten Prämien auszahlen. Vorbereitung Bereit zum Abheben @@ -691,17 +700,22 @@ Belohnungen Stake gesperrt Mehr staken + Du setzt %1$s ein und erhältst jährlich ~ %2$s + Zum Entsperren antippen + Zum abheben antippen Stake %s Staking beenden%s Lösen der Bindungen Gelocktes unlocken Unstaken Staking beenden + Unstake Assets %s Validator/ Prüfer Validatoren Abstimmung Abstimmung gesperrt Zurückziehen + Deine Einsätze Bewahre deine Krypto-Assets sicher auf, während die privaten Schlüssel auf deiner Karte bleiben Revolutionäre Hardware-Wallet Bis zu 3 physische Karten pro Wallet @@ -725,7 +739,6 @@ Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis. Unzureichende Mittel Erlaubnis erteilen - In Arbeit Tauschen Du erhältst Token auswählen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index d69ca4e032..667b0cf956 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -115,10 +115,7 @@ Ir al proveedor Ir al token Importe - - en %d día - en %d días - + En progreso Más tarde Bloqueado Red principal @@ -132,6 +129,7 @@ Tarjeta principal Frase de contraseña Pegar + Política de privacidad %1$s-%2$s %1$s — %2$s Leer más @@ -159,6 +157,7 @@ Soporte Intercambiar términos y condiciones + Condiciones de uso Hoy Transacción fallida Transacciones @@ -264,7 +263,6 @@ Al utilizar la función de intercambio, acepta el %s Al utilizar la función de intercambio, aceptas las condiciones de %1$s y %2$s del proveedor Próximamente habrá más proveedores.\n¡Estén atentos! - Política de privacidad Proveedor Mejor tarifa Disponible hasta %s @@ -272,7 +270,6 @@ No disponible para este par Permiso requerido Recomendado - Condiciones de uso No se encontraron fichas. Por favor intenta con otra solicitud ID : %s ID de transacción copiado @@ -413,6 +410,8 @@ La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular Volumen de operaciones (24 horas) La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado. + Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado + Agregar tokens Deba configurar un único código de acceso para proteger todas sus tarjetas Proteger Puede configurar un código de acceso individual en cada tarjeta más tarde @@ -637,8 +636,6 @@ Olvidar la billetera Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. Nombre - Activo - Para eliminar el staking de sus activos, presione el bloque de arriba El monto del staking debe ser al menos %s Unstaking de la reclamación Porcentaje de rendimiento anual @@ -697,7 +694,6 @@ Desunión Desbloquear Sin staking - Marque activos sin staking para reclamarlos Unstaking Validador Validadores @@ -727,7 +723,6 @@ Hacer swap de esta cantidad de tokens seleccionados causará un impacto significativo en el precio y reducirá su resultado. Fondos insuficientes Dar autorización - En progreso Intercambie Usted recibe Elige token @@ -808,7 +803,7 @@ Hemos encontrado un error desconocido. Mensaje de error: %s. Si el problema persiste, no duda en contactar con nuestro soporte Tarjeta incorrecta seleccionada en Tangem App Error al crear la transacción a partir de los datos de la dApp. Código: %s - Hemos encontrado un error desconocido. Mensaje de error: %s. Si el problema persiste, contacte con nuestro soporte + Hemos encontrado un error desconocido. Mensaje de error: %d. Si el problema persiste, contacte con nuestro soporte No hay sesiones abiertas de WalletConnect ¡Qué lástima! No hay sesiones. Error al emparejar la sesión de WalletConnect: %1$s diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index dd3a253e57..89862022aa 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -115,6 +115,7 @@ Aller au fournisseur Aller au jeton Importez + En cours Plus tard Verrouillé Réseau principal @@ -409,6 +410,8 @@ Le nombre maximal de pièces ou de jetons pouvant exister pour une crypto-monnaie particulière Volume des échanges (24h) Le montant total d\'une crypto-monnaie qui a été échangé au cours des dernières 24 heures, indiquant le niveau d\'activité et de liquidité du marché + Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché + Ajouter des jetons Vous devez définir un seul code d\'accès pour protéger toutes vos cartes Protéger Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard @@ -452,7 +455,7 @@ Sauvegarde en cours En savoir plus sur les seed phrases - Vide + Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret. Votre seed phrase @@ -720,7 +723,6 @@ Échanger ce montant de jetons sélectionnés aura un impact significatif sur les prix et réduira votre résultat. Fonds insuffisants Donner l\'autorisation - En cours Échangez Vous recevez Choisir le jeton diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 5e94e5d5f8..7cb6050f2b 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -114,7 +114,9 @@ プロバイダーへ移動 トークンへ移動 インポート + 進行中 後で + 残り%1$s ロックされています メインネットワーク ネットワーク手数料 @@ -406,6 +408,9 @@ 特定の暗号資産に存在しうるコインまたはトークンの最大数 取引量(24時間) 過去24時間以内に取引された暗号資産の合計額。市場の活発さと流動性を示します。 + これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します + トークンを追加 + お使いのデバイスではNFCが使用できません すべてのカードを保護するには、単一のアクセスコードを設定する必要があります 保護する 後で各カードに個別のアクセスコードを設定できます @@ -684,6 +689,8 @@ ステーキングはロックされています もっとステーキングする %1$sをステーキングし、年間 ~ %2$sを受け取ります + タップしてロック解除 + タップして引き出す %sをステーキングする %sのステーキング解除 ステーキング解約中 @@ -696,6 +703,7 @@ 投票する 投票はロックされています 引き出す + あなたのステーキング カード内に秘密鍵を保管しながら暗号資産を安全に保管します 革新的なハードウェアウォレット 1つのウォレットに最大3枚のカード @@ -719,7 +727,6 @@ 選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。 残高不足 許可を与える - 進行中 スワップ 受け取る トークンを選択 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9011127a90..3cd80972f4 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -84,6 +84,7 @@ Перейти на %1$s Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. Отмена + Получить Вывести награду Закрыть Продолжить @@ -117,7 +118,9 @@ К провайдеру Перейти в токен Импортировать + В процессе Позже + Осталось %1$s Заблокирован Основная сеть Сетевая комиссия @@ -363,7 +366,7 @@ Все - Опытные покупатели + Опытные трейдеры Рейтинг Сортировать по Лидеры роста @@ -377,12 +380,12 @@ На основе %d оценок Веб-сайт - Покупательское предпочтение + Покуп. предпочтение Разница между объемом покупателей и продавцов - Циркулирующее предложение + Циркулир. предл. Общее количество монет, доступных для торговли и находящихся в обращении на рынке Опытные покупкатели - Полностью разбавленная капитализация + Полн. разб. кап. Общая теоретическая стоимость криптовалюты, если все монеты, которые могут существовать, находятся в обращении, включая те, которые в настоящее время не обращаются Дата создания Высокий @@ -394,9 +397,9 @@ Изменение объема ликвидности, доступной для токена в течение указанного периода времени. Индекс ликвидности Низкий - Рыночная капитализация + Рын. кап. Общая рыночная стоимость криптовалюты, рассчитываемая путем умножения текущей цены монеты на общее количество монет в обращении. - Рыночный рейтинг + Рейтинг Максимальный объем Метрики Официальные ссылки @@ -408,6 +411,9 @@ Максимальное количество монет или токенов, которое может когда-либо существовать для выбранной криптовалюты. Объем торгов (24ч) Общая сумма криптовалюты, которая была продана за последние 24 часа, показывающая уровень активности и ликвидности на рынке. + Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из маркета + Добавить токены + Функция NFC недоступна на вашем устройстве Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -652,9 +658,9 @@ Метрики Минимальное количество Нет вознаграждений к получению - Способ возраграждения + Способ вознаграждения Способ получения вознаграждений за стейкинг. Он может быть автоматическим, при котором вознаграждение само зачисляется вам на адрес или в ручную, когда вознаграждение нужно вывести, создав транзакцию на её получение. - Период возрагражения + Период вознагражения Это период, определяющий, когда участники стейкинга получат свои вознаграждения. Вознаграждение для получения: %s Стейкинг %s @@ -662,15 +668,18 @@ Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. Период прогрева Время, необходимое для начала процесса стейкинга и активации процесса начисления наград + Пользуясь стейкинг сервисом, вы соглашаетесь с %1$s и %2$s Заблокировано Переместить Нативный стейкинг + Полученная награда будет отправлена на ваш кошелек и будет сразу доступна для использования Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый день. Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый час. Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый месяц. Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг - Награда перестанет начисляться сразу после того, как вы начнете процесс завершения стейкинга. Процесс завершения длится %s. + Ваши средства будут доступны после %s периода отзыва. + Вы можете вывести свои средства из стейкинга, они будут доступны для использования незамедлительно. Сменить валидатора Застейкать вознаграждения Отозвать @@ -688,6 +697,8 @@ Вознаграждения Стейкинг закрыт Застейкать еще + Нажмите для разблокировки + Нажмите для вывода Застейкать %s Вывести %s Отзыв @@ -723,7 +734,6 @@ Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств Дать разрешение - В процессе Обменять Вы получите Выберите токен @@ -805,7 +815,7 @@ Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. Неверная карта выбрана в приложении Tangem Не удалось создать транзакцию из данных Dapp. Код: %s - Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. + Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. Нет открытых сессий WalletConnect Упс. Нет сессий. Не удалось создать пару WalletConnect: %1$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 96f929750f..d15a97ab14 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -116,6 +116,7 @@ Перейти до провайдера Перейти до токену Імпортувати + процесі Пізніше Заблокований Основна мережа @@ -397,7 +398,7 @@ Низький Ринкова капіталізація Загальна ринкова вартість криптовалюти, що розраховується шляхом множення поточної ціни монети на загальну кількість монет в обігу - Рейтинг ринку + Рейтинг Позиція в крипторейтингу між усіма монетами на основі ринкової капіталізації Максимальна пропозиція Метрики @@ -722,7 +723,6 @@ Обмін цієї кількості обраних токенів призведе до значного впливу на ціну і зменшить вашу кінцеву суму. Недостатньо коштів Надати дозвіл - В процесі Обміняти Ви отримаєте Оберіть токен 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 b860623796..f144efa47f 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -58,6 +58,7 @@ 啟用 錯誤 導入 + 進行中 網路費 OK @@ -271,7 +272,6 @@ 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 賦予權限 - 進行中 交易 選擇代幣 無法使用 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 3999c3c8e3..d4457dd0f7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -116,6 +116,7 @@ Go to provider Go to token Import + In progress Later %1$s left Locked @@ -396,7 +397,7 @@ The change in how much liquidity is available for the token during the specified timeframe Liquidity index Low - Market capitalization + Market cap The total market value of a cryptocurrency, calculated by multiplying the current price of the coin by the total number of coins in circulation Market rating Position in crypto rating between all coins based on market capitalization @@ -666,15 +667,15 @@ Locked Migrate Native staking - Earned rewards are sent directly to your address. Once claimed, the funds are available for use. + Earned rewards will be sent to your wallet and available for use immediately 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 Reinvests your earned rewards in your staked amount, increasing potential earnings. - Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. - Your funds have been unstaked. You can now withdraw them along with your earned rewards. + Your funds will be available for use after the %s unbonding period. + You can now withdraw your funds, it will be available to use immediatly Preparing Ready to withdraw Rebond @@ -700,6 +701,7 @@ Tap to withdraw Stake %s Unstake %s + The transaction is being processed! Validation is currently underway in the blockchain. This may take a few minutes. Unbonding Unlock locked Unstaked @@ -734,7 +736,6 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Give Permission - In progress Swap You receive Choose token diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt index aea7aaa822..0a5cc124d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt @@ -1,7 +1,6 @@ package com.tangem.core.ui.components.transactions import android.content.res.Configuration -import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -13,22 +12,24 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.DateTimeFormatters -import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.core.ui.res.TangemThemePreview /** * Common transaction done screen title * - * @param titleRes title resource - * @param date transaction timestamp in millis + * @param title title resource + * @param subtitle subtitle text */ @Composable -fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifier = Modifier) { +fun TransactionDoneTitle(title: TextReference, subtitle: TextReference, modifier: Modifier = Modifier) { Column( modifier = modifier .fillMaxWidth(), @@ -41,20 +42,17 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie .size(TangemTheme.dimens.size64), ) Text( - text = stringResource(id = titleRes), + text = title.resolveReference(), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, modifier = Modifier .padding(top = TangemTheme.dimens.spacing16), ) Text( - text = stringResource( - id = R.string.send_date_format, - date.toTimeFormat(DateTimeFormatters.dateFormatter), - date.toTimeFormat(), - ), + text = subtitle.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, modifier = Modifier .padding(top = TangemTheme.dimens.spacing4), ) @@ -68,8 +66,8 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie private fun TransactionDoneTitlePreview() { TangemThemePreview { TransactionDoneTitle( - titleRes = R.string.sent_transaction_sent_title, - date = 0, + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = stringReference("0"), modifier = Modifier .background(TangemTheme.colors.background.tertiary) .padding(TangemTheme.dimens.spacing16), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 2576d7b37f..d65fdea64d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -26,8 +26,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.amountScreen.ui.AmountBlock import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.previewdata.ConfirmStatePreviewData @@ -69,8 +73,14 @@ private fun LazyListScope.blocks(uiState: SendUiState) { modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), ) { TransactionDoneTitle( - titleRes = R.string.sent_transaction_sent_title, - date = timestamp, + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + timestamp.toTimeFormat(DateTimeFormatters.dateFormatter), + timestamp.toTimeFormat(), + ), + ), ) } RecipientBlock( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index be78b5321c..55740369f4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -104,7 +104,9 @@ internal class AddStakingNotificationsTransformer( return prevState.copy( confirmationState = confirmationState.copy( notifications = notifications.toImmutableList(), - isPrimaryButtonEnabled = notifications.any { it is StakingNotification.Error }, + isPrimaryButtonEnabled = notifications.none { + it is StakingNotification.Error || it is NotificationUM.Error + }, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 8071e92164..df10d96663 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -9,13 +9,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.common.ui.amountScreen.ui.AmountBlock import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -52,10 +52,9 @@ internal fun StakingConfirmationContent( visible = doneState is TransactionDoneState.Content, modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), ) { - val transactionDoneStateContent = remember(this) { doneState as TransactionDoneState.Content } TransactionDoneTitle( - titleRes = R.string.sent_transaction_sent_title, - date = transactionDoneStateContent.timestamp, + title = resourceReference(R.string.common_in_progress), + subtitle = resourceReference(R.string.staking_transaction_in_progress_text), ) } AmountBlock( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 93802cd66d..cfb15b93e0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -55,10 +55,9 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { when (feeState) { is FeeState.Content -> { val feeAmount = feeState.fee?.amount - val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 SelectorRowItem( - titleRes = title, - iconRes = icon, + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, preDot = stringReference( BigDecimalFormatter.formatCryptoFeeAmount( cryptoAmount = feeAmount?.value, @@ -80,9 +79,19 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { ) } is FeeState.Loading -> { + SelectorRowItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + paddingValues = PaddingValues(), + ) FeeLoading(feeState) } is FeeState.Error -> { + SelectorRowItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + paddingValues = PaddingValues(), + ) FeeError(feeState) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index c9e3891056..b5c056c643 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -18,8 +18,12 @@ import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImage import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.models.SwapSuccessStateHolder import com.tangem.feature.swap.presentation.R @@ -60,7 +64,16 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - TransactionDoneTitle(titleRes = R.string.swapping_success_view_title, date = state.timestamp) + TransactionDoneTitle( + title = resourceReference(R.string.common_in_progress), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + state.timestamp.toTimeFormat(DateTimeFormatters.dateFormatter), + state.timestamp.toTimeFormat(), + ), + ), + ) SpacerH16() InputRowImage( title = TextReference.Res(R.string.swapping_from_title), From 2aa9866f07d7c07c7a673814c70ee7f468f405b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 19:54:56 +0500 Subject: [PATCH 151/197] Updated on 2026-08-14 --- .../com/tangem/common/extensions/KNode.kt | 8 + .../scenarios/OpenMainScreenScenario.kt | 38 +++ .../com/tangem/screens/DetailsTestScreen.kt | 51 ++++ .../com/tangem/screens/DisclaimerScreen.kt | 17 -- .../tangem/screens/DisclaimerTestScreen.kt | 17 ++ .../{WalletScreen.kt => MainTestScreen.kt} | 6 +- ...{StoriesScreen.kt => StoriesTestScreen.kt} | 4 +- .../kotlin/com/tangem/screens/TestTopBar.kt | 16 + .../screens/WalletSettingsTestScreen.kt | 31 ++ .../com/tangem/tests/DetailsScreenTest.kt | 160 ++++++++++ .../kotlin/com/tangem/tests/MainScreenTest.kt | 27 +- .../{ScanEmptyTest.kt => ScanErrorTest.kt} | 34 +-- .../kotlin/com/tangem/tests/StoriesTest.kt | 24 +- .../tap/domain/sdk/mocks/MockProvider.kt | 2 + .../sdk/mocks/content/NoteMockContent.kt | 117 ++++++++ .../sdk/mocks/content/Wallet2MockContent.kt | 283 +++++++++++++++++- .../java/com/tangem/core/ui/test/TestTags.kt | 13 +- .../features/details/ui/DetailsScreen.kt | 6 +- .../disclaimer/impl/ui/DisclaimerScreen.kt | 7 +- .../walletsettings/ui/WalletSettingsScreen.kt | 5 +- .../presentation/wallet/ui/WalletScreen.kt | 2 +- .../ui/components/common/WalletTopBar.kt | 7 +- .../DefaultBlockchainSDKFactory.kt | 4 + .../plugin/configuration/model/AppConfig.kt | 1 + 24 files changed, 779 insertions(+), 101 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt rename app/src/androidTest/kotlin/com/tangem/screens/{WalletScreen.kt => MainTestScreen.kt} (58%) rename app/src/androidTest/kotlin/com/tangem/screens/{StoriesScreen.kt => StoriesTestScreen.kt} (87%) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt rename app/src/androidTest/kotlin/com/tangem/tests/{ScanEmptyTest.kt => ScanErrorTest.kt} (56%) create mode 100644 app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt new file mode 100644 index 0000000000..a7baa714a4 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt @@ -0,0 +1,8 @@ +package com.tangem.common.extensions + +import io.github.kakaocup.compose.node.element.KNode + +fun KNode.clickWithAssertion() { + assertIsDisplayed() + performClick() +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt new file mode 100644 index 0000000000..0c6b1790ea --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt @@ -0,0 +1,38 @@ +package com.tangem.scenarios + +import androidx.compose.ui.test.junit4.ComposeTestRule +import com.kaspersky.kaspresso.testcases.api.scenario.Scenario +import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.domain.models.scan.ProductType +import com.tangem.screens.DisclaimerTestScreen +import com.tangem.screens.MainTestScreen +import com.tangem.screens.StoriesTestScreen +import com.tangem.tap.domain.sdk.mocks.MockProvider +import io.github.kakaocup.compose.node.element.ComposeScreen + +class OpenMainScreenScenario( + private val testRule: ComposeTestRule, + private val productType: ProductType? = null, +) : Scenario() { + override val steps: TestContext.() -> Unit = { + if (productType != null) { + MockProvider.setMocks(productType) + } + ComposeScreen.onComposeScreen(testRule) { + step("Click on \"Accept\" button") { + acceptButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(testRule) { + step("Click on \"Scan\" button") { + scanButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(testRule) { + step("Make sure wallet screen is visible") { + assertIsDisplayed() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt new file mode 100644 index 0000000000..528989a4e3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt @@ -0,0 +1,51 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class DetailsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.DETAILS_SCREEN) } + ) { + + val walletConnectButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.wallet_connect_title)) + } + + private val walletBlock: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + } + + val walletNameButton: KNode = walletBlock.child { + hasClickAction() + hasPosition(0) + } + + val scanCardButton: KNode = walletBlock.child { + hasText(getResourceString(R.string.scan_card_settings_button)) + } + + val buyTangemButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.details_buy_wallet)) + } + + val appSettingsButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.app_settings_title)) + } + val contactSupportButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.details_row_title_contact_to_support)) + } + val toSButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.disclaimer_title)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt deleted file mode 100644 index 51b94ab791..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.screens - -import com.kaspersky.kaspresso.screens.KScreen -import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment -import com.tangem.wallet.R -import io.github.kakaocup.kakao.text.KButton - -object DisclaimerScreen : KScreen(){ - - override val layoutId = R.layout.fragment_disclaimer - - override val viewClass = DisclaimerFragment::class.java - - val acceptButton: KButton = KButton { - withId(R.id.btn_accept) - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt new file mode 100644 index 0000000000..4af7f33d51 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt @@ -0,0 +1,17 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class DisclaimerTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.DISCLAIMER_SCREEN_CONTAINER) } + ) { + + val acceptButton: KNode = child { + hasTestTag(TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainTestScreen.kt similarity index 58% rename from app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt rename to app/src/androidTest/kotlin/com/tangem/screens/MainTestScreen.kt index 0988625537..a342754478 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainTestScreen.kt @@ -4,8 +4,8 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.core.ui.test.TestTags import io.github.kakaocup.compose.node.element.ComposeScreen -class WalletScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen( +class MainTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( semanticsProvider = semanticsProvider, - viewBuilderAction = { hasTestTag(TestTags.WALLET_SCREEN) } + viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN) } ) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesTestScreen.kt similarity index 87% rename from app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt rename to app/src/androidTest/kotlin/com/tangem/screens/StoriesTestScreen.kt index cda17d33c2..9dd1a415e9 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/StoriesTestScreen.kt @@ -8,8 +8,8 @@ import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.views.KView import io.github.kakaocup.kakao.text.KButton -class StoriesScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen( +class StoriesTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( semanticsProvider = semanticsProvider, viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) } ) { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt b/app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt new file mode 100644 index 0000000000..83d2e8a6a9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt @@ -0,0 +1,16 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class TestTopBar(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN_TOP_BAR) } + ) { + val moreButton: KNode = child { + hasTestTag(TestTags.MAIN_SCREEN_MORE_BUTTON) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt new file mode 100644 index 0000000000..1b31bd509c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt @@ -0,0 +1,31 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class WalletSettingsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.WALLET_SETTINGS_SCREEN) } + ) { + private val walletSettingsItem: KNode = child { + hasTestTag(TestTags.WALLET_SETTINGS_SCREEN_ITEM) + } + + val linkMoreCardsButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.details_row_title_create_backup)) + } + val cardSettingsButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.card_settings_title)) + } + val referralProgramButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.details_referral_title)) + } + val forgetWalletButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.settings_forget_wallet)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt new file mode 100644 index 0000000000..8c4bd6d3c9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt @@ -0,0 +1,160 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.domain.models.scan.ProductType +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.DetailsTestScreen +import com.tangem.screens.TestTopBar +import com.tangem.screens.WalletSettingsTestScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.compose.node.element.ComposeScreen +import org.junit.Test + +@HiltAndroidTest +class DetailsScreenTest : BaseTestCase() { + + @Test + fun walletWithoutBackupDetails() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule)) + ComposeScreen.onComposeScreen(composeTestRule) { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert wallet connect button is visible") { + walletConnectButton.assertIsDisplayed() + } + step("Assert scan card button is visible") { + scanCardButton.assertIsDisplayed() + } + step("Assert buy Tangem card button is visible") { + buyTangemButton.assertIsDisplayed() + } + step("Assert app settings button is visible") { + appSettingsButton.assertIsDisplayed() + } + step("Assert contact support button is visible") { + contactSupportButton.assertIsDisplayed() + } + step("Assert terms or service button is visible") { + toSButton.assertIsDisplayed() + } + step("Open wallet settings screen") { + walletNameButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert Link more cards button is visible") { + linkMoreCardsButton.assertIsDisplayed() + } + step("Assert Card Settings button is visible") { + cardSettingsButton.assertIsDisplayed() + } + step("Assert Referral program button is visible") { + referralProgramButton.assertIsDisplayed() + } + step("Assert Forget wallet button is visible") { + forgetWalletButton.assertIsDisplayed() + } + } + } + + @Test + fun wallet2Details() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2)) + ComposeScreen.onComposeScreen(composeTestRule) { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert wallet connect button is visible") { + walletConnectButton.assertIsDisplayed() + } + step("Assert scan card button is visible") { + scanCardButton.assertIsDisplayed() + } + step("Assert buy Tangem card button is visible") { + buyTangemButton.assertIsDisplayed() + } + step("Assert app settings button is visible") { + appSettingsButton.assertIsDisplayed() + } + step("Assert contact support button is visible") { + contactSupportButton.assertIsDisplayed() + } + step("Assert terms or service button is visible") { + toSButton.assertIsDisplayed() + } + step("Open wallet settings screen") { + walletNameButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert Link more cards button does not exist") { + linkMoreCardsButton.assertIsNotDisplayed() + } + step("Assert Card Settings button is visible") { + cardSettingsButton.assertIsDisplayed() + } + step("Assert Referral program button is visible") { + referralProgramButton.assertIsDisplayed() + } + step("Assert Forget wallet button is visible") { + forgetWalletButton.assertIsDisplayed() + } + } + } + + @Test + fun noteDetails() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note)) + ComposeScreen.onComposeScreen(composeTestRule) { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert wallet connect button does not exist") { + walletConnectButton.assertIsNotDisplayed() + } + step("Assert scan card button is visible") { + scanCardButton.assertIsDisplayed() + } + step("Assert buy Tangem card button is visible") { + buyTangemButton.assertIsDisplayed() + } + step("Assert app settings button is visible") { + appSettingsButton.assertIsDisplayed() + } + step("Assert contact support button is visible") { + contactSupportButton.assertIsDisplayed() + } + step("Assert terms or service button is visible") { + toSButton.assertIsDisplayed() + } + step("Open wallet settings screen") { + walletNameButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert Link more cards button does not exist") { + linkMoreCardsButton.assertIsNotDisplayed() + } + step("Assert Card Settings button is visible") { + cardSettingsButton.assertIsDisplayed() + } + step("Assert Referral program button does not exist") { + referralProgramButton.assertIsNotDisplayed() + } + step("Assert Forget wallet button is visible") { + forgetWalletButton.assertIsDisplayed() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt index dcebde4556..98e9cad75c 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt @@ -1,11 +1,8 @@ package com.tangem.tests import com.tangem.common.BaseTestCase -import com.tangem.screens.DisclaimerScreen -import com.tangem.screens.StoriesScreen -import com.tangem.screens.WalletScreen +import com.tangem.scenarios.OpenMainScreenScenario import dagger.hilt.android.testing.HiltAndroidTest -import io.github.kakaocup.compose.node.element.ComposeScreen import org.junit.Test @HiltAndroidTest @@ -14,26 +11,6 @@ class MainScreenTest : BaseTestCase() { @Test fun goToMain() = setupHooks().run { - ComposeScreen.onComposeScreen(composeTestRule) { - step("Click on \"Scan\" button") { - scanButton { - assertIsDisplayed() - performClick() - } - } - } - DisclaimerScreen { - step("Click on \"Accept\" button") { - acceptButton { - isVisible() - click() - } - } - } - ComposeScreen.onComposeScreen(composeTestRule) { - step("Make sure wallet screen is visible") { - assertIsDisplayed() - } - } + scenario(OpenMainScreenScenario(composeTestRule)) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanEmptyTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt similarity index 56% rename from app/src/androidTest/kotlin/com/tangem/tests/ScanEmptyTest.kt rename to app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt index a1d67d8c1c..da686950a7 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/ScanEmptyTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt @@ -1,9 +1,10 @@ package com.tangem.tests import com.tangem.common.BaseTestCase -import com.tangem.screens.DisclaimerScreen -import com.tangem.screens.StoriesScreen -import com.tangem.screens.WalletScreen +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.screens.DisclaimerTestScreen +import com.tangem.screens.MainTestScreen +import com.tangem.screens.StoriesTestScreen import com.tangem.tap.domain.sdk.mocks.MockProvider import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.compose.node.element.ComposeScreen @@ -15,31 +16,22 @@ class ScanErrorTest : BaseTestCase() { @Test fun goToMain() = setupHooks().run { - ComposeScreen.onComposeScreen(composeTestRule) { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Accept\" button") { + acceptButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { step("Click on \"Scan\" button emulating scan error") { MockProvider.setEmulateError() - scanButton { - assertIsDisplayed() - performClick() - } + scanButton.clickWithAssertion() } step("Click on \"Scan\" button again without emulating error") { MockProvider.resetEmulateError() - scanButton { - assertIsDisplayed() - performClick() - } + scanButton.clickWithAssertion() } } - DisclaimerScreen { - step("Click on \"Accept\" button") { - acceptButton { - isVisible() - click() - } - } - } - ComposeScreen.onComposeScreen(composeTestRule) { + ComposeScreen.onComposeScreen(composeTestRule) { step("Make sure wallet screen is visible") { assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index d6418c9364..9eb88b0075 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -2,7 +2,9 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW import com.tangem.common.BaseTestCase -import com.tangem.screens.StoriesScreen +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.screens.DisclaimerTestScreen +import com.tangem.screens.StoriesTestScreen import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.compose.node.element.ComposeScreen @@ -13,22 +15,16 @@ import org.junit.Test class StoriesTest : BaseTestCase() { @Test - fun clickOnButtons() = + fun clickOnOrderButton() = setupHooks().run { - ComposeScreen.onComposeScreen(composeTestRule) { - step("Click on \"Scan\" button") { - scanButton { - assertIsDisplayed() - performClick() - } - } - step("Assert: \"Scan card\" popup opened") { - enableNFCAlert.isDisplayed() - cancelButton.click() - device.uiDevice.pressBack() + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Accept\" button") { + acceptButton.clickWithAssertion() } + } + ComposeScreen.onComposeScreen(composeTestRule) { step("Click on \"Order\" button") { - orderButton.performClick() + orderButton.clickWithAssertion() } step("Assert: browser opened") { val expectedIntent = KIntent { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt index 9468912e1c..3811371aa7 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.domain.models.scan.ProductType +import com.tangem.tap.domain.sdk.mocks.content.NoteMockContent import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @@ -67,6 +68,7 @@ object MockProvider { return when (productType) { ProductType.Wallet -> WalletMockContent ProductType.Wallet2 -> Wallet2MockContent + ProductType.Note -> NoteMockContent else -> TODO() } } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt new file mode 100644 index 0000000000..de2da2c546 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt @@ -0,0 +1,117 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import java.util.Date + +object NoteMockContent : MockContent { + + override val cardDto = CardDTO( + cardId = "AB04000000010905", + batchId = "AB04", + cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119), + firmwareVersion = CardDTO.FirmwareVersion( + major = 4, + minor = 39, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1649635200000), + signature = byteArrayOf(-2, -45, 100, -79, 7, -120, 49, 74, 126, -3, -75, 54, -36, -1, 19, -83, 47, -82, 52, -43, -119, 75, 58, 97, 50, 37, 103, -6, -2, 28, 120, 103, -38, -95, 7, -126, -3, -23, -22, -30, -24, -126, 3, 70, 7, -20, -6, -49, 55, -93, 26, 57, -71, 12, -20, 41, -105, -75, 82, 116, 12, -75, -22, 55), + ), + issuer = CardDTO.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 1, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = false, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = false, + isHDWalletAllowed = false, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Secp256r1, + ), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, -27, -117, 23, 68, -3, 21, -109, 18, -67, -107, -42, -44, -16, -127, -53, 46, -109, -46, -51, 89, 119, 79, 111, 78, 62, -125, 72, 109, 8, 45, 59, 117), + chainCode = byteArrayOf(), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 3, + remainingSignatures = null, + index = 0, + hasBackup = false, + derivedKeys = emptyMap(), + extendedPublicKey = null, + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.NoBackup, + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Note, + walletData = WalletData(blockchain = "DOGE", token = null), + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse( + entries = emptyMap(), + ) + + override val extendedPublicKey + get() = error("Available only for wallet+?") + + override val successResponse = SuccessResponse(cardId = "AB04000000010905") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt index 2b9a29b938..4920acf082 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt @@ -1,33 +1,292 @@ package com.tangem.tap.domain.sdk.mocks.content import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.tap.domain.sdk.mocks.MockContent import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import java.util.Date object Wallet2MockContent : MockContent { - override val scanResponse: ScanResponse - get() = TODO("Not yet implemented") + private val primaryCard = PrimaryCard( + cardId = "AF10000000981426", + batchId = "AF10", + cardPublicKey = byteArrayOf(3, 7, 80, -118, 6, 77, -15, -22, 107, 105, -64, 103, 77, -79, -102, 106, 46, 84, 21, -34, 47, -74, -56, 124, 17, -49, -29, 76, 84, 59, 50, -15, -52), + linkingKey = byteArrayOf( // + 2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121, + -57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91, + ), + existingWalletsCount = 5, isHDWalletAllowed = true, + issuer = Card.Issuer( + name = "TANGEM 2.0", + publicKey = byteArrayOf(2, -120, 89, -52, -60, 36, -73, -17, 103, 107, -110, -36, 3, 110, -122, 72, 43, -38, 8, 30, -50, 25, -23, -17, 38, 94, 5, -112, -20, 9, 54, -24, -32), + ), + walletCurves = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Secp256r1, + EllipticCurve.Ed25519Slip0010, + EllipticCurve.Bls12381G2, + EllipticCurve.Bls12381G2Pop, + EllipticCurve.Bip0340, + ), + firmwareVersion = FirmwareVersion( + major = 6, + minor = 33, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + isKeysImportAllowed = false, + certificate = null, + ) - override val cardDto: CardDTO - get() = TODO("Not yet implemented") + override val cardDto = CardDTO( + cardId = "AF10000000981426", + batchId = "AF10", + cardPublicKey = byteArrayOf(3, 7, 80, -118, 6, 77, -15, -22, 107, 105, -64, 103, 77, -79, -102, 106, 46, 84, 21, -34, 47, -74, -56, 124, 17, -49, -29, 76, 84, 59, 50, -15, -52), + firmwareVersion = CardDTO.FirmwareVersion( + major = 6, + minor = 33, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1698094800000), + signature = byteArrayOf(71, -20, 9, 31, 25, 111, 61, 119, 109, -123, -63, 51, 58, -71, -44, 53, 57, 20, -16, 97, -87, -82, 1, -35, -48, 63, 77, -78, -89, -112, -27, 25, -10, 90, 7, -53, -84, -125, 112, 68, -14, -85, -100, -64, -115, 31, 42, 119, 87, -79, 127, 42, -87, -102, 13, -9, -10, -51, -29, -63, -1, -52, -117, 57), + ), + issuer = CardDTO.Issuer( + name = "TANGEM 2.0", + publicKey = byteArrayOf(2, -120, 89, -52, -60, 36, -73, -17, 103, 107, -110, -36, 3, 110, -122, 72, 43, -38, 8, 30, -50, 25, -23, -17, 38, 94, 5, -112, -20, 9, 54, -24, -32), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 20, + isSettingAccessCodeAllowed = true, + isSettingPasscodeAllowed = true, + isResettingUserCodesAllowed = false, + isLinkedTerminalEnabled = true, + isBackupAllowed = true, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = true, + isHDWalletAllowed = true, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = true, + isPasscodeSet = false, + supportedCurves = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Secp256r1, + EllipticCurve.Ed25519Slip0010, + EllipticCurve.Bls12381G2, + EllipticCurve.Bls12381G2Pop, + EllipticCurve.Bip0340, + ), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, -114, -64, 120, -121, 11, -28, 89, -91, 114, 10, 84, -87, -36, 36, 19, -69, 95, 66, 14, 32, -35, -99, -67, 118, 51, 26, 71, -78, -36, 59, -126, -58), + chainCode = byteArrayOf(-47, -8, 74, 69, -1, 52, 10, -56, -85, 118, 56, 77, 125, -12, 85, -23, 42, -58, 99, 47, -87, -34, -83, 72, -122, -29, 88, -85, 46, -118, -26, 116), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 73, + remainingSignatures = null, + index = 0, + hasBackup = true, + derivedKeys = mapOf( + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, 55, -114, -94, -73, -61, -50, 51, -115, 55, -79, 63, -96, -44, -64, -24, -36, -122, -123, -38, 81, -15, -127, -97, -42, 72, -85, -62, -98, 46, 119, 16, -55), + chainCode = byteArrayOf(31, 17, 71, -28, -29, 17, 72, -29, -98, 112, 31, -8, 72, -75, 4, -11, 60, -100, 9, 35, 58, -42, -38, 96, -71, -68, 24, -119, -43, -18, -122, 72), + ), + DerivationPath("m/84'/0'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -26, 103, 96, 112, 127, -125, 0, 7, 53, 30, -12, -82, 45, 14, 107, -9, 126, 75, 104, -67, -49, 35, -12, -82, -90, 101, -101, 125, -76, 88, -54, 99), + chainCode = byteArrayOf(-42, 36, 97, 65, -64, -113, 76, -91, -9, 11, 89, 123, -9, -3, 21, 103, -113, -60, 48, -31, -34, 108, 111, -38, -110, -80, 109, 17, -29, 2, 45, -71), + ), + DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -96, -3, -83, 101, 63, -25, -125, -4, -65, -42, -56, 24, -52, 118, -11, -104, -105, 40, -59, 20, -109, -97, 29, -95, -6, -80, 2, 67, 103, -80, -22, -94), + chainCode = byteArrayOf(-125, 27, -91, 38, -66, 109, -92, 16, -37, 93, 107, -29, -128, -1, 115, -64, 108, -63, 17, 27, 58, 78, -2, 39, 88, -39, 44, 89, 32, -38, -16, -38), + ), + ), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(2, -114, -64, 120, -121, 11, -28, 89, -91, 114, 10, 84, -87, -36, 36, 19, -69, 95, 66, 14, 32, -35, -99, -67, 118, 51, 26, 71, -78, -36, 59, -126, -58), + chainCode = byteArrayOf(-47, -8, 74, 69, -1, 52, 10, -56, -85, 118, 56, 77, 125, -12, 85, -23, 42, -58, 99, 47, -87, -34, -83, 72, -122, -29, 88, -85, 46, -118, -26, 116), + ), + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(-64, 18, -128, 121, -89, -37, -99, 44, -125, -72, -111, -79, 7, 85, 40, 67, -39, 117, 123, 11, 105, -6, -5, -79, 19, -10, -29, 20, -14, -40, 5, 90), + chainCode = byteArrayOf(33, -97, 53, -112, 61, 112, -24, 74, -87, -85, -124, -4, 103, -94, -97, 76, -41, -27, 118, 33, 55, 121, -17, -52, 60, 122, 27, 25, 29, -76, 78, 11), + curve = EllipticCurve.Ed25519, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 1, + hasBackup = true, + derivedKeys = emptyMap(), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(-64, 18, -128, 121, -89, -37, -99, 44, -125, -72, -111, -79, 7, 85, 40, 67, -39, 117, 123, 11, 105, -6, -5, -79, 19, -10, -29, 20, -14, -40, 5, 90), + chainCode = byteArrayOf(33, -97, 53, -112, 61, 112, -24, 74, -87, -85, -124, -4, 103, -94, -97, 76, -41, -27, 118, 33, 55, 121, -17, -52, 60, 122, 27, 25, 29, -76, 78, 11), + ), + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(-94, 111, -89, -77, -62, -113, 16, -118, -46, -16, -20, 28, 53, -82, -109, 28, 99, -98, -54, 59, -3, 99, 16, -70, -73, 43, -6, 33, -53, -66, 76, -72, 6, -49, 83, -121, -122, 111, -111, -116, -119, 98, -94, -98, -121, -37, 20, -95), + chainCode = null, + curve = EllipticCurve.Bls12381G2Aug, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 1, + remainingSignatures = null, + index = 2, + hasBackup = true, + derivedKeys = emptyMap(), + extendedPublicKey = null, + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(115, 14, 11, 0, -93, 81, -103, -95, -75, -84, 18, -120, -31, 76, -83, -81, 91, 25, -75, 36, -99, -53, -25, -15, -1, -57, 14, -39, 98, -116, -63, -123), + chainCode = byteArrayOf(23, 5, 38, -48, 67, -42, -31, -21, 89, 11, 22, -28, 44, -19, -115, -78, 123, -27, 57, 57, -24, -86, 55, 15, 104, 114, -36, 80, 81, -108, -41, 112), + curve = EllipticCurve.Bip0340, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 3, + hasBackup = true, + derivedKeys = emptyMap(), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(115, 14, 11, 0, -93, 81, -103, -95, -75, -84, 18, -120, -31, 76, -83, -81, 91, 25, -75, 36, -99, -53, -25, -15, -1, -57, 14, -39, 98, -116, -63, -123), + chainCode = byteArrayOf(23, 5, 38, -48, 67, -42, -31, -21, 89, 11, 22, -28, 44, -19, -115, -78, 123, -27, 57, 57, -24, -86, 55, 15, 104, 114, -36, 80, 81, -108, -41, 112), + ), + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(-43, 1, -81, -47, -8, -103, -66, 42, 37, -7, 65, 54, 57, -24, 127, -89, 69, -112, 42, -46, -128, 36, -117, -28, 30, -48, 37, 52, 93, -47, 92, -47), + chainCode = byteArrayOf(4, -97, 81, -37, 76, -67, -87, -4, -82, -36, -45, -28, -117, -59, -62, 93, -73, 50, 65, -91, -83, 25, -95, 89, -64, -40, 113, 28, 59, 113, -99, 89), + curve = EllipticCurve.Ed25519Slip0010, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 15, + remainingSignatures = null, + index = 4, + hasBackup = true, + derivedKeys = emptyMap(), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(-43, 1, -81, -47, -8, -103, -66, 42, 37, -7, 65, 54, 57, -24, 127, -89, 69, -112, 42, -46, -128, 36, -117, -28, 30, -48, 37, 52, 93, -47, 92, -47), + chainCode = byteArrayOf(4, -97, 81, -37, 76, -67, -87, -4, -82, -36, -45, -28, -117, -59, -62, 93, -73, 50, 65, -91, -83, 25, -95, 89, -64, -40, 113, 28, 59, 113, -99, 89), + ), + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.Active(1), + ) - override val derivationTaskResponse: DerivationTaskResponse - get() = TODO("Not yet implemented") + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Wallet2, + walletData = null, + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) - override val extendedPublicKey: ExtendedPublicKey - get() = TODO("Not yet implemented") + override val derivationTaskResponse = DerivationTaskResponse( + entries = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch + publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121), + chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge + publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124), + chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + ) - override val successResponse: SuccessResponse - get() = TODO("Not yet implemented") + override val extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110), + chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ) - override val createProductWalletTaskResponse: CreateProductWalletTaskResponse - get() = TODO("Not yet implemented") + override val successResponse = SuccessResponse(cardId = "AF10000000981426") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + primaryCard = primaryCard, + ) override val importWalletResponse: CreateProductWalletTaskResponse get() = TODO("Not yet implemented") diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt index 8548896b31..d7de6de18e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt @@ -5,5 +5,16 @@ object TestTags { const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" - const val WALLET_SCREEN = "WALLET_SCREEN_CONTAINER" + const val MAIN_SCREEN = "MAIN_SCREEN_CONTAINER" + const val MAIN_SCREEN_MORE_BUTTON = "MAIN_SCREEN_MORE_BUTTON" + const val MAIN_SCREEN_TOP_BAR = "MAIN_SCREEN_TOP_BAR" + + const val DETAILS_SCREEN = "DETAILS_SCREEN_CONTAINER" + const val DETAILS_SCREEN_ITEM = "DETAILS_SCREEN_ITEM" + + const val WALLET_SETTINGS_SCREEN = "WALLET_SETTINGS_SCREEN" + const val WALLET_SETTINGS_SCREEN_ITEM = "WALLET_SETTINGS_SCREEN_ITEM" + + const val DISCLAIMER_SCREEN_CONTAINER = "DISCLAIMER_SCREEN_CONTAINER" + const val DISCLAIMER_SCREEN_ACCEPT_BUTTON = "DISCLAIMER_SCREEN_ACCEPT_BUTTON" } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 9132fff08b..98302fc6f7 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -27,6 +28,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags import com.tangem.features.details.component.preview.PreviewDetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsItemUM @@ -74,7 +76,7 @@ private fun Content( modifier: Modifier = Modifier, ) { LazyColumn( - modifier = modifier, + modifier = modifier.testTag(TestTags.DETAILS_SCREEN), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), contentPadding = PaddingValues( top = TangemTheme.dimens.spacing12, @@ -125,7 +127,7 @@ private fun Block( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, ) { - val itemModifier = Modifier.fillMaxWidth() + val itemModifier = Modifier.fillMaxWidth().testTag(TestTags.DETAILS_SCREEN_ITEM) when (model) { is DetailsItemUM.Basic -> { diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 208feec00e..f3fe9913c7 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -34,6 +35,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON +import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_CONTAINER import com.tangem.features.disclaimer.impl.R import com.tangem.features.disclaimer.impl.entity.DisclaimerUM import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer @@ -57,7 +60,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { Box( modifier = Modifier .background(backgroundColor) - .statusBarsPadding(), + .statusBarsPadding() + .testTag(DISCLAIMER_SCREEN_CONTAINER), ) { Column( modifier = Modifier @@ -158,6 +162,7 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { disabledContentColor = TangemColorPalette.Dark6, ), modifier = Modifier + .testTag(DISCLAIMER_SCREEN_ACCEPT_BUTTON) .align(Alignment.BottomCenter) .navigationBarsPadding() .padding( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index 29223948a2..e2d06d71c8 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +24,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags import com.tangem.feature.walletsettings.component.preview.PreviewWalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM @@ -55,7 +57,7 @@ internal fun WalletSettingsScreen( }, content = { paddingValues -> Content( - modifier = Modifier.padding(paddingValues), + modifier = Modifier.padding(paddingValues).testTag(TestTags.WALLET_SETTINGS_SCREEN), state = state, ) @@ -89,6 +91,7 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { val itemModifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth() + .testTag(TestTags.WALLET_SETTINGS_SCREEN_ITEM) when (item) { is WalletSettingsItemUM.WithItems -> ItemsBlock( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 1250ea7853..1c17411267 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -164,7 +164,7 @@ private fun WalletContent( LazyColumn( modifier = Modifier .fillMaxSize() - .testTag(TestTags.WALLET_SCREEN), + .testTag(TestTags.MAIN_SCREEN), contentPadding = PaddingValues( bottom = TangemTheme.dimens.spacing92 + bottomBarHeight, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index c8e9a69c9d..ebb2fdcfa9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -3,10 +3,14 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import android.content.res.Configuration import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags.MAIN_SCREEN_MORE_BUTTON +import com.tangem.core.ui.test.TestTags.MAIN_SCREEN_TOP_BAR import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig @@ -24,7 +28,7 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { Icon(painter = painterResource(id = R.drawable.img_tangem_logo_90_24), contentDescription = null) }, actions = { - IconButton(onClick = config.onDetailsClick) { + IconButton(onClick = config.onDetailsClick, modifier = Modifier.testTag(MAIN_SCREEN_MORE_BUTTON)) { Icon(painter = painterResource(id = R.drawable.ic_more_vertical_24), contentDescription = null) } }, @@ -34,6 +38,7 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { actionIconContentColor = TangemTheme.colors.icon.primary1, ), scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), + modifier = Modifier.testTag(MAIN_SCREEN_TOP_BAR), ) } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index 8fb67b9107..2172977e82 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -45,6 +45,10 @@ internal class DefaultBlockchainSDKFactory( private val mainScope = CoroutineScope(dispatchers.main) private val walletManagerFactory: Flow = createWalletManagerFactory() + // TODO: [REDACTED_JIRA] + // private val walletManagerFactory: Flow by lazy(LazyThreadSafetyMode.NONE) { + // createWalletManagerFactory() + // } override suspend fun init() { coroutineScope { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index ad8aa17560..3db7478662 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -4,6 +4,7 @@ internal object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 const val versionName = "1.0.0-SNAPSHOT" + // const val versionName = "100.0.0-SNAPSHOT" //TODO: [REDACTED_JIRA] const val minSdkVersion = 24 const val targetSdkVersion = 34 const val compileSdkVersion = 34 From f5c4e4a0ccda13df118b6407d86db8efad311483 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 22:46:59 +0300 Subject: [PATCH 152/197] Updated on 2026-08-14 --- .../staking/impl/presentation/ui/StakingInitialInfoContent.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 15aeef6efe..b9b3c42672 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 @@ -167,8 +167,7 @@ private fun LazyListScope.activeStakingBlock( items = state.yieldBalance.balance, key = { // Staked balance does not have unique identifier. - // Balance type and group id is used to identify item. - it.id + it.type + it.toString() }, ) { balance -> ActiveStakingBlock( From 830bd014284744a0af9f868164345c3d972c04fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 14:46:54 +0300 Subject: [PATCH 153/197] Updated on 2026-08-14 --- .../walletconnect/WalletConnectSdkHelper.kt | 2 +- .../assets/configs/feature_toggles_config.json | 4 ++++ .../impl/presentation/state/fee/FeeConverter.kt | 5 ++++- .../state/fee/SendFeeCustomFieldConverter.kt | 2 +- .../fee/custom/EthereumCustomFeeConverter.kt | 9 ++++++--- .../feature/swap/domain/SwapInteractorImpl.kt | 16 ++++++++-------- gradle/dependencies.toml | 2 +- .../blockchainsdk/WalletManagerFactoryCreator.kt | 6 ++++-- .../BlockchainSDKFeatureToggles.kt | 5 ++++- .../DefaultBlockchainSDKFeatureToggles.kt | 8 ++++++-- 10 files changed, 39 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index bd7f90f809..8c78ffa3a5 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -89,7 +89,7 @@ class WalletConnectSdkHelper { } else { feeAmount } - Fee.Ethereum(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) + Fee.Ethereum.Legacy(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) } else { Fee.Common(feeAmount) } diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index b8b2cfbf61..652af05e72 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -34,5 +34,9 @@ { "name": "NEW_MANAGE_TOKENS", "version": "5.15.0" + }, + { + "name": "IS_ETHEREUM_EIP_1559_ENABLED", + "version": "5.16.0" } ] diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index 670da73277..42ed64cd0e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -73,7 +73,10 @@ internal class FeeConverter( normalFee } else { when (normalFee) { - is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) + is Fee.Ethereum.Legacy -> ethereumCustomFeeConverter.convertBack( + normalFee = normalFee, + value = customValues, + ) is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) is Fee.Kaspa -> kaspaCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) else -> { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index d80cf917b0..5902d63552 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -50,7 +50,7 @@ internal class SendFeeCustomFieldConverter( override fun convert(value: Fee): ImmutableList { return when (value) { - is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value) + is Fee.Ethereum.Legacy -> ethereumCustomFeeConverter.convert(value) is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value) is Fee.Kaspa -> kaspaCustomFeeConverter.convert(value) else -> persistentListOf() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index fdee783db4..b44dc65a5b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -27,9 +27,9 @@ internal class EthereumCustomFeeConverter( private val stateRouterProvider: Provider, private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, -) : CustomFeeConverter { +) : CustomFeeConverter { - override fun convert(value: Fee.Ethereum): ImmutableList { + override fun convert(value: Fee.Ethereum.Legacy): ImmutableList { val feeValue = value.amount.value val feeCurrency = feeCryptoCurrencyStatusProvider()?.value return persistentListOf( @@ -91,7 +91,10 @@ internal class EthereumCustomFeeConverter( ) } - override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList): Fee.Ethereum { + override fun convertBack( + normalFee: Fee.Ethereum.Legacy, + value: ImmutableList, + ): Fee.Ethereum.Legacy { val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals) val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 5ef36ed16a..b892338148 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -879,7 +879,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return when { blockchain.isEvm() -> { val feeAmountWithDecimals = feeAmountValue.movePointRight(fee.decimals) - Fee.Ethereum( + Fee.Ethereum.Legacy( amount = feeAmount, gasLimit = fee.gasLimit.toBigInteger(), gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(), @@ -1308,7 +1308,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId = networkId, transaction = transaction, fromToken = fromToken.currency, - cardId = userWallet?.scanResponse?.card?.cardId, + cardId = userWallet.scanResponse.card.cardId, ) ) { is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) @@ -1503,8 +1503,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapAmount = swapAmount, spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, ) - val cardId = userWallet?.scanResponse?.card?.cardId - val feeData = if (cardId != null && isDemoCardUseCase(cardId)) { + val cardId = userWallet.scanResponse.card.cardId + val feeData = if (isDemoCardUseCase(cardId)) { getDemoFees(fromTokenStatus.currency) } else { try { @@ -1805,10 +1805,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( val increasedAmount = this.amount.copy( value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals), ) - return this.copy( - amount = increasedAmount, - gasLimit = increasedGasLimit, - ) + return when (this) { + is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + } } private fun hasOutgoingTransaction(cryptoCurrencyStatuses: CryptoCurrencyStatus): Boolean { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 9fc95abab5..f6d9a1fde9 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-759" +tangemBlockchainSdk = "develop-762" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-380" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 3e8ec9fb13..4b8a887b99 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -23,7 +23,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - @Suppress("UnusedPrivateMember") private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, + private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -33,7 +33,9 @@ internal class WalletManagerFactoryCreator @Inject constructor( config = config, blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, - featureToggles = BlockchainFeatureToggles(isCardanoTokenSupport = true), + featureToggles = BlockchainFeatureToggles( + isEthereumEIP1559Enabled = blockchainSDKFeatureToggles.isEthereumEIP1559Enabled, + ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), ) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt index de92518ae5..482df0cd5a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt @@ -1,3 +1,6 @@ package com.tangem.blockchainsdk.featuretoggles -internal interface BlockchainSDKFeatureToggles \ No newline at end of file +internal interface BlockchainSDKFeatureToggles { + + val isEthereumEIP1559Enabled: Boolean +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt index 3d2504e78c..29df94634c 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt @@ -3,5 +3,9 @@ package com.tangem.blockchainsdk.featuretoggles import com.tangem.core.featuretoggle.manager.FeatureTogglesManager internal class DefaultBlockchainSDKFeatureToggles( - @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager, -) : BlockchainSDKFeatureToggles \ No newline at end of file + private val featureTogglesManager: FeatureTogglesManager, +) : BlockchainSDKFeatureToggles { + + override val isEthereumEIP1559Enabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "IS_ETHEREUM_EIP_1559_ENABLED") +} \ No newline at end of file From c08f69995b466f5e0ea2cefc50a9992c9ae9b8be Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Sep 2024 12:21:26 +0500 Subject: [PATCH 154/197] Updated on 2026-08-14 --- lokalize.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 lokalize.py diff --git a/lokalize.py b/lokalize.py new file mode 100644 index 0000000000..3906426d80 --- /dev/null +++ b/lokalize.py @@ -0,0 +1,87 @@ +# Steps to use: +# 1) Install Python (depends on OS) +# 2) Install Lokalise API for python: +# > pip3 install python-lokalise-api --break-system-packages +# 3) Obtain token here: https://app.lokalise.com/profile#apitokens +# 4) Create file local.properties (if not exists) and define variables there: +# > lokalise.project.id=[PROJECT_ID] +# > lokalise.token=[YOUR_TOKEN] +# 5) Use script when you need to download translations: +# > python3 lokalize.py +# In case you want to filter languages, just add --langs argument with specific values +# > python3 lokalize.py --langs 'en' 'ru' + +import os +import urllib.request +import zipfile +import lokalise +import configparser +import argparse +import sys +import shutil + +config = configparser.ConfigParser() + +parser = argparse.ArgumentParser() +parser.add_argument( + "--langs", + nargs="*", + type=str, + help="--langs 'en' 'ru'", + default=[], +) + +config_file_path = "local.properties" +section_default = "default" +project_id_key = "lokalise.project.id" +token_key = "lokalise.token" + +directory_prefix = "core/res/src/main/res/values-%LANG_ISO%/" + +if not os.path.isfile(config_file_path): + print("{config_file_path} not found!") +else: + with open(config_file_path) as stream: + config.read_string(f'[{section_default}]\n' + stream.read()) + api_token = config.get(section_default, token_key, fallback=None) + project_id = config.get(section_default, project_id_key, fallback=None) + if project_id is None: + print(f"Key '{project_id_key}' not found in file {config_file_path}") + elif api_token is None: + print(f"Key '{token_key}' not found in file {config_file_path}") + else: + print(f"Found project: {project_id_key}") + print(f"Found token: {api_token}") + + folder_path = "." + file_name = "lokalize.zip" + + client = lokalise.Client(api_token) + project = client.project(project_id) + + print('Found project: ' + project.name) + + print('Read args...') + args = parser.parse_args() + + print("Generating bundle...") + response = client.download_files(project_id, { + "format": "xml", + "original_filenames": True, + "filter_langs": args.langs, + "directory_prefix": directory_prefix, + "filter_data": ["translated"] + }) + + print("Downloading translation file...") + bundle_url = response['bundle_url'] + urllib.request.urlretrieve(bundle_url, file_name) + + print("Unzipping archive...") + with zipfile.ZipFile(file_name, 'r') as zip_ref: + zip_ref.extractall(folder_path) + + print("Removing archive...") + os.remove(file_name) + + print("All done!") From 42d67bedb091f992d2bf14f9507c4559694ec2a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 16:57:58 +0300 Subject: [PATCH 155/197] Updated on 2026-08-14 --- .../block/information/InformationBlock.kt | 46 +------ .../core/ui/components/rows/NetworkTitle.kt | 103 +++++++++++++++ .../common/component/NetworkGroupItem.kt | 124 ------------------ .../common/component/NetworkTitleItem.kt | 103 +++++++++++++++ .../organizetokens/OrganizeTokensScreen.kt | 10 +- .../multicurrency/MultiCurrencyContentItem.kt | 8 +- 6 files changed, 219 insertions(+), 175 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt 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 25bac7feca..3c5b7a606d 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 @@ -16,6 +16,7 @@ import com.tangem.core.ui.R 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.components.rows.NetworkTitle import com.tangem.core.ui.components.text.TooltipText import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -27,7 +28,6 @@ import kotlinx.collections.immutable.persistentListOf @Immutable class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope -// TODO: [REDACTED_JIRA] @Composable fun InformationBlock( title: @Composable BoxScope.() -> Unit, @@ -43,49 +43,7 @@ fun InformationBlock( .background(color = TangemTheme.colors.background.action), horizontalAlignment = Alignment.Start, ) { - val minHeight = if (action == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40 - val padding = if (action == null) { - PaddingValues( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing4, - ) - } else { - PaddingValues( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing11, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing5, - ) - } - - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = minHeight) - .padding(paddingValues = padding), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .weight(weight = 1f) - .heightIn(min = TangemTheme.dimens.size20), - contentAlignment = Alignment.CenterStart, - content = title, - ) - if (action != null) { - Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing8)) - Box( - modifier = Modifier - .weight(weight = 1f) - .heightIn(min = TangemTheme.dimens.size24), - contentAlignment = Alignment.CenterEnd, - content = action, - ) - } - } + NetworkTitle(title = title, action = action) if (content != null) { Box( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt new file mode 100644 index 0000000000..383854fd65 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/NetworkTitle.kt @@ -0,0 +1,103 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +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.res.painterResource +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.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Network title component. [title] and [action] composables are placed horizontally + * + * @param title title composable in [BoxScope] + * @param modifier modifier + * @param action optional action composable in [BoxScope] + * + * @see Figma component + * +[REDACTED_AUTHOR] + */ +@Composable +fun NetworkTitle( + title: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, + action: (@Composable BoxScope.() -> Unit)? = null, +) { + val minHeight = if (action == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40 + + val padding = if (action == null) { + PaddingValues( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing4, + ) + } else { + PaddingValues( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing11, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing5, + ) + } + + Row( + modifier = modifier + .fillMaxWidth() + .heightIn(min = minHeight) + .padding(paddingValues = padding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Box( + modifier = Modifier + .weight(weight = 1f) + .heightIn(min = TangemTheme.dimens.size20), + contentAlignment = Alignment.CenterStart, + content = title, + ) + + if (action != null) { + Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing8)) + Box( + modifier = Modifier + .weight(weight = 1f) + .heightIn(min = TangemTheme.dimens.size24), + contentAlignment = Alignment.CenterEnd, + content = action, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NetworkTitlePreview(@PreviewParameter(NetworkTitleIconVisibilityProvider::class) isIconVisible: Boolean) { + TangemThemePreview { + NetworkTitle( + title = { Text(text = "Network") }, + action = { + if (isIconVisible) { + Icon( + painter = painterResource(id = R.drawable.ic_group_drop_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + }, + ) + } +} + +private object NetworkTitleIconVisibilityProvider : CollectionPreviewParameterProvider(listOf(true, false)) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt deleted file mode 100644 index aca25673e4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.tangem.feature.wallet.presentation.common.component - -import android.content.res.Configuration -import androidx.compose.foundation.background -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.res.painterResource -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.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.impl.R -import org.burnoutcrew.reorderable.ReorderableLazyListState -import org.burnoutcrew.reorderable.detectReorder - -@Composable -internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier) { - InternalNetworkGroupItem(modifier = modifier, networkName = networkName) -} - -@Composable -internal fun DraggableNetworkGroupItem( - networkName: String, - modifier: Modifier = Modifier, - reorderableTokenListState: ReorderableLazyListState? = null, -) { - InternalNetworkGroupItem( - modifier = modifier, - networkName = networkName, - endIcon = { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size32) - .let { - if (reorderableTokenListState != null) { - it.detectReorder(reorderableTokenListState) - } else { - it - } - }, - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_group_drop_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - }, - ) -} - -// TODO: [REDACTED_JIRA] -@Composable -private fun InternalNetworkGroupItem( - networkName: String, - modifier: Modifier = Modifier, - endIcon: @Composable (RowScope.() -> Unit)? = null, -) { - val minHeight = if (endIcon == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40 - val padding = if (endIcon == null) { - PaddingValues( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing4, - ) - } else { - PaddingValues( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing11, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing5, - ) - } - - Row( - modifier = modifier - .background(TangemTheme.colors.background.primary) - .fillMaxWidth() - .heightIn(min = minHeight) - .padding(paddingValues = padding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(id = R.string.wallet_network_group_title, networkName), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - - if (endIcon != null) endIcon() - } -} - -// region Preview -@Composable -private fun NetworkGroupItemSample(isDraggable: Boolean) { - if (isDraggable) { - DraggableNetworkGroupItem(networkName = "Ethereum") - } else { - NetworkGroupItem(networkName = "Ethereum") - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun NetworkGroupItemPreview(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) { - TangemThemePreview { - NetworkGroupItemSample(isDraggable) - } -} - -private class NetworkGroupProvider : CollectionPreviewParameterProvider( - collection = listOf(true, false), -) -// endregion Preview \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt new file mode 100644 index 0000000000..5bd9e5e129 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt @@ -0,0 +1,103 @@ +package com.tangem.feature.wallet.presentation.common.component + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.size +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.graphics.vector.rememberVectorPainter +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.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.rows.NetworkTitle +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.wallet.impl.R +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.detectReorder +import org.burnoutcrew.reorderable.rememberReorderableLazyListState + +@Composable +internal fun NetworkTitleItem(networkName: String, modifier: Modifier = Modifier) { + BaseNetworkTitleItem(networkName = networkName, modifier = modifier) +} + +@Composable +internal fun DraggableNetworkTitleItem( + networkName: String, + reorderableTokenListState: ReorderableLazyListState, + modifier: Modifier = Modifier, +) { + BaseNetworkTitleItem( + networkName = networkName, + modifier = modifier, + action = { DraggableIcon(reorderableTokenListState = reorderableTokenListState) }, + ) +} + +@Composable +private fun BaseNetworkTitleItem( + networkName: String, + modifier: Modifier = Modifier, + action: (@Composable BoxScope.() -> Unit)? = null, +) { + NetworkTitle( + title = { + Text( + text = stringResource(id = R.string.wallet_network_group_title, networkName), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + modifier = modifier, + action = action, + ) +} + +@Composable +private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size32) + .detectReorder(reorderableTokenListState), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_group_drop_24), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NetworkTitleItemPreview(@PreviewParameter(NetworkTitleItemProvider::class) isDraggable: Boolean) { + TangemThemePreview { + if (isDraggable) { + DraggableNetworkTitleItem( + networkName = "Ethereum", + reorderableTokenListState = rememberReorderableLazyListState(onMove = { _, _ -> }), + modifier = Modifier.background(color = TangemTheme.colors.background.primary), + ) + } else { + NetworkTitleItem( + networkName = "Ethereum", + modifier = Modifier.background(color = TangemTheme.colors.background.primary), + ) + } + } +} + +private object NetworkTitleItemProvider : CollectionPreviewParameterProvider(collection = listOf(true, false)) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 0654725a22..b7df4b8f3e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -39,7 +39,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem +import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkTitleItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState @@ -173,14 +173,16 @@ private fun LazyItemScope.DraggableItem( ) { isItemDragging -> isDragging = isItemDragging + val modifierWithBackground = itemModifier.background(color = TangemTheme.colors.background.primary) + when (item) { - is DraggableItem.GroupHeader -> DraggableNetworkGroupItem( - modifier = itemModifier, + is DraggableItem.GroupHeader -> DraggableNetworkTitleItem( + modifier = modifierWithBackground, networkName = item.networkName, reorderableTokenListState = reorderableState, ) is DraggableItem.Token -> TokenItem( - modifier = itemModifier.background(color = TangemTheme.colors.background.primary), + modifier = modifierWithBackground, state = item.tokenItemState, reorderableTokenListState = reorderableState, isBalanceHidden = isBalanceHidden, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index 755447b863..6c3ca94c12 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem +import com.tangem.feature.wallet.presentation.common.component.NetworkTitleItem import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState /** @@ -23,15 +23,17 @@ internal fun MultiCurrencyContentItem( isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { + val modifierWithBackground = modifier.background(color = TangemTheme.colors.background.primary) + when (state) { is TokensListItemState.NetworkGroupTitle -> { - NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) + NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifierWithBackground) } is TokensListItemState.Token -> { TokenItem( state = state.state, isBalanceHidden = isBalanceHidden, - modifier = modifier.background(color = TangemTheme.colors.background.primary), + modifier = modifierWithBackground, ) } } From ac4aa9c88c4172a9660be2537ad47bcd8a10fa47 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 13:46:27 +0500 Subject: [PATCH 156/197] Updated on 2026-08-14 --- .../com/tangem/features/details/ui/UserWalletListBlock.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 b09124ca04..98383d104d 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 @@ -54,7 +54,11 @@ private fun AddWalletButton( Row( modifier = Modifier .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing12), + .padding( + start = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { From f076ab08e77492a1114ea80068f7206e86b73b50 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 16:25:43 +0500 Subject: [PATCH 157/197] Updated on 2026-08-14 --- .../preview/PreviewWalletSettingsComponent.kt | 9 ++------- .../walletsettings/model/WalletSettingsModel.kt | 3 +++ .../feature/walletsettings/utils/ItemsBuilder.kt | 14 ++++++++++---- 3 files changed, 15 insertions(+), 11 deletions(-) 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 4c4d0e3f25..c0f134ef22 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,18 +8,12 @@ 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 { private val previewState = WalletSettingsUM( popBack = {}, - items = ItemsBuilder( - router = DummyRouter(), - manageTokensToggles = object : ManageTokensToggles { - override val isFeatureEnabled: Boolean = true - }, - ).buildItems( + items = ItemsBuilder(router = DummyRouter()).buildItems( userWalletId = UserWalletId("011"), userWalletName = "My Wallet", isReferralAvailable = true, @@ -27,6 +21,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { renameWallet = {}, forgetWallet = {}, onLinkMoreCardsClick = {}, + isManageTokensAvailable = true, ), ) 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 fcb721457e..ba19734477 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 @@ -32,6 +32,7 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.features.managetokens.ManageTokensToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -53,6 +54,7 @@ internal class WalletSettingsModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsContextProxy: AnalyticsContextProxy, private val reduxStateHolder: ReduxStateHolder, + private val manageTokensToggles: ManageTokensToggles, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -86,6 +88,7 @@ internal class WalletSettingsModel @Inject constructor( userWalletName = userWallet.name, isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(), isLinkMoreCardsAvailable = userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, + isManageTokensAvailable = userWallet.isMultiCurrency && manageTokensToggles.isFeatureEnabled, renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, forgetWallet = { messageSender.send( 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 4a7d294e77..896d1bbf0c 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,7 +9,6 @@ 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 @@ -18,7 +17,6 @@ import javax.inject.Inject @ComponentScoped internal class ItemsBuilder @Inject constructor( private val router: Router, - private val manageTokensToggles: ManageTokensToggles, ) { @Suppress("LongParameterList") @@ -27,12 +25,19 @@ internal class ItemsBuilder @Inject constructor( userWalletName: String, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, + isManageTokensAvailable: Boolean, forgetWallet: () -> Unit, renameWallet: () -> Unit, onLinkMoreCardsClick: () -> Unit, ): PersistentList = persistentListOf( buildNameItem(userWalletName, renameWallet), - buildCardItem(userWalletId, isLinkMoreCardsAvailable, isReferralAvailable, onLinkMoreCardsClick), + buildCardItem( + userWalletId = userWalletId, + isLinkMoreCardsAvailable = isLinkMoreCardsAvailable, + isReferralAvailable = isReferralAvailable, + isManageTokensAvailable = isManageTokensAvailable, + onLinkMoreCardsClick = onLinkMoreCardsClick, + ), buildForgetItem(forgetWallet), ) @@ -47,12 +52,13 @@ internal class ItemsBuilder @Inject constructor( userWalletId: UserWalletId, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, + isManageTokensAvailable: Boolean, onLinkMoreCardsClick: () -> Unit, ) = WalletSettingsItemUM.WithItems( id = "card", description = resourceReference(R.string.settings_card_settings_footer), blocks = buildList { - if (manageTokensToggles.isFeatureEnabled) { + if (isManageTokensAvailable) { BlockUM( text = resourceReference(R.string.add_tokens_title), iconRes = R.drawable.ic_tether_24, From 2d3a1d3bc7fbae48fd1ff38a5a3db1c47f34b5c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 17:36:36 +0500 Subject: [PATCH 158/197] Updated on 2026-08-14 --- .../PreviewCustomTokenSelectorComponent.kt | 1 + .../preview/PreviewManageTokensComponent.kt | 1 + .../entity/item/CurrencyNetworkUM.kt | 3 +++ .../managetokens/ui/ManageTokensScreen.kt | 13 ++++++++++++- .../utils/list/ManageTokensListManager.kt | 4 ++++ .../utils/list/ManageTokensUiManager.kt | 17 +++++++++++++++++ .../utils/mapper/CurrencyNetworksMapper.kt | 5 +++++ .../utils/ui/CurrencyItemOperations.kt | 2 ++ 8 files changed, 45 insertions(+), 1 deletion(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 0a37116d7d..24b389d850 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -68,6 +68,7 @@ internal class PreviewCustomTokenSelectorComponent( isMainNetwork = false, isSelected = n.id == params.selectedNetwork?.id, onSelectedStateChange = { params.onNetworkSelected(n) }, + onLongClick = {}, ) } } 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 ff77045832..fae9aafb14 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 @@ -158,6 +158,7 @@ internal class PreviewManageTokensComponent( isMainNetwork = networkIndex == 0, isSelected = false, onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, + onLongClick = {}, ) }.toImmutableList() diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt index b666a09c2a..bdec914faa 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt @@ -8,9 +8,12 @@ internal data class CurrencyNetworkUM( val type: String, val iconResId: Int, val isMainNetwork: Boolean, + val onLongClick: () -> Unit, override val isSelected: Boolean, override val onSelectedStateChange: (Boolean) -> Unit, ) : SelectableItemUM { override val id: String = network.id.value + + data class LongTapConfig(val contractAddress: String, val onLongTap: () -> Unit) } \ No newline at end of file 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 3b4b3bb478..6c5489f29b 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,8 +3,11 @@ 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.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -364,6 +367,7 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, isEditable: Boolean, m } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun NetworksList( networks: NetworksUM, @@ -392,7 +396,14 @@ private fun NetworksList( isLastItem = index == currentItems.lastIndex, content = { BlockchainRow( - modifier = Modifier.padding(end = TangemTheme.dimens.spacing8), + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing8) + .combinedClickable( + onLongClick = network.onLongClick, + onClick = {}, + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ), model = with(network) { BlockchainRowUM( id = id, 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 835316aa30..da24501ab1 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 @@ -3,6 +3,7 @@ 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.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage @@ -32,6 +33,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ComponentScoped internal class ManageTokensListManager @Inject constructor( private val getManagedTokensUseCase: GetManagedTokensUseCase, @@ -40,6 +42,7 @@ internal class ManageTokensListManager @Inject constructor( private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, + clipboardManager: ClipboardManager, ) : ManageTokensUiActions { private lateinit var scope: CoroutineScope @@ -59,6 +62,7 @@ internal class ManageTokensListManager @Inject constructor( dispatchers = dispatchers, actions = this, scopeProvider = Provider { scope }, + clipboardManager = clipboardManager, ) val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd.asStateFlow() 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 b43eb3542c..167a7f88a7 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 @@ -1,9 +1,12 @@ package com.tangem.features.managetokens.utils.list import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.ContentMessage +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network @@ -32,6 +35,7 @@ internal class ManageTokensUiManager( private val dispatchers: CoroutineDispatcherProvider, private val scopeProvider: Provider, private val actions: ManageTokensUiActions, + private val clipboardManager: ClipboardManager, ) { private val scope: CoroutineScope @@ -130,6 +134,7 @@ internal class ManageTokensUiManager( onSelectCurrencyNetwork = { networkId, isSelected -> selectNetwork(currencyBatch.key, currency, networkId, isSelected) }, + onLongTap = ::copyContractAddress, ) batches.updateUiBatchesItem( @@ -139,6 +144,18 @@ internal class ManageTokensUiManager( } } + private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) { + if (source is ManagedCryptoCurrency.SourceNetwork.Default) { + clipboardManager.setText(text = source.contractAddress) + showSnackbarMessage(resourceReference(R.string.contract_address_copied_message)) + } + } + + private fun showSnackbarMessage(messageText: TextReference) { + val message = SnackbarMessage(message = messageText) + messageSender.send(message) + } + private fun selectNetwork( batchKey: Int, currency: ManagedCryptoCurrency, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt index 2e88d82956..7bb461b84b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -12,6 +12,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( isExpanded: Boolean, isItemsEditable: Boolean, onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, + onLongTap: (SourceNetwork) -> Unit, ): NetworksUM { return if (isExpanded) { NetworksUM.Expanded( @@ -20,6 +21,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, + onLongTap = onLongTap, ) }.toImmutableList(), ) @@ -38,6 +40,7 @@ internal fun Network.toCurrencyNetworkModel( iconResId = id.getIconRes(isColored = true), isSelected = isSelected, type = standardType.name, + onLongClick = {}, isMainNetwork = false, onSelectedStateChange = onSelectedStateChange, ) @@ -47,6 +50,7 @@ private fun SourceNetwork.toCurrencyNetworkModel( isSelected: Boolean, isEditable: Boolean, onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, + onLongTap: (SourceNetwork) -> Unit, ): CurrencyNetworkUM { return CurrencyNetworkUM( network = network, @@ -54,6 +58,7 @@ private fun SourceNetwork.toCurrencyNetworkModel( iconResId = id.getIconRes(isColored = isSelected || !isEditable), isSelected = isSelected || !isEditable, type = typeName, + onLongClick = { onLongTap(this) }, isMainNetwork = this is SourceNetwork.Main, onSelectedStateChange = { selected -> onSelectedStateChange(this, selected) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt index a539a0524d..dc27478f94 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt @@ -11,6 +11,7 @@ internal fun CurrencyItemUM.toggleExpanded( currency: ManagedCryptoCurrency, isEditable: Boolean, onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit, + onLongTap: (SourceNetwork) -> Unit, ): CurrencyItemUM { if (currency !is ManagedCryptoCurrency.Token) return this @@ -29,6 +30,7 @@ internal fun CurrencyItemUM.toggleExpanded( isExpanded = isExpanded, isItemsEditable = isEditable, onSelectedStateChange = onSelectCurrencyNetwork, + onLongTap = onLongTap, ), ) } From 29b63f2e1a5e70a84275b408a7b799850cfd2b02 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Sep 2024 13:52:31 +0300 Subject: [PATCH 159/197] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 5 +++ .../tangem/tap/data/DefaultCardSdkProvider.kt | 3 ++ .../tangem/tap/features/home/HomeViewModel.kt | 4 +- .../home/TangemTangemErrorsHandler.kt | 44 +++++++++++++++++++ .../home/errors/TangemSdkErrorHandler.kt | 8 ++++ .../com/tangem/domain/redux/StateDialog.kt | 2 + gradle/dependencies.toml | 2 +- 7 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt create mode 100644 app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index a13d000c44..8bc1f8e7ae 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -51,6 +51,11 @@ class DialogManager : StoreSubscriber { source = state.dialog.source, onTryAgain = state.dialog.onTryAgain, ) + is StateDialog.NfcFeatureIsUnavailable -> SimpleAlertDialog.create( + titleRes = R.string.common_error, + messageRes = R.string.nfc_error_unavailable, + context = context, + ) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog( diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index 395f26e868..a1751aff85 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -16,6 +16,7 @@ import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.sdk.DefaultSessionViewDelegate import com.tangem.sdk.extensions.* +import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider import com.tangem.sdk.nfc.NfcManager import com.tangem.sdk.storage.create import com.tangem.tap.foregroundActivityObserver @@ -98,9 +99,11 @@ internal class DefaultCardSdkProvider @Inject constructor( val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity) viewDelegate.sdkConfig = config + val androidNfcAvailabilityProvider = AndroidNfcAvailabilityProvider(activity) val sdk = TangemSdk( reader = nfcManager.reader, viewDelegate = viewDelegate, + nfcAvailabilityProvider = androidNfcAvailabilityProvider, secureStorage = secureStorage, authenticationManager = authenticationManager, keystoreManager = keystoreManager, 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 ce18962585..dd335deba7 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 @@ -47,6 +47,8 @@ internal class HomeViewModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel() { + private val tangemErrorHandler = TangemTangemErrorsHandler(store) + fun onScanClick() { analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) scanCard() @@ -83,7 +85,7 @@ internal class HomeViewModel @Inject constructor( } }, onFailure = { - Timber.e(it, "Unable to scan card") + tangemErrorHandler.onErrorReceived(error = it) delay(HIDE_PROGRESS_DELAY) store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) }, diff --git a/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt b/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt new file mode 100644 index 0000000000..40e51a8de8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt @@ -0,0 +1,44 @@ +package com.tangem.tap.features.home + +import com.tangem.blockchain.common.BlockchainError +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.domain.redux.StateDialog +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.home.errors.TangemSdkErrorHandler +import org.rekotlin.Store +import timber.log.Timber + +class TangemTangemErrorsHandler(val store: Store) : TangemSdkErrorHandler { + + override fun onErrorReceived(error: TangemError) { + when (error) { + is TangemSdkError -> { + handleCardSdkError(error) + } + is BlockchainError -> { + handleBlockchainSdkError(error) + } + else -> { + Timber.e("Error happened", error) + } + } + } + + private fun handleCardSdkError(error: TangemSdkError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> { + store.dispatchOnMain(GlobalAction.ShowDialog(StateDialog.NfcFeatureIsUnavailable)) + } + else -> { + Timber.e(error, "Unable to scan card") + } + } + } + + private fun handleBlockchainSdkError(error: TangemError) { + Timber.e("Sdk error happened", error) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt b/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt new file mode 100644 index 0000000000..7946b7ec5e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.home.errors + +import com.tangem.common.core.TangemError + +interface TangemSdkErrorHandler { + + fun onErrorReceived(error: TangemError) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt index e4beb677bd..120604ae39 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt @@ -2,6 +2,8 @@ package com.tangem.domain.redux interface StateDialog { + data object NfcFeatureIsUnavailable : StateDialog + data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog enum class ScanFailsSource { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f6d9a1fde9..7872a1cb27 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -91,7 +91,7 @@ markdownComposeView = "0.5.4" # region Tangem tangemBlockchainSdk = "develop-762" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-380" +tangemCardSdk = "develop-381" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem16" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 15a2e1432c991a554bdfa7b649a7442557e23efb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 19:35:46 +0500 Subject: [PATCH 160/197] Updated on 2026-08-14 --- .../portfolio/impl/ui/PortfolioItem.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 3b3ed01682..3ed29f0521 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 @@ -12,6 +12,8 @@ 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.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.preview.PreviewMyPortfolioUMProvider @@ -22,8 +24,23 @@ import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState @Composable internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { Column(modifier) { + val hapticManager = LocalHapticManager.current + val tokenItemState = remember(state.tokenItemState) { + when (state.tokenItemState) { + is TokenItemState.Content -> state.tokenItemState.copy( + onItemClick = { + val onClick = state.tokenItemState.onItemClick + if (onClick != null) { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + onClick.invoke() + } + }, + ) + else -> state.tokenItemState + } + } TokenItem( - state = state.tokenItemState, + state = tokenItemState, isBalanceHidden = state.isBalanceHidden, modifier = Modifier.background(color = TangemTheme.colors.background.action), itemPaddingValues = PaddingValues( From d0895b8a5e32d5400399996ad5796b9c62db8d1e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 16:15:37 +0200 Subject: [PATCH 161/197] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 15 +++++-- core/res/src/main/res/values-es/strings.xml | 7 ++-- core/res/src/main/res/values-fr/strings.xml | 7 ++-- core/res/src/main/res/values-ja/strings.xml | 9 +++-- core/res/src/main/res/values-ru/strings.xml | 11 +++-- .../src/main/res/values-uk-rUA/strings.xml | 40 +++++++++++++++++-- core/res/src/main/res/values/strings.xml | 10 ++++- 7 files changed, 75 insertions(+), 24 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 73dbc1635e..eee487a5c1 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -400,7 +400,7 @@ Leer Niedrig Marktkapitalisierung - Der Gesamtmarktwert einer Kryptowährung, berechnet durch Multiplikation des aktuellen Preises der Münze mit der Gesamtzahl der im Umlauf befindlichen Münzen + Das MCap eines Assets, errechnet sich durch Multiplikation des aktuellen Preises mit der Gesamtzahl der im Umlauf befindlichen Coins Marktbewertung Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung Maximale Versorgung @@ -636,6 +636,10 @@ Versende %s Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s Du sendest **%1$s** und %2$s + Du sendest **%1$s** + Die Netzwerkgebühr wird durch die Nutzung von %1$s %2$s gedeckt. + Die Netzwerkgebühr wird durch das Ausgeben von %1$s %2$s reduziert. + inklusive einer Netzgebühr von %1$s Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Transaktion gesendet @@ -677,9 +681,11 @@ Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen + Beim Staking im %1$s -Netzwerk mit einem neuen Validator werden alle zuvor eingesetzten Kryptos automatisch an diesen Validator übertragen Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. - Die Belohnungen werden sofort nach dem start des unstaken gestoppt. Der unstakingprozess dauert %s. - Dein Guthaben wurde freigegeben. Du kannst es jetzt zusammen mit deinen verdienten Prämien auszahlen. + Nach Ablauf der 21-tägigen Bindungsfrist kannst Du über Dein Guthaben verfügen. Die Prämie wird zusammen mit dem ungestaketen Guthaben abgehoben. + Deine Assets steht Dir nach Ablauf der Frist für die Aufhebung der Bindung %s zur Verfügung. + Du kannst Dein Guthaben jetzt abheben. Es steht Dir sofort zur Verfügung. Vorbereitung Bereit zum Abheben Erneut binden @@ -700,11 +706,12 @@ Belohnungen Stake gesperrt Mehr staken - Du setzt %1$s ein und erhältst jährlich ~ %2$s + Du setzt %1$s ein und erhältst jährlich %2$s Zum Entsperren antippen Zum abheben antippen Stake %s Staking beenden%s + Die Transaktion wird bearbeitet! Derzeit findet eine Validierung in der Blockchain statt. Dies kann einige Minuten dauern. Lösen der Bindungen Gelocktes unlocken Unstaken diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 667b0cf956..b44332a6c6 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -396,7 +396,7 @@ Índice de liquidez Bajo Capitalización de mercado - El valor total de mercado de una criptomoneda, calculado multiplicando el precio actual de la moneda por el número total de monedas en circulación. + El valor total de una criptomoneda calculado multiplicando su precio por la cantidad de monedas en circulación Evaluación de mercado Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado Suministro máximo @@ -663,13 +663,14 @@ Bloqueado Migrar Native staking + Recomp. won se le enviará y estará disponible para su uso inmediato El staking le permite ganar %1$s. Sus recompensas por apostar llegan todos los días. El staking le permite ganar %1$s. Sus recompensas del staking llegan cada hora. El staking le permite ganar %1$s. Sus recompensas del staking llegan todos los meses. El staking le permite ganar %1$s. Sus recompensas por apostar llegan todas las semanas. Gane recompensas por staking - Las recompensas se acumulan inmediatamente después de comenzar el unstaking. El proceso de unstaking dura %s. - El unstaking de sus fondos está hecho. Ahora puede retirarlos con las recompensas obtenidas + Sus fondos estarán disponibles para su uso después del período de desvinculación de %s. + Ya puede retirar sus fondos, estarán disponibles para usar de inmediato. Listo para retirar Reunir Haga el staking de nuevo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 89862022aa..ca8c7cf4f2 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -396,7 +396,7 @@ Indice de liquidité Faible Capitalisation boursière - La valeur marchande totale d\'une crypto-monnaie, calculée en multipliant le prix actuel de la pièce par le nombre total de pièces en circulation + La valeur totale d\'une crypto-monnaie calculée en multipliant son prix actuel par le nbre de pièces en circulation Évaluation du marché Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière Approvisionnement maximal @@ -663,13 +663,14 @@ Bloqué Migrer Native staking + Récomp. gagnées vous seront envoyées et dispo pour utilisation immédiate Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent tous les jours. Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent toutes les heures. Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent tous les mois. Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent toutes les semaines. Gagnez des récompenses de staking - Les récompenses cessent de s\'accumuler immédiatement après que vous ayez commencé le unstaking. Le processus de unstaking prend %s. - Vos fonds ont été destakés. Vous pouvez désormais les retirer avec vos récompenses gagnées. + Vos fonds seront disponibles pour utilisation après la période de désengagement %s. + Vous pouvez désormais retirer vos fonds, ils seront disponibles à l\'utilisation immédiatement Prêt à retirer Réassocier Restakez diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 7cb6050f2b..3d5a19d86e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -659,15 +659,15 @@ ロック中 移行 ネイティブステーキング - 獲得した報酬はあなたのアドレスに直接送られます。受け取り後、資金は使用可能になります。 + 獲得した報酬はあなたのアドレスに直接送られ、すぐ使用可能です。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎時間受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 - ステーキング解除プロセスの開始後、報酬の獲得はすぐに停止します。ステーキング解除には%sかかります。 - あなたの資金はステーキング解除されました。獲得した報酬とともに引き出すことができます。 + %sのステーキング解除期間後、資金はすぐ利用可能となります。 + 資金を引き出すことができ、すぐに使用できます 準備中 引き出し準備完了 再結束 @@ -688,11 +688,12 @@ 報酬 ステーキングはロックされています もっとステーキングする - %1$sをステーキングし、年間 ~ %2$sを受け取ります + %1$sをステーキングし、年間 %2$sを受け取ります タップしてロック解除 タップして引き出す %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 3cd80972f4..e3715ed204 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -351,6 +351,7 @@ Рынок Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту Tangem кошелька Данные раздела получены из следующих сетей: %s + Невозможно загрузить данные Нет данных Быстрые действия Результат @@ -380,11 +381,12 @@ На основе %d оценок Веб-сайт - Покуп. предпочтение - Разница между объемом покупателей и продавцов + Покуп. предпоч. + Разница между объемом покупателей и продавцов Циркулир. предл. Общее количество монет, доступных для торговли и находящихся в обращении на рынке - Опытные покупкатели + Опытные трейдеры + Покупатели, у которых было как минимум 100 исходящих транзакций. Полн. разб. кап. Общая теоретическая стоимость криптовалюты, если все монеты, которые могут существовать, находятся в обращении, включая те, которые в настоящее время не обращаются Дата создания @@ -400,6 +402,7 @@ Рын. кап. Общая рыночная стоимость криптовалюты, рассчитываемая путем умножения текущей цены монеты на общее количество монет в обращении. Рейтинг + Позиция в рейтинге криптовалют среди всех монет на основе рыночной капитализации. Максимальный объем Метрики Официальные ссылки @@ -672,7 +675,7 @@ Заблокировано Переместить Нативный стейкинг - Полученная награда будет отправлена на ваш кошелек и будет сразу доступна для использования + Полученная награда отправится на ваш кошелек и сразу станет доступна для использования Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый день. Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый час. Стейкинг дает возможность вам получать %1$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 d15a97ab14..f0517e2467 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -39,7 +39,7 @@ Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. Почніть процес резервного копіювання - З вашої фіатної картки або банківського рахунку + За допомогою банківської картки або банківського рахунку %d картка %d картки @@ -72,6 +72,7 @@ Доступ заборонено Усе Дозволити + Аналітика Застосовувати Затвердження Підтвердити @@ -84,6 +85,8 @@ Перейдіть до %1$s Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності Скасувати + Виберіть дію + Отримати Отримати винагороди Закрити Продовжити @@ -104,6 +107,7 @@ Увімкнути Увімкнено Помилка + Обміняти Оглядач Переглянути історію транзакцій Оглядач @@ -118,6 +122,7 @@ Імпортувати процесі Пізніше + Залишилося %1$s Заблокований Основна мережа Комісія мережі @@ -174,6 +179,8 @@ Адреса контракту Адреса контракту недійсна Будь ласка, оберіть мережу + Цей токен вже додано до списку + Токен вже існує Десяткове число повинно бути дійсним цілим числом, до %d Власна деривація Наприклад, m/00\'/0000\'/0\'/0/0 @@ -344,12 +351,14 @@ Щоб почати купувати, обмінювати або отримувати цей актив, додайте цей токен принаймні в 1 мережу Цей актив недоступний Додати в портфоліо - Додати токен + Додати Доступні мережі Моє портфоліо Маркет Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem + Дані цього розділу отримані з наступних мереж: %s Не вдалося завантажити дані... + Немає даних Швидкі дії Результат Переглянути токени до 100к USD ринкової капіталізації @@ -396,7 +405,7 @@ Зміна того, скільки ліквідності доступно для токена протягом зазначеного періоду часу Індекс ліквідності Низький - Ринкова капіталізація + Рин. кап. Загальна ринкова вартість криптовалюти, що розраховується шляхом множення поточної ціни монети на загальну кількість монет в обігу Рейтинг Позиція в крипторейтингу між усіма монетами на основі ринкової капіталізації @@ -411,6 +420,9 @@ Максимальна кількість монет або токенів, яка може коли-небудь існувати для певної криптовалюти Обсяг торгів (24г) Загальна сума криптовалюти, якою торгували протягом останніх 24 годин, що вказує на рівень активності та ліквідності на ринку + Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету + Додати токени + Функція NFC недоступна на вашому пристрої Вам потрібно встановити єдиний код доступу для захисту всіх ваших карток Захист Пізніше ви зможете налаштувати індивідуальний код доступу до кожної картки @@ -648,6 +660,7 @@ Процентна ставка Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. APR + Винагороди автоматично накопичуються на вашому балансі щодня. Доступно Середня ставка винагороди Що таке стейкінг? @@ -666,14 +679,21 @@ Період, який ви повинні чекати після запиту на виведення коштів зі стейкінгу, перш ніж токени стануть доступними. Період блокування Відведений час для активації участі в стейкінгу. + Використовуючи функцію стейкінгу, ви погоджуєтесь з %1$s та %2$s + Заблоковано Перемістити Нативний стейкінг + Зароблені винагороди будуть надіслані на ваш гаманець і доступні для використання відразу Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щодня. Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються кожну годину. Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щомісяця. Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щотиждня. Отримуйте винагороду за стейкінг - Винагорода припиняє нараховуватися одразу після того, як ви почнете процес завершення стейкінгу. Процес завершення триває %s. + Реінвестуйте зароблені винагороди у суму стейкінгу, щоб збільшити потенційний прибуток. + Ваші кошти будуть доступні після %s періоду розблокування. + Тепер ви можете вивести свої кошти, вони будуть доступні для використання відразу + Підготовка + Готова до виведення Зʼєднати ще раз Повторно застейкати Застейкати винагороди @@ -683,6 +703,7 @@ Вручну Блок День + Щодня Епоха Ера Година @@ -691,15 +712,23 @@ Винагороди Стейкінг закрито Застейкати більше + Ви стейкаєте %1$s і будете отримувати %2$s щороку + Натисніть, щоб розблокувати + Натисніть, щоб вивести кошти Застейкати %s Зняти зі стейкінгу %s + Транзакція обробляється! Наразі триває перевірка в блокчейні. Це може зайняти кілька хвилин. + Розблокування Розблокувати Вивід зі стейкінгу Зняти зі стейкінгу + Виведення зі стейкінгу %s Валідатор + Валідатори Проголосувати Голос заблоковано Зняти + Ваші стейкінги Тримайте свою криптовалюту в безпеці. Приватні ключі надійно зберігаються на картці. Революційний апаратний гаманець До трьох карток з одним гаманцем @@ -827,6 +856,8 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця + Процес підтвердження наразі триває і буде завершено найближчим часом + Підтвердження в процесі Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. @@ -869,6 +900,7 @@ Низька кількість підписів Токени в різних мережах можуть мати різні адреси. Переконайтеся, що ваша адреса відповідає мережі, коли переказуєте кошти. Токен MATIC мігрує до POL. Однак кінцевого терміну не встановлено і MATIC ще не застарілий. Ви можете безпечно продовжувати використовувати токен MATIC або скористатися біржами, щоб обміняти його на POL. + Міграція MATIC у POL Використовуйте вашу картку, щоб отримати адресу для %d мережі Використовуйте вашу картку, щоб отримати адреси для %d мереж diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d4457dd0f7..fe5a90bc36 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -632,6 +632,10 @@ Send %s You are sending **%1$s** including a network fee of %2$s You are sending **%1$s** and %2$s + You are sending **%1$s** + network fee will be covered by using %1$s %2$s + network fee will be reduced by spending %1$s %2$s + including a network fee of %1$s Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. Transaction sent @@ -673,9 +677,11 @@ 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 + Staking in the %1$s network with a new validator will automatically transfer all previously staked funds to this validator Reinvests your earned rewards in your staked amount, increasing potential earnings. + Your funds will be available for use after the 21-day unbonding period. Reward will be withdrawn along with your unstaking funds. Your funds will be available for use after the %s unbonding period. - You can now withdraw your funds, it will be available to use immediatly + You can now withdraw your funds, it will be available to use immediately Preparing Ready to withdraw Rebond @@ -696,7 +702,7 @@ Rewards Stake locked Stake more - You stake %1$s and will be receiving ~%2$s yearly + You stake %1$s and will be receiving %2$s yearly Tap to unlock Tap to withdraw Stake %s From 9e74c7d2ebc3a2ffc21ea32bae176eacbb00f7a9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 17:32:45 +0200 Subject: [PATCH 162/197] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index fe5a90bc36..4bb0770e41 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -672,10 +672,14 @@ Migrate Native staking Earned rewards will be sent to your wallet and available for use immediately + Stake securely and start earning daily rewards + Stake securely and start earning hourly rewards + Stake securely and start earning monthly rewards 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. + Stake securely and start earning weekly rewards Earn staking rewards Staking in the %1$s network with a new validator will automatically transfer all previously staked funds to this validator Reinvests your earned rewards in your staked amount, increasing potential earnings. @@ -709,7 +713,7 @@ Unstake %s The transaction is being processed! Validation is currently underway in the blockchain. This may take a few minutes. Unbonding - Unlock locked + Unlock Unstaked Unstaking Unstaking assets %s From c21f0374daaa301151f029162c8a5142df04989d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Sep 2024 12:23:56 +0300 Subject: [PATCH 163/197] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../AddStakingNotificationsTransformer.kt | 31 ++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4bb0770e41..ccbd670078 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -683,6 +683,7 @@ Earn staking rewards Staking in the %1$s network with a new validator will automatically transfer all previously staked funds to this validator Reinvests your earned rewards in your staked amount, increasing potential earnings. + Unlock your money to withdraw it from staking process. Unlocking takes %s. Your funds will be available for use after the 21-day unbonding period. Reward will be withdrawn along with your unstaking funds. Your funds will be available for use after the %s unbonding period. You can now withdraw your funds, it will be available to use immediately diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 55740369f4..6f68eb4cc8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachable import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency @@ -222,24 +223,40 @@ internal class AddStakingNotificationsTransformer( else -> { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data val pendingActionType = confirmationState?.pendingAction?.type - val (titleId, textId) = when (pendingActionType) { + val (titleReference, textReference) = when (pendingActionType) { StakingActionType.CLAIM_REWARDS -> { - R.string.common_claim to R.string.staking_notification_claim_rewards_text + resourceReference(R.string.common_claim) to + resourceReference(R.string.staking_notification_claim_rewards_text) } StakingActionType.RESTAKE_REWARDS -> { - R.string.staking_restake to R.string.staking_notification_restake_rewards_text + resourceReference(R.string.staking_restake) to + resourceReference(R.string.staking_notification_restake_rewards_text) } StakingActionType.WITHDRAW -> { - R.string.staking_withdraw to R.string.staking_notification_withdraw_text + resourceReference(R.string.staking_withdraw) to + resourceReference(R.string.staking_notification_withdraw_text) + } + StakingActionType.UNLOCK_LOCKED -> { + val cooldownPeriodDays = yield.metadata.cooldownPeriod.days + resourceReference(R.string.staking_unlocked_locked) to resourceReference( + R.string.staking_notification_unlock_text, + wrappedList( + pluralReference( + id = R.plurals.common_days, + count = cooldownPeriodDays, + formatArgs = wrappedList(cooldownPeriodDays), + ), + ), + ) } else -> null to null } - if (titleId != null && textId != null) { + if (titleReference != null && textReference != null) { add( StakingNotification.Info.PendingAction( - title = resourceReference(titleId), - text = resourceReference(textId), + title = titleReference, + text = textReference, ), ) } From 16e32259c64e7fe7b5076fdb4ef5f608ffcf9d06 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 18:53:55 +0300 Subject: [PATCH 164/197] Updated on 2026-08-14 --- .../impl/model/AddToPortfolioManager.kt | 2 -- .../impl/model/MyPortfolioUMFactory.kt | 26 ++++++++++++------- .../impl/model/TokensPortfolioUMConverter.kt | 4 +-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt index bf366c445c..be5ee4d9ed 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import timber.log.Timber import javax.inject.Inject -import javax.inject.Singleton internal typealias WalletsWithNetworks = Map> @@ -17,7 +16,6 @@ internal typealias WalletsWithNetworks = Map?>(value = null) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 041c92c05f..c52637100f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -29,12 +30,13 @@ internal class MyPortfolioUMFactory( fun create(portfolioData: PortfolioData, portfolioUIData: PortfolioUIData): MyPortfolioUM { val addToPortfolioData = portfolioUIData.addToPortfolioData - if (addToPortfolioData.availableNetworks == null) return MyPortfolioUM.Loading + if (addToPortfolioData.availableNetworks?.isEmpty() == true) return MyPortfolioUM.Unavailable - if (addToPortfolioData.availableNetworks.isEmpty()) return MyPortfolioUM.Unavailable - - val walletsWithCurrencies = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) + val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) { + portfolioData.walletsWithCurrencies + } else { + portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) + } val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() if (isPortfolioEmpty) { @@ -56,7 +58,7 @@ internal class MyPortfolioUMFactory( return TokensPortfolioUMConverter( appCurrency = portfolioData.appCurrency, isBalanceHidden = portfolioData.isBalanceHidden, - isAllAvailableNetworksAdded = walletsWithCurrencies.isAllAvailableNetworksAdded( + addButtonState = walletsWithCurrencies.getAddButtonState( availableNetworks = addToPortfolioData.availableNetworks, ), bsConfig = createAddToPortfolioBSConfig(portfolioData = portfolioData, portfolioUIData = portfolioUIData), @@ -94,17 +96,21 @@ internal class MyPortfolioUMFactory( ) } - private fun Map>.isAllAvailableNetworksAdded( - availableNetworks: Set, - ): Boolean { + private fun Map>.getAddButtonState( + availableNetworks: Set?, + ): AddButtonState { + if (availableNetworks == null) return AddButtonState.Loading + val networkIds = availableNetworks.map { it.networkId } - return this + val isAllAvailableNetworksAdded = this // User can add currencies only in multi-currency wallets .filterKeys(UserWallet::isMultiCurrency) .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } // Each wallets contains all available networks? .all { it.value.containsAll(networkIds) } + + return if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available } /** Filter map values by available networks [networks] */ diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index 4dc478786c..7e847834d2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -23,7 +23,7 @@ import kotlinx.collections.immutable.toImmutableList internal class TokensPortfolioUMConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, - private val isAllAvailableNetworksAdded: Boolean, + private val addButtonState: AddButtonState, private val bsConfig: TangemBottomSheetConfig, private val onAddClick: () -> Unit, private val quickActionsIntents: TokenActionsHandler, @@ -47,7 +47,7 @@ internal class TokensPortfolioUMConverter( } .setQuickActionsVisibility(currentState = currentTokensState) .toImmutableList(), - buttonState = if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available, + buttonState = addButtonState, addToPortfolioBSConfig = bsConfig, onAddClick = onAddClick, tokenReceiveBSConfig = (currentState() as? MyPortfolioUM.Tokens) From 27ae1ee2a0faae29370cfe974d7f5110bb9c4370 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Sep 2024 10:47:20 +0300 Subject: [PATCH 165/197] Updated on 2026-08-14 --- .../warningMessage/WarningMessage.kt | 62 ------------------- .../utils/ManagedCryptoCurrencyFactory.kt | 7 ++- .../markets/DefaultMarketsTokenRepository.kt | 4 +- .../repository/DefaultCurrenciesRepository.kt | 5 +- .../converters/LeastTokenInfoConverter.kt | 4 +- .../blockchainsdk/compatibility/L2Networks.kt | 59 ++++++++++++++++++ 6 files changed, 72 insertions(+), 69 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt create mode 100644 libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt deleted file mode 100644 index 2f8f892e51..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.tap.domain.configurable.warningMessage - -import androidx.annotation.StringRes -import com.squareup.moshi.Json -import com.tangem.blockchain.common.Blockchain - -/** -[REDACTED_AUTHOR] - */ -data class WarningMessage( - val title: String, - val message: String, - val type: Type, - val priority: Priority, - val location: List, - private val blockchains: List?, - @StringRes val titleResId: Int? = null, - @StringRes val messageResId: Int? = null, - val origin: Origin = Origin.Remote, - @StringRes val buttonTextId: Int? = null, - val titleFormatArg: String? = null, - val messageFormatArg: String? = null, -) { - val blockchainList: List? by lazy { - blockchains?.map { Blockchain.fromId(it.uppercase()) } - } - - var isHidden = false - - enum class Priority { - @Json(name = "critical") - Critical, - - @Json(name = "warning") - Warning, - - @Json(name = "info") - Info, - } - - enum class Type { - @Json(name = "permanent") - Permanent, // нельзя скрыть - - @Json(name = "temporary") - Temporary, // можно скрыть (кнопка ОК) - - AppRating, - - TestCard, - } - - enum class Location { - - @Json(name = "send") - SendScreen, - } - - enum class Origin { - Remote, - } -} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 5089f51294..1832d13c52 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -1,6 +1,8 @@ package com.tangem.data.managetokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.applyL2Compatibility +import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toCoinId @@ -90,12 +92,13 @@ internal class ManagedCryptoCurrencyFactory { ): ManagedCryptoCurrency? { if (coinResponse.networks.isEmpty() || !coinResponse.active) return null + val updatedNetworks = coinResponse.networks.applyL2Compatibility(coinResponse.id) return ManagedCryptoCurrency.Token( id = ManagedCryptoCurrency.ID(coinResponse.id), name = coinResponse.name, symbol = coinResponse.symbol, iconUrl = getIconUrl(coinResponse.id, imageHost), - availableNetworks = coinResponse.networks.mapNotNull { network -> + availableNetworks = updatedNetworks.mapNotNull { network -> createSource(network, derivationStyleProvider) }, addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, derivationStyleProvider), @@ -136,7 +139,7 @@ internal class ManagedCryptoCurrencyFactory { if (tokensResponse == null) return emptySet() return tokensResponse.tokens - .filter { it.id == currencyId } + .filter { getL2CompatibilityTokenComparison(it, currencyId) } .mapNotNullTo(mutableSetOf()) { token -> val blockchain = Blockchain.fromNetworkId(token.networkId) diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 8f9fa44f1b..420a16f3e4 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.markets import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.applyL2Compatibility import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetwork @@ -144,7 +145,8 @@ internal class DefaultMarketsTokenRepository( language = languageCode, ) - return TokenMarketInfoConverter.convert(response.getOrThrow()) + val resultResponse = response.getOrThrow().applyL2Compatibility(tokenId) + return TokenMarketInfoConverter.convert(resultResponse) } override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes { 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 238150055c..f76a9353fb 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 @@ -2,6 +2,7 @@ package com.tangem.data.tokens.repository import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall @@ -488,7 +489,9 @@ internal class DefaultCurrenciesRepository( .map { userWallet -> if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> - val filterResponse = storedTokens.tokens.filter { it.id == currencyRawId } + val filterResponse = storedTokens.tokens.filter { + getL2CompatibilityTokenComparison(it, currencyRawId) + } responseCurrenciesFactory.createCurrencies( response = storedTokens.copy(tokens = filterResponse), diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt index ad6e5fb457..1fde5f7040 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt @@ -1,7 +1,5 @@ package com.tangem.feature.swap.converters -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.converter.Converter @@ -11,7 +9,7 @@ class LeastTokenInfoConverter : Converter { override fun convert(value: CryptoCurrency): LeastTokenInfo { return LeastTokenInfo( contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = Blockchain.fromId(value.id.rawNetworkId).toNetworkId(), + network = value.network.backendId, ) } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt new file mode 100644 index 0000000000..b3572820d3 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt @@ -0,0 +1,59 @@ +package com.tangem.blockchainsdk.compatibility + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse + +val l2NetworksList = listOf( + Blockchain.Optimism, + Blockchain.Arbitrum, + Blockchain.ZkSyncEra, + Blockchain.Manta, + Blockchain.PolygonZkEVM, + Blockchain.Aurora, + Blockchain.Base, + Blockchain.Blast, + Blockchain.Cyber, +) + +fun getL2CompatibilityTokenComparison(token: UserTokensResponse.Token, currencyId: String): Boolean { + return if (currencyId == ETHEREUM_COIN_ID) { + l2NetworksList.map { it.toCoinId() }.contains(token.id) || currencyId == token.id + } else { + token.id == currencyId + } +} + +fun List.applyL2Compatibility(coinId: String): List { + return if (coinId == ETHEREUM_COIN_ID) { + val l2Networks = l2NetworksList.map { + CoinsResponse.Coin.Network( + networkId = it.toNetworkId(), + ) + } + this + l2Networks + } else { + this + } +} + +fun TokenMarketInfoResponse.applyL2Compatibility(coinId: String): TokenMarketInfoResponse { + val networks = this.networks ?: return this + return if (coinId == ETHEREUM_COIN_ID) { + val l2Networks = l2NetworksList.map { + TokenMarketInfoResponse.Network( + networkId = it.toNetworkId(), + contractAddress = null, + decimalCount = null, + ) + } + this.copy(networks = networks + l2Networks) + } else { + this + } +} + +const val ETHEREUM_COIN_ID = "ethereum" \ No newline at end of file From 1f3f592cee1c200e0ff83e6501216088b0bc854b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Sep 2024 12:04:51 +0300 Subject: [PATCH 166/197] Updated on 2026-08-14 --- .../DefaultManageTokensRepository.kt | 9 +++++-- .../markets/DefaultMarketsTokenRepository.kt | 9 ++++--- .../QuotesUnsupportedCurrenciesIdAdapter.kt | 13 ++-------- features/markets/impl/build.gradle.kts | 1 + .../DefaultMarketsTokenDetailsComponent.kt | 13 +++++++--- .../block/impl/model/TokenMarketBlockModel.kt | 5 +++- .../WalletCurrencyActionsClickIntents.kt | 25 ++++++++++--------- .../blockchainsdk/compatibility/L2Networks.kt | 20 +++++++++++---- 8 files changed, 58 insertions(+), 37 deletions(-) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 6c0d025397..f1749f04d4 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.managetokens import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.currency.getBlockchain @@ -86,6 +87,10 @@ internal class DefaultManageTokensRepository( } else { retryOnError(call = call) } + // filter l2 coins + val updatedCoinsResponse = coinsResponse.copy( + coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) }, + ) val tokensResponse = request.params.userWalletId?.let { getSavedUserTokensResponseSync(it) } val items = if (isFirstBatchFetching && @@ -94,13 +99,13 @@ internal class DefaultManageTokensRepository( request.params.searchText.isNullOrBlank() ) { managedCryptoCurrencyFactory.createWithCustomTokens( - coinsResponse = coinsResponse, + coinsResponse = updatedCoinsResponse, tokensResponse = tokensResponse, derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, ) } else { managedCryptoCurrencyFactory.create( - coinsResponse = coinsResponse, + coinsResponse = updatedCoinsResponse, tokensResponse = tokensResponse, derivationStyleProvider = userWallet?.scanResponse?.derivationStyleProvider, ) diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 420a16f3e4..cd5b9ca9fc 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.markets import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.applyL2Compatibility +import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetwork @@ -109,9 +110,10 @@ internal class DefaultMarketsTokenRepository( interval: PriceChangeInterval, tokenId: String, ): TokenChart { + val mappedTokenId = getTokenIdIfL2Network(tokenId) val response = marketsApi.getCoinChart( currency = fiatCurrencyCode, - coinId = tokenId, + coinId = mappedTokenId, interval = interval.toRequestParam(), ) @@ -123,13 +125,14 @@ internal class DefaultMarketsTokenRepository( interval: PriceChangeInterval, tokenId: String, ): TokenChart { + val mappedTokenId = getTokenIdIfL2Network(tokenId) val response = marketsApi.getCoinsListCharts( - coinIds = tokenId, + coinIds = mappedTokenId, currency = fiatCurrencyCode, interval = interval.toRequestParam(), ) - val chart = response.getOrThrow()[tokenId] ?: error("No chart preview data for token $tokenId") + val chart = response.getOrThrow()[mappedTokenId] ?: error("No chart preview data for token $mappedTokenId") return TokenChartConverter.convert(interval, chart) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index 5ae6618a2c..66f0ea8b64 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -1,6 +1,7 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.QuotesResponse @@ -51,16 +52,6 @@ internal class QuotesUnsupportedCurrenciesIdAdapter { * Map that contains unsupported currencies and their replacement for request */ private val ethCoinId = Blockchain.Ethereum.toCoinId() - private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = mapOf( - Blockchain.Optimism.toCoinId() to ethCoinId, - Blockchain.Arbitrum.toCoinId() to ethCoinId, - Blockchain.ZkSyncEra.toCoinId() to ethCoinId, - Blockchain.Manta.toCoinId() to ethCoinId, - Blockchain.PolygonZkEVM.toCoinId() to ethCoinId, - Blockchain.Aurora.toCoinId() to ethCoinId, - Blockchain.Base.toCoinId() to ethCoinId, - Blockchain.Blast.toCoinId() to ethCoinId, - Blockchain.Cyber.toCoinId() to ethCoinId, - ) + private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId } } } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index d356234b52..3ac39315a8 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -69,4 +69,5 @@ dependencies { /* Libs */ implementation(projects.libs.crypto) + implementation(projects.libs.blockchainSdk) } \ No newline at end of file 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 22c2c2b970..d78eb7c19f 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,6 +6,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel @@ -31,12 +32,18 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( portfolioComponentFactory: MarketsPortfolioComponent.Factory, ) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { - private val model: MarketsTokenDetailsModel = getOrCreateModel(params) + // applying l2 compatibility + private val updatedParams = params.copy( + token = params.token.copy( + id = getTokenIdIfL2Network(params.token.id), + ), + ) + private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams) - private val portfolioComponent: MarketsPortfolioComponent? = if (params.showPortfolio) { + private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.showPortfolio) { portfolioComponentFactory.create( context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params(params.token), + params = MarketsPortfolioComponent.Params(updatedParams.token), ) } else { null diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 71c71a4d5e..49adf98ff9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -14,7 +14,10 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.* +import com.tangem.domain.markets.GetCurrencyQuotesUseCase +import com.tangem.domain.markets.GetTokenPriceChartUseCase +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider 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 1a067b02f7..ebd530cb7c 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 @@ -386,20 +386,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( viewModelScope.launch { val rawId = cryptoCurrencyStatus.currency.id.rawCurrencyId ?: return@launch + val tokenMarketParams = TokenMarketParams( + id = rawId, + name = cryptoCurrencyStatus.currency.name, + symbol = cryptoCurrencyStatus.currency.symbol, + tokenQuotes = TokenMarketParams.Quotes( + currentPrice = cryptoCurrencyStatus.value.fiatRate ?: BigDecimal.ZERO, + h24Percent = cryptoCurrencyStatus.value.priceChange, + weekPercent = null, + monthPercent = null, + ), + imageUrl = cryptoCurrencyStatus.currency.iconUrl, + ) appRouter.push( AppRoute.MarketsTokenDetails( - TokenMarketParams( - id = rawId, - name = cryptoCurrencyStatus.currency.name, - symbol = cryptoCurrencyStatus.currency.symbol, - tokenQuotes = TokenMarketParams.Quotes( - currentPrice = cryptoCurrencyStatus.value.fiatRate ?: BigDecimal.ZERO, - h24Percent = cryptoCurrencyStatus.value.priceChange, - weekPercent = null, - monthPercent = null, - ), - imageUrl = cryptoCurrencyStatus.currency.iconUrl, - ), + token = tokenMarketParams, appCurrency = getSelectedAppCurrencyUseCase.unwrap(), showPortfolio = true, ), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt index b3572820d3..1248e3dedd 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -val l2NetworksList = listOf( +internal val l2BlockchainsList = listOf( Blockchain.Optimism, Blockchain.Arbitrum, Blockchain.ZkSyncEra, @@ -19,9 +19,13 @@ val l2NetworksList = listOf( Blockchain.Cyber, ) +val l2BlockchainsCoinIds = l2BlockchainsList.map { it.toCoinId() } + +val ETHEREUM_COIN_ID = Blockchain.Ethereum.toCoinId() + fun getL2CompatibilityTokenComparison(token: UserTokensResponse.Token, currencyId: String): Boolean { return if (currencyId == ETHEREUM_COIN_ID) { - l2NetworksList.map { it.toCoinId() }.contains(token.id) || currencyId == token.id + l2BlockchainsCoinIds.contains(token.id) || currencyId == token.id } else { token.id == currencyId } @@ -29,7 +33,7 @@ fun getL2CompatibilityTokenComparison(token: UserTokensResponse.Token, currencyI fun List.applyL2Compatibility(coinId: String): List { return if (coinId == ETHEREUM_COIN_ID) { - val l2Networks = l2NetworksList.map { + val l2Networks = l2BlockchainsList.map { CoinsResponse.Coin.Network( networkId = it.toNetworkId(), ) @@ -43,7 +47,7 @@ fun List.applyL2Compatibility(coinId: String): List< fun TokenMarketInfoResponse.applyL2Compatibility(coinId: String): TokenMarketInfoResponse { val networks = this.networks ?: return this return if (coinId == ETHEREUM_COIN_ID) { - val l2Networks = l2NetworksList.map { + val l2Networks = l2BlockchainsList.map { TokenMarketInfoResponse.Network( networkId = it.toNetworkId(), contractAddress = null, @@ -56,4 +60,10 @@ fun TokenMarketInfoResponse.applyL2Compatibility(coinId: String): TokenMarketInf } } -const val ETHEREUM_COIN_ID = "ethereum" \ No newline at end of file +fun getTokenIdIfL2Network(tokenId: String): String { + return if (l2BlockchainsCoinIds.contains(tokenId)) { + ETHEREUM_COIN_ID + } else { + tokenId + } +} \ No newline at end of file From 0d8a99dd488b80dcd3ca8dcce10c3a9ff4fec695 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 18:36:06 +0400 Subject: [PATCH 167/197] Updated on 2026-08-14 --- .../com/tangem/utils/coroutines/JobHolder.kt | 4 +- .../managetokens/model/AddCustomTokenForm.kt | 2 +- .../entity/customtoken/CustomTokenFormUM.kt | 1 + .../model/CustomTokenFormModel.kt | 47 ++++-- .../utils/CustomCurrencyValidator.kt | 143 +++++++++++------- .../utils/ui/CustomCurrencyFormOperations.kt | 2 + 6 files changed, 130 insertions(+), 69 deletions(-) diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 14e64f1b62..d50f9f295c 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -24,4 +24,6 @@ class JobHolder { } } -fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) \ No newline at end of file +fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) + +suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join() \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt index b70cd54ab9..a5b5dc5bdc 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt @@ -9,7 +9,7 @@ sealed class AddCustomTokenForm { val decimals: String, ) : AddCustomTokenForm() - sealed class Validated { + sealed class Validated : AddCustomTokenForm() { data class ContractAddress( val contractAddress: String, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index 1fbd8e4de3..fe72248f0d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -20,6 +20,7 @@ internal data class CustomTokenFormUM( val name: TextInputFieldUM, val symbol: TextInputFieldUM, val decimals: TextInputFieldUM, + val wasFilled: Boolean = false, ) data class NotificationUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 2fa77b8e92..4e678f1bb4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -92,15 +92,15 @@ internal class CustomTokenFormModel @Inject constructor( @OptIn(FlowPreview::class) private fun observeTokenFormUpdates() { state - .sample(periodMillis = 1_000) - .transform { - val values = it.tokenForm?.mapToDomainModel() + .transform { state -> + val form = state.tokenForm - if (values != null) { - emit(values) + if (form != null && !form.wasFilled) { + emit(form.mapToDomainModel()) } } .distinctUntilChanged() + .sample(periodMillis = 1_000) .onEach { formValues -> customCurrencyValidator.validateForm( userWalletId = params.userWalletId, @@ -117,14 +117,16 @@ internal class CustomTokenFormModel @Inject constructor( createdCurrency = null when (validatorState) { - is CustomCurrencyValidator.State.NotStarted -> Unit - is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching() - is CustomCurrencyValidator.State.UnexpectedException -> showErrorDialog() - is CustomCurrencyValidator.State.FormValidationException -> updateStateWithExceptions( + is CustomCurrencyValidator.Status.NotStarted, + is CustomCurrencyValidator.Status.Validating, + -> Unit + is CustomCurrencyValidator.Status.SearchingToken -> updateStateWithProgress() + is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog() + is CustomCurrencyValidator.Status.FormValidationException -> updateStateWithExceptions( exceptions = validatorState.exceptions, ) - is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification() - is CustomCurrencyValidator.State.Validated -> { + is CustomCurrencyValidator.Status.TokenNotFound -> updateStateWithNotFoundNotification() + is CustomCurrencyValidator.Status.Validated -> { createdCurrency = validatorState.currency updateStateWithCurrency( @@ -149,6 +151,7 @@ internal class CustomTokenFormModel @Inject constructor( .updateWithProgress( showProgress = false, canAddToken = !isAlreadyAdded, + isWasFilled = fillForm, clearNotifications = true, clearFieldErrors = true, disableSecondaryFields = !isCustom, @@ -183,7 +186,7 @@ internal class CustomTokenFormModel @Inject constructor( } } - private fun updateStateWithSearching() { + private fun updateStateWithProgress() { state.update { state -> state.updateWithProgress( showProgress = true, @@ -274,7 +277,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateContractAddress(value: String) { state.update { state -> state.updateTokenForm { - copy(contractAddress = contractAddress.updateValue(value)) + copy( + contractAddress = contractAddress.updateValue(value), + wasFilled = false, + ) } } } @@ -282,7 +288,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateTokenName(value: String) { state.update { state -> state.updateTokenForm { - copy(name = name.updateValue(value)) + copy( + name = name.updateValue(value), + wasFilled = false, + ) } } } @@ -290,7 +299,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateTokenSymbol(value: String) { state.update { state -> state.updateTokenForm { - copy(symbol = symbol.updateValue(value)) + copy( + symbol = symbol.updateValue(value), + wasFilled = false, + ) } } } @@ -298,7 +310,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateDecimals(value: String) { state.update { state -> state.updateTokenForm { - copy(decimals = decimals.updateValue(value)) + copy( + decimals = decimals.updateValue(value), + wasFilled = false, + ) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt index 373b4640c7..c4876c9914 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -12,8 +12,11 @@ import com.tangem.domain.managetokens.model.exceptoin.FindTokenException import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveInAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -25,12 +28,20 @@ internal class CustomCurrencyValidator @Inject constructor( private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, ) { - private val state: MutableStateFlow = MutableStateFlow(State.NotStarted) + private val validateFormJobHolder = JobHolder() + private val state: MutableStateFlow = MutableStateFlow( + value = State( + prevValidatedForm = null, + prevFoundOrCreatedCurrency = null, + status = Status.NotStarted, + ), + ) - suspend fun consumeUpdates(block: suspend (State) -> Unit) { - state.collectLatest { s -> - block(s) - } + suspend fun consumeUpdates(block: suspend (Status) -> Unit) { + state + .map { it.status } + .distinctUntilChanged() + .collectLatest { block(it) } } suspend fun validateForm( @@ -38,25 +49,37 @@ internal class CustomCurrencyValidator @Inject constructor( networkId: Network.ID, derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Raw, - ) { + ) = coroutineScope { + updateStatus(Status.Validating) + val result = validateTokenFormUseCase( networkId = networkId, formValues = formValues, ) val validatedForm = result.getOrElse { e -> - state.value = State.FormValidationException(e) - return + updateStatus(Status.FormValidationException(e)) + return@coroutineScope } - when (validatedForm) { - is AddCustomTokenForm.Validated.All -> { - findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm) - } - is AddCustomTokenForm.Validated.ContractAddress -> { - findToken(userWalletId, networkId, derivationPath, validatedForm) + if (state.value.prevValidatedForm == validatedForm) { + return@coroutineScope + } else { + state.update { state -> + state.copy(prevValidatedForm = validatedForm) } } + + launch { + when (validatedForm) { + is AddCustomTokenForm.Validated.All -> { + findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm) + } + is AddCustomTokenForm.Validated.ContractAddress -> { + findToken(userWalletId, networkId, derivationPath, validatedForm) + } + } + }.saveInAndJoin(validateFormJobHolder) } suspend fun createCoin(userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath) { @@ -69,7 +92,16 @@ internal class CustomCurrencyValidator @Inject constructor( derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.All, ) { - state.value = State.SearchingToken + val currentState = state.value + if (currentState.prevFoundOrCreatedCurrency is CryptoCurrency.Token && + currentState.prevFoundOrCreatedCurrency.contractAddress == validatedForm.contractAddress + ) { + // No need to search for token again if contract address is not changed + createCurrency(userWalletId, networkId, derivationPath, validatedForm) + return + } + + updateStatus(Status.SearchingToken) val foundToken = findTokenUseCase( userWalletId = userWalletId, @@ -80,7 +112,7 @@ internal class CustomCurrencyValidator @Inject constructor( when (e) { is FindTokenException.DataError -> { Timber.e(e.cause, "Unable to find custom currency") - state.value = State.UnexpectedException(e.cause) + updateStatus(Status.UnexpectedException(e.cause)) return } is FindTokenException.NotFound -> { @@ -102,7 +134,7 @@ internal class CustomCurrencyValidator @Inject constructor( derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.ContractAddress, ) { - state.value = State.SearchingToken + updateStatus(Status.SearchingToken) val token = findTokenUseCase( userWalletId = userWalletId, @@ -110,15 +142,16 @@ internal class CustomCurrencyValidator @Inject constructor( networkId = networkId, derivationPath = derivationPath, ).getOrElse { e -> - state.value = when (e) { + val newStatus = when (e) { is FindTokenException.DataError -> { Timber.e(e.cause, "Unable to find custom currency") - State.UnexpectedException(e.cause) + Status.UnexpectedException(e.cause) } is FindTokenException.NotFound -> { - State.TokenNotFound + Status.TokenNotFound } } + updateStatus(newStatus) return } @@ -138,7 +171,7 @@ internal class CustomCurrencyValidator @Inject constructor( formValues = validatedForm, ).getOrElse { e -> Timber.e(e, "Unable to create custom currency") - state.value = State.UnexpectedException(e) + updateStatus(Status.UnexpectedException(e)) return } @@ -151,6 +184,9 @@ internal class CustomCurrencyValidator @Inject constructor( fillForm: Boolean, isCustom: Boolean, ) { + val currentStatus = state.value.status + if (currentStatus is Status.Validated && currentStatus.currency == currency) return + val isNotAdded = checkIsCurrencyNotAddedUseCase( userWalletId = userWalletId, networkId = currency.network.id, @@ -161,54 +197,59 @@ internal class CustomCurrencyValidator @Inject constructor( }, ).getOrElse { e -> Timber.e(e, "Unable to check if currency is already added") - state.value = State.UnexpectedException(e) + updateStatus(Status.UnexpectedException(e)) return } - state.value = State.Validated( - currency = currency, - fillForm = fillForm, - isAlreadyAdded = !isNotAdded, - isCustom = isCustom, - ) + state.update { state -> + state.copy( + status = Status.Validated( + currency = currency, + fillForm = fillForm, + isAlreadyAdded = !isNotAdded, + isCustom = isCustom, + ), + prevFoundOrCreatedCurrency = currency, + ) + } } - sealed class State { - - abstract val isFinished: Boolean - - data object NotStarted : State() { - override val isFinished: Boolean = false + private fun updateStatus(status: Status) { + state.update { state -> + state.copy(status = status) } + } - data object SearchingToken : State() { - override val isFinished: Boolean = false - } + data class State( + val prevValidatedForm: AddCustomTokenForm.Validated?, + val prevFoundOrCreatedCurrency: CryptoCurrency?, + val status: Status, + ) + + sealed class Status { + + data object NotStarted : Status() + + data object SearchingToken : Status() + + data object Validating : Status() data class Validated( val currency: CryptoCurrency, val fillForm: Boolean, val isAlreadyAdded: Boolean, val isCustom: Boolean, - ) : State() { - override val isFinished: Boolean = true - } + ) : Status() data class FormValidationException( val exceptions: List, - ) : State() { - override val isFinished: Boolean = true - } + ) : Status() - data object TokenNotFound : State() { - override val isFinished: Boolean = true - } + data object TokenNotFound : Status() data class UnexpectedException( val cause: Throwable, - ) : State() { - override val isFinished: Boolean = true - } + ) : Status() } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt index 71ecd3cd8d..ae05953156 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt @@ -38,6 +38,7 @@ internal fun TextInputFieldUM.updateValue( internal fun CustomTokenFormUM.updateWithProgress( showProgress: Boolean, + isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false, canAddToken: Boolean = this.canAddToken, clearNotifications: Boolean = false, clearFieldErrors: Boolean = false, @@ -64,6 +65,7 @@ internal fun CustomTokenFormUM.updateWithProgress( isEnabled = !showProgress && !disableSecondaryFields, clearError = clearFieldErrors, ), + wasFilled = isWasFilled, ) } } From 57be17a027da461e4456977ce83a92d8a91edd9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 14:30:58 +0500 Subject: [PATCH 168/197] Updated on 2026-08-14 --- .../ui/components/common/WalletBottomSheet.kt | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt index 72233c34b7..23d7179e34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt @@ -56,19 +56,24 @@ private fun BottomSheetContent(config: WalletBottomSheetConfig) { }, ) - Text( - text = config.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) + Column( + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = config.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) - Text( - text = config.subtitle.resolveReference(), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body2, - ) + Text( + text = config.subtitle.resolveReference(), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body2, + ) + } Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { val buttonModifier = Modifier.fillMaxWidth() From 69c0d69adb0561a3e397b5c431dcf3b6b4831767 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Sep 2024 20:08:13 +0300 Subject: [PATCH 169/197] Updated on 2026-08-14 --- app/build.gradle.kts | 3 ++ .../core/ui/extensions/BlockchainIcons.kt | 7 ++- core/ui/src/main/res/drawable/ic_icp_22.xml | 12 +++++ core/ui/src/main/res/drawable/img_icp_22.xml | 45 +++++++++++++++++++ gradle.properties | 1 - gradle/dependencies.toml | 2 +- .../tangem/blockchainsdk/utils/Blockchain.kt | 5 +++ settings.gradle.kts | 12 ++++- 8 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_icp_22.xml create mode 100644 core/ui/src/main/res/drawable/img_icp_22.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01c88fedd1..0412c6eacf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,9 @@ android { jniLibs { useLegacyPackaging = true } + resources.excludes.add("META-INF/DEPENDENCIES") + resources.excludes.add("META-INF/LICENSE.md") + resources.excludes.add("META-INF/NOTICE.md") } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 97adf7cbe4..59fe1fa692 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -74,6 +74,7 @@ fun getActiveIconRes(blockchainId: String): Int { "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 "sei", "sei/test" -> R.drawable.img_sei_22 + "internet-computer" -> R.drawable.img_icp_22 else -> R.drawable.ic_alert_24 } } @@ -149,11 +150,12 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 "sei", "sei/test" -> R.drawable.img_sei_22 + "internet-computer" -> R.drawable.img_icp_22 else -> R.drawable.ic_alert_24 } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") @DrawableRes fun getActiveIconResByCoinId(coinId: String): Int { return when (coinId) { @@ -221,6 +223,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "filecoin" -> R.drawable.img_filecoin_22 "cyber", "cyber/test" -> R.drawable.img_cyber_22 "sei", "sei/test" -> R.drawable.img_sei_22 + "internet-computer" -> R.drawable.img_icp_22 else -> R.drawable.ic_alert_24 } } @@ -296,6 +299,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "filecoin" -> R.drawable.ic_filecoin_22 "cyber", "cyber/test" -> R.drawable.ic_cyber_22 "sei", "sei/test" -> R.drawable.ic_sei_22 + "internet-computer" -> R.drawable.ic_icp_22 else -> R.drawable.ic_alert_24 } } @@ -371,6 +375,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "filecoin" -> R.drawable.ic_filecoin_22 "cyber", "cyber/test" -> R.drawable.ic_cyber_22 "sei", "sei/test" -> R.drawable.ic_sei_22 + "internet-computer" -> R.drawable.ic_icp_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_icp_22.xml b/core/ui/src/main/res/drawable/ic_icp_22.xml new file mode 100644 index 0000000000..cd67883760 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_icp_22.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_icp_22.xml b/core/ui/src/main/res/drawable/img_icp_22.xml new file mode 100644 index 0000000000..6210d2c900 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_icp_22.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties index b2ef2745d2..c4b0b8ac32 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,5 @@ org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX: org.gradle.parallel = true org.gradle.daemon = true android.useAndroidX = true -android.enableJetifier = true org.gradle.unsafe.configuration-cache = true android.nonTransitiveRClass = false diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7872a1cb27..74c59e4344 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-762" +tangemBlockchainSdk = "develop-765" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-381" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 8974e4e1ce..cddafe6527 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -125,6 +125,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "cyber/test" -> Blockchain.CyberTestnet "sei-network" -> Blockchain.Sei "sei-network/test" -> Blockchain.SeiTestnet + "internet-computer" -> Blockchain.InternetComputer else -> null } } @@ -251,6 +252,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.CyberTestnet -> "cyber/test" Blockchain.Sei -> "sei-network" Blockchain.SeiTestnet -> "sei-network/test" + Blockchain.InternetComputer -> "internet-computer" } } @@ -334,6 +336,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Blast, Blockchain.BlastTestnet -> "blast-ethereum" Blockchain.Cyber, Blockchain.CyberTestnet -> "cyber-ethereum" Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network" + Blockchain.InternetComputer -> "internet-computer" } } @@ -364,4 +367,6 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, + Blockchain.Sei, + Blockchain.InternetComputer, ) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 740ce7ce0c..1940d94012 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -95,8 +95,18 @@ dependencyResolutionManagement { includeGroupAndSubgroups("com.tangem.vico") } } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/ic4j-agent") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { + includeGroupAndSubgroups("com.tangem.ic4j") + } + } maven("https://jitpack.io") - maven("https://clients-nexus.sprinklr.com/") } versionCatalogs { From cb6c79e11b8bb439cfe465f84c2a48074471f2b1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Sep 2024 14:10:05 +0300 Subject: [PATCH 170/197] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../components/notifications/Notification.kt | 39 +++++++++++++------ .../notifications/NotificationConfig.kt | 4 +- .../NotificationWithBackground.kt | 33 +++++++++------- .../notifications/OkxPromoNotification.kt | 15 ++++--- core/ui/src/main/res/drawable/ic_magic_28.xml | 9 +++++ .../impl/ui/components/InfoBottomSheet.kt | 36 +++++++++++++---- 8 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_magic_28.xml diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e3715ed204..567496ce4d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -297,6 +297,7 @@ Безлимитно Купить Сканировать + Информация сгенерирована ИИ Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ccbd670078..5fc80e50c7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -297,6 +297,7 @@ Unlimited Order card Scan card + This information was generated with AI To change the access code tap the card as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation To create the wallet tap the card as shown above and do not remove until the end of the operation diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index f25c4014cf..7a77b4cb67 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -30,6 +30,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.extensions.TextReference 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.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState @@ -51,6 +52,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsSta fun Notification( config: NotificationConfig, modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.tertiary, containerColor: Color? = null, iconTint: Color? = null, isEnabled: Boolean = true, @@ -68,6 +70,7 @@ fun Notification( iconTint = iconTint, title = config.title, subtitle = config.subtitle, + subtitleColor = subtitleColor, isClickableComponent = isEnabled && config.onClick != null, ) } @@ -94,7 +97,7 @@ internal fun NotificationBaseContainer( Surface( onClick = onClick ?: {}, modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .defaultMinSize(minHeight = TangemTheme.dimens.size44) .fillMaxWidth(), enabled = onClick != null && isEnabled, shape = TangemTheme.shapes.roundedCornersXMedium, @@ -119,15 +122,17 @@ internal fun NotificationBaseContainer( } } +@Suppress("LongParameterList") @Composable private fun MainContent( iconResId: Int, iconTint: Color?, - title: TextReference, + title: TextReference?, subtitle: TextReference, + subtitleColor: Color, isClickableComponent: Boolean, ) { - Row { + Row(verticalAlignment = Alignment.CenterVertically) { Icon( iconResId = iconResId, tint = iconTint, @@ -138,7 +143,7 @@ private fun MainContent( SpacerW(width = TangemTheme.dimens.spacing10) - TextsBlock(title = title, subtitle = subtitle) + TextsBlock(title = title, subtitle = subtitle, subtitleColor = subtitleColor) if (isClickableComponent) { SpacerWMax() @@ -174,17 +179,23 @@ private fun Icon(@DrawableRes iconResId: Int, tint: Color?, modifier: Modifier = } @Composable -private fun TextsBlock(title: TextReference, subtitle: TextReference) { - Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { - Text( - text = title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.button, - ) +private fun TextsBlock(title: TextReference?, subtitle: TextReference, subtitleColor: Color) { + Column { + val titleText = title?.resolveReference() + + if (titleText != null) { + Text( + text = titleText, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + SpacerH(height = TangemTheme.dimens.spacing2) + } Text( text = subtitle.resolveReference(), - color = TangemTheme.colors.text.tertiary, + color = subtitleColor, style = TangemTheme.typography.caption2, ) } @@ -363,5 +374,9 @@ private class NotificationConfigProvider : CollectionPreviewParameterProvider Unit)? = null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 34ae262e6d..13a8aac488 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -7,9 +7,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -33,8 +33,8 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.TangemColorPalette.Dark6 import com.tangem.core.ui.res.TangemColorPalette.Light4 -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Custom notification with image background @@ -85,17 +85,22 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) }, ) - Text( - text = config.title.resolveReference(), - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.constantWhite, - modifier = Modifier.constrainAs(titleRef) { - top.linkTo(parent.top, spacing12) - start.linkTo(iconRef.end, spacing12) - end.linkTo(closeIconRef.start, spacing2) - width = Dimension.fillToConstraints - }, - ) + + val titleText = config.title?.resolveReference() + if (titleText != null) { + Text( + text = titleText, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.constantWhite, + modifier = Modifier.constrainAs(titleRef) { + top.linkTo(parent.top, spacing12) + start.linkTo(iconRef.end, spacing12) + end.linkTo(closeIconRef.start, spacing2) + width = Dimension.fillToConstraints + }, + ) + } + Text( text = config.subtitle.resolveReference(), style = TangemTheme.typography.caption2, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt index eacaa9567e..19d96e409b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt @@ -63,11 +63,16 @@ private fun Content(config: NotificationConfig) { .weight(1f) .padding(TangemTheme.dimens.spacing12), ) { - Text( - text = config.title.resolveReference(), - style = TangemTheme.typography.button, - color = OxkPromoColor, - ) + val titleText = config.title?.resolveReference() + + if (titleText != null) { + Text( + text = titleText, + style = TangemTheme.typography.button, + color = OxkPromoColor, + ) + } + Text( text = config.subtitle.resolveReference(), style = TangemTheme.typography.caption2, diff --git a/core/ui/src/main/res/drawable/ic_magic_28.xml b/core/ui/src/main/res/drawable/ic_magic_28.xml new file mode 100644 index 0000000000..c6f4bf1724 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_magic_28.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt index d78f6b2d99..180c7eefa9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt @@ -1,9 +1,6 @@ package com.tangem.features.markets.details.impl.ui.components -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -13,9 +10,13 @@ import com.tangem.core.ui.components.SpacerH 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.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +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.markets.details.impl.ui.state.InfoBottomSheetContent +import com.tangem.features.markets.impl.R import dev.jeziellago.compose.markdowntext.MarkdownText @Composable @@ -26,14 +27,12 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { config = config, skipPartiallyExpanded = false, addBottomInsets = false, - title = { - TangemBottomSheetTitle(title = it.title) - }, + title = { TangemBottomSheetTitle(title = it.title) }, content = { Column( modifier = Modifier .verticalScroll(rememberScrollState()) - .padding(horizontal = TangemTheme.dimens.spacing28), + .padding(horizontal = TangemTheme.dimens.spacing16), ) { MarkdownText( markdown = it.body.resolveReference(), @@ -44,8 +43,29 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { color = TangemTheme.colors.text.secondary, ), ) + + AdditionalInfoNotification( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + SpacerH(bottomBarHeight) } }, ) +} + +@Composable +private fun AdditionalInfoNotification(modifier: Modifier = Modifier) { + Notification( + config = NotificationConfig( + subtitle = TextReference.Res(id = R.string.information_generated_with_ai), + iconResId = R.drawable.ic_magic_28, + ), + modifier = modifier, + subtitleColor = TangemTheme.colors.text.primary1, + containerColor = TangemTheme.colors.button.disabled, + iconTint = TangemTheme.colors.icon.accent, + ) } \ No newline at end of file From 88ed3c4379847c432665573989078677473a9527 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 09:23:25 +0300 Subject: [PATCH 171/197] Updated on 2026-08-14 --- .../impl/ui/MarketsTokenDetailsContent.kt | 13 ++--- .../impl/model/MarketsPortfolioModel.kt | 3 - .../impl/model/MyPortfolioUMFactory.kt | 4 +- .../impl/model/PortfolioTokenUMConverter.kt | 3 - .../impl/model/TokenActionsHandler.kt | 55 ------------------- .../impl/model/TokensPortfolioUMConverter.kt | 3 - .../markets/portfolio/impl/ui/MyPortfolio.kt | 3 +- .../preview/PreviewMyPortfolioUMProvider.kt | 3 - .../portfolio/impl/ui/state/MyPortfolioUM.kt | 1 - .../WalletCurrencyActionsClickIntents.kt | 2 +- 10 files changed, 11 insertions(+), 79 deletions(-) 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 295497c053..275a6427e7 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 @@ -4,11 +4,11 @@ import android.content.res.Configuration import androidx.compose.animation.Animatable import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -151,9 +151,7 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween, ) { - Column( - Modifier.weight(1f), - ) { + Column(modifier = Modifier.weight(1f)) { TokenPriceText( price = state.priceText, triggerPriceChange = state.triggerPriceChange, @@ -208,10 +206,11 @@ private fun TokenPriceText( color.animateTo(generalColor, tween(durationMillis = 500)) } - Text( - modifier = modifier, + ResizableText( text = price, + modifier = modifier, color = color.value, + maxLines = 1, style = TangemTheme.typography.head, ) } 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 42c3dd2e07..d3291921c8 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 @@ -71,9 +71,6 @@ internal class MarketsPortfolioModel @Inject constructor( currentState = Provider { _state.value }, tokenActionsHandler = tokenActionsIntentsFactory.create( currentAppCurrency = Provider { currentAppCurrency.value }, - updateTokenActionsBSConfig = { updateBlock -> - updateTokensState { it.copy(tokenActionsBSConfig = updateBlock(it.tokenActionsBSConfig)) } - }, updateTokenReceiveBSConfig = { updateBlock -> updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index c52637100f..83af1ca3c2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -30,7 +30,9 @@ internal class MyPortfolioUMFactory( fun create(portfolioData: PortfolioData, portfolioUIData: PortfolioUIData): MyPortfolioUM { val addToPortfolioData = portfolioUIData.addToPortfolioData - if (addToPortfolioData.availableNetworks?.isEmpty() == true) return MyPortfolioUM.Unavailable + val hasAvailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true + val isOnlySingleWalletsAdded = portfolioData.walletsWithCurrencies.keys.all { !it.isMultiCurrency } + if (hasAvailableNetworks || isOnlySingleWalletsAdded) return MyPortfolioUM.Unavailable val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) { portfolioData.walletsWithCurrencies diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index 218d63c9ae..1125fda4b1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -34,9 +34,6 @@ internal class PortfolioTokenUMConverter( TokenItemState.SubtitleState.TextContent(value = value.status.currency.name) }, onItemClick = onTokenItemClick, - onItemLongClick = { - tokenActionsHandler.onTokenLongClick(cryptoCurrencyData = value) - }, ) return PortfolioTokenUM( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index c134371f8e..8b8d8ffa82 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -13,13 +13,11 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.utils.Provider -import com.tangem.utils.isNullOrZero import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -36,8 +34,6 @@ internal class TokenActionsHandler @AssistedInject constructor( private val currentAppCurrency: Provider, @Assisted("updateTokenReceiveBSConfig") private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, - @Assisted("updateTokenActionsBSConfig") - private val updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, ) { fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { @@ -52,38 +48,6 @@ internal class TokenActionsHandler @AssistedInject constructor( } } - fun onTokenLongClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val actions = cryptoCurrencyData.actions - .mapNotNull { it.toAction(cryptoCurrencyData) } - .sortedBy { it.order } - .toImmutableList() - - if (actions.isEmpty()) { - return - } - - updateTokenActionsBSConfig { - TangemBottomSheetConfig( - isShow = true, - onDismissRequest = { - updateTokenActionsBSConfig { - it.copy(isShow = false) - } - }, - content = TokenActionsBSContentUM( - title = cryptoCurrencyData.userWallet.name, - actions = actions, - onActionClick = { action -> - handle(action, cryptoCurrencyData) - updateTokenActionsBSConfig { - it.copy(isShow = false) - } - }, - ), - ) - } - } - private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { val cryptoCurrencyStatus = cryptoCurrencyData.status val currency = cryptoCurrencyStatus.currency @@ -174,31 +138,12 @@ internal class TokenActionsHandler @AssistedInject constructor( ) } - private fun TokenActionsState.ActionState.toAction( - cryptoCurrencyData: PortfolioData.CryptoCurrencyData, - ): TokenActionsBSContentUM.Action? = when (this) { - is TokenActionsState.ActionState.Buy -> TokenActionsBSContentUM.Action.Buy - is TokenActionsState.ActionState.CopyAddress -> TokenActionsBSContentUM.Action.CopyAddress - is TokenActionsState.ActionState.HideToken -> null - is TokenActionsState.ActionState.Receive -> TokenActionsBSContentUM.Action.Receive - is TokenActionsState.ActionState.Sell -> TokenActionsBSContentUM.Action.Sell - is TokenActionsState.ActionState.Send -> TokenActionsBSContentUM.Action.Send.takeIf { - cryptoCurrencyData.status.value.amount.isNullOrZero().not() - } - is TokenActionsState.ActionState.Stake -> TokenActionsBSContentUM.Action.Stake - is TokenActionsState.ActionState.Swap -> TokenActionsBSContentUM.Action.Exchange - is TokenActionsState.ActionState.Analytics -> null - } - .takeIf { this.unavailabilityReason == ScenarioUnavailabilityReason.None } - @AssistedFactory interface Factory { fun create( currentAppCurrency: Provider, @Assisted("updateTokenReceiveBSConfig") updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, - @Assisted("updateTokenActionsBSConfig") - updateTokenActionsBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, ): TokenActionsHandler } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index 7e847834d2..5078f51123 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -53,9 +53,6 @@ internal class TokensPortfolioUMConverter( tokenReceiveBSConfig = (currentState() as? MyPortfolioUM.Tokens) ?.tokenReceiveBSConfig ?: TangemBottomSheetConfig.Empty, - tokenActionsBSConfig = (currentState() as? MyPortfolioUM.Tokens) - ?.tokenActionsBSConfig - ?: TangemBottomSheetConfig.Empty, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index dda1777df3..58247416e1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -25,8 +25,8 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState @Composable internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { @@ -108,7 +108,6 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier } TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig) - TokenActionsBottomSheet(config = state.tokenActionsBSConfig) } @Composable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 3d027349ed..bdff4f722c 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 @@ -20,7 +20,6 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider Unit, ) : MyPortfolioUM() { 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 ebd530cb7c..d22db8edce 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 @@ -402,7 +402,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( AppRoute.MarketsTokenDetails( token = tokenMarketParams, appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - showPortfolio = true, + showPortfolio = false, ), ) } From 8e05a68123f81b14b95a21e74a7186d86bf1e018 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 18:36:40 +0400 Subject: [PATCH 172/197] Updated on 2026-08-14 --- .../presentation/wallet/ui/WalletScreen.kt | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 1c17411267..106556de2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -31,8 +31,10 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -142,6 +144,8 @@ private fun WalletContent( var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] } + val listState = rememberLazyListState() + val scaffoldContent: @Composable () -> Unit = { val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) @@ -162,11 +166,10 @@ private fun WalletContent( val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TestTags.MAIN_SCREEN), + modifier = Modifier.testTag(TestTags.MAIN_SCREEN), + state = listState, contentPadding = PaddingValues( - bottom = TangemTheme.dimens.spacing92 + bottomBarHeight, + bottom = 184.dp + bottomBarHeight, ), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -239,6 +242,7 @@ private fun WalletContent( BaseScaffoldWithMarkets( state = state, + listState = listState, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, bottomSheetHeaderHeightProvider = { headerSize }, @@ -329,11 +333,12 @@ private fun BaseScaffold( ) } -@Suppress("LongParameterList", "LongMethod") +@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable private inline fun BaseScaffoldWithMarkets( state: WalletScreenState, + listState: LazyListState, selectedWallet: WalletState, snackbarHostState: SnackbarHostState, bottomSheetHeaderHeightProvider: () -> Dp, @@ -376,6 +381,16 @@ private inline fun BaseScaffoldWithMarkets( val coroutineScope = rememberCoroutineScope() val backgroundPrimary = TangemTheme.colors.background.primary + val showMarketsHint by remember { + derivedStateOf { + // Show hint only when there are items in the list + // and when there a no items to scroll + listState.layoutInfo.totalItemsCount > 0 && + !listState.canScrollBackward && !listState.canScrollForward || + listState.canScrollBackward && !listState.canScrollForward + } + } + CompositionLocalProvider( LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) }, ) { @@ -432,8 +447,18 @@ private inline fun BaseScaffoldWithMarkets( ) Box { + MarketsHint( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = peekHeight + 12.dp) + .fillMaxWidth(fraction = .4f), + isVisible = showMarketsHint, + ) + Column { - WalletTopBar(config = state.topBarConfig) + WalletTopBar( + config = state.topBarConfig, + ) Box( modifier = Modifier.pullRefresh(pullRefreshState), ) { @@ -522,6 +547,34 @@ private fun MarketsTooltip( } } +@Composable +internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier, + visible = isVisible, + enter = fadeIn(animationSpec = tween(durationMillis = 500)), + exit = fadeOut(), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(space = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "To add tokens pull this up or tap the search bar", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + Icon( + modifier = Modifier.size(size = 24.dp), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } +} + @Composable private fun MarketsTooltipContent(modifier: Modifier = Modifier) { val backgroundColor = TangemTheme.colors.background.primary From af9be6d3c9996cb4f21a5cba21ec0b7e79ae3e3c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 13:29:01 +0500 Subject: [PATCH 173/197] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../presentation/state/StakingNotification.kt | 5 + .../AddStakingNotificationsTransformer.kt | 138 ++++++++---------- .../state/utils/RewardScheduleUtils.kt | 10 +- 4 files changed, 75 insertions(+), 79 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5fc80e50c7..03805641a9 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -688,6 +688,7 @@ Your funds will be available for use after the 21-day unbonding period. Reward will be withdrawn along with your unstaking funds. Your funds will be available for use after the %s unbonding period. You can now withdraw your funds, it will be available to use immediately + Staking in the Tron network with a new validator will automatically transfer all previously staked funds to this validator Preparing Ready to withdraw Rebond 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 3f8be15c42..65968aeafa 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 @@ -90,5 +90,10 @@ internal object StakingNotification { title = title, subtitle = text, ) + + data object TronRevote : StakingNotification.Info( + title = resourceReference(R.string.staking_revote), + subtitle = resourceReference(R.string.staking_notifications_revote_tron_text), + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 6f68eb4cc8..7132cb9280 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -15,7 +15,9 @@ import com.tangem.core.ui.extensions.pluralReference 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.BalanceType import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -30,6 +32,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer @@ -203,86 +206,73 @@ internal class AddStakingNotificationsTransformer( private fun MutableList.addInfoNotifications(prevState: StakingUiState) { when (prevState.actionType) { - StakingActionCommonType.EXIT -> { - add( - StakingNotification.Info.Unstake( - cooldownPeriodDays = yield.metadata.cooldownPeriod.days, - ), - ) - } - StakingActionCommonType.ENTER -> { - add( - StakingNotification.Info.EarnRewards( - subtitleText = resourceReference( - id = getEarnRewardsPeriod(yield.metadata.rewardSchedule), - formatArgs = wrappedList(yield.token.name), - ), - ), - ) - } - else -> { - val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - val pendingActionType = confirmationState?.pendingAction?.type - val (titleReference, textReference) = when (pendingActionType) { - StakingActionType.CLAIM_REWARDS -> { - resourceReference(R.string.common_claim) to - resourceReference(R.string.staking_notification_claim_rewards_text) - } - StakingActionType.RESTAKE_REWARDS -> { - resourceReference(R.string.staking_restake) to - resourceReference(R.string.staking_notification_restake_rewards_text) - } - StakingActionType.WITHDRAW -> { - resourceReference(R.string.staking_withdraw) to - resourceReference(R.string.staking_notification_withdraw_text) - } - StakingActionType.UNLOCK_LOCKED -> { - val cooldownPeriodDays = yield.metadata.cooldownPeriod.days - resourceReference(R.string.staking_unlocked_locked) to resourceReference( - R.string.staking_notification_unlock_text, - wrappedList( - pluralReference( - id = R.plurals.common_days, - count = cooldownPeriodDays, - formatArgs = wrappedList(cooldownPeriodDays), - ), - ), - ) - } - else -> null to null - } - - if (titleReference != null && textReference != null) { - add( - StakingNotification.Info.PendingAction( - title = titleReference, - text = textReference, - ), - ) - } - } + StakingActionCommonType.EXIT -> addExitInfoNotifications() + StakingActionCommonType.ENTER -> addEnterInfoNotifications() + else -> addPendingInfoNotifications(prevState) } } - private fun getEarnRewardsPeriod(rewardSchedule: Yield.Metadata.RewardSchedule): Int { - return when (rewardSchedule) { - 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 + private fun MutableList.addExitInfoNotifications() { + add( + StakingNotification.Info.Unstake( + cooldownPeriodDays = yield.metadata.cooldownPeriod.days, + ), + ) + } - Yield.Metadata.RewardSchedule.HOUR, - -> R.string.staking_notification_earn_rewards_text_period_hour + private fun MutableList.addEnterInfoNotifications() { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) + val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance + ?.items?.any { + it.type == BalanceType.PREPARING || + it.type == BalanceType.STAKED || + it.type == BalanceType.LOCKED + } == true + if (hasStakedBalance && isTron) { + add(StakingNotification.Info.TronRevote) + } + } - Yield.Metadata.RewardSchedule.WEEK, - -> R.string.staking_notification_earn_rewards_text_period_week + private fun MutableList.addPendingInfoNotifications(prevState: StakingUiState) { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val pendingActionType = confirmationState?.pendingAction?.type + val (titleReference, textReference) = when (pendingActionType) { + StakingActionType.CLAIM_REWARDS -> { + resourceReference(R.string.common_claim) to + resourceReference(R.string.staking_notification_claim_rewards_text) + } + StakingActionType.RESTAKE_REWARDS -> { + resourceReference(R.string.staking_restake) to + resourceReference(R.string.staking_notification_restake_rewards_text) + } + StakingActionType.WITHDRAW -> { + resourceReference(R.string.staking_withdraw) to + resourceReference(R.string.staking_notification_withdraw_text) + } + StakingActionType.UNLOCK_LOCKED -> { + val cooldownPeriodDays = yield.metadata.cooldownPeriod.days + resourceReference(R.string.staking_unlocked_locked) to resourceReference( + R.string.staking_notification_unlock_text, + wrappedList( + pluralReference( + id = R.plurals.common_days, + count = cooldownPeriodDays, + formatArgs = wrappedList(cooldownPeriodDays), + ), + ), + ) + } + else -> null to null + } - Yield.Metadata.RewardSchedule.MONTH, - -> R.string.staking_notification_earn_rewards_text_period_month - - else - -> R.string.staking_notification_earn_rewards_text_period_day + if (titleReference != null && textReference != null) { + add( + StakingNotification.Info.PendingAction( + title = titleReference, + text = textReference, + ), + ) } } } \ No newline at end of file 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 index 8bb6550f79..1219166e7b 100644 --- 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 @@ -9,18 +9,18 @@ internal fun Yield.Metadata.RewardSchedule.getStringResourceId(): Int { Yield.Metadata.RewardSchedule.DAY, Yield.Metadata.RewardSchedule.ERA, Yield.Metadata.RewardSchedule.EPOCH, - -> R.string.staking_notification_earn_rewards_text_period_day + -> R.string.staking_notification_earn_rewards_text_daily Yield.Metadata.RewardSchedule.HOUR, - -> R.string.staking_notification_earn_rewards_text_period_hour + -> R.string.staking_notification_earn_rewards_text_hourly Yield.Metadata.RewardSchedule.WEEK, - -> R.string.staking_notification_earn_rewards_text_period_week + -> R.string.staking_notification_earn_rewards_text_weekly Yield.Metadata.RewardSchedule.MONTH, - -> R.string.staking_notification_earn_rewards_text_period_month + -> R.string.staking_notification_earn_rewards_text_monthly else - -> R.string.staking_notification_earn_rewards_text_period_day + -> R.string.staking_notification_earn_rewards_text_daily } } \ No newline at end of file From e2d014420a94415f9f3da72ae1d7ddaa49ea4bc7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 12:05:36 +0300 Subject: [PATCH 174/197] Updated on 2026-08-14 --- .../core/ui/components/rows/RowComponents.kt | 2 +- .../ResponseCryptoCurrenciesFactory.kt | 15 ++------------- .../utils/ManagedCryptoCurrencyFactory.kt | 2 ++ .../SdkTransactionHistoryItemConverter.kt | 8 ++++++++ .../utils/SdkTransactionTypeConverter.kt | 9 +++++++++ .../model/ManagedCryptoCurrency.kt | 8 +++++++- .../impl/model/BlockchainRowUMConverter.kt | 19 ++++++++++++++++++- gradle/dependencies.toml | 2 +- .../blockchainsdk/compatibility/L2Networks.kt | 2 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 6 ++++++ 10 files changed, 55 insertions(+), 18 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt index 258a153de4..78c7a724b7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -78,7 +78,7 @@ internal fun RowText( ) Text( - modifier = Modifier.weight(weight = 4f, fill = false), + modifier = Modifier.weight(weight = 5f, fill = false), text = secondText, style = TangemTheme.typography.body2, color = if (isEnabled) { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 24db327e04..35b0736c8e 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -2,6 +2,7 @@ package com.tangem.data.common.currency import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.compatibility.l2BlockchainsList import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -91,22 +92,10 @@ class ResponseCryptoCurrenciesFactory { // get name and symbol from enum Blockchain until backend renamed // [REDACTED_JIRA] Blockchain.Dischain, - Blockchain.Arbitrum, - Blockchain.ArbitrumTestnet, - Blockchain.Aurora, - Blockchain.AuroraTestnet, - Blockchain.Manta, - Blockchain.MantaTestnet, - Blockchain.ZkSyncEra, - Blockchain.ZkSyncEraTestnet, - Blockchain.PolygonZkEVM, - Blockchain.PolygonZkEVMTestnet, - Blockchain.Base, - Blockchain.BaseTestnet, Blockchain.Telos, Blockchain.Cronos, Blockchain.TON, - Blockchain.Cyber, + in l2BlockchainsList, -> this.fullName else -> responseToken.name } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 1832d13c52..5a5b9fd6eb 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -3,6 +3,7 @@ package com.tangem.data.managetokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.applyL2Compatibility import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison +import com.tangem.blockchainsdk.compatibility.l2BlockchainsList import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toCoinId @@ -121,6 +122,7 @@ internal class ManagedCryptoCurrencyFactory { SourceNetwork.Main( network = network, decimals = blockchain.decimals(), + isL2Network = l2BlockchainsList.contains(blockchain), ) } else { SourceNetwork.Default( diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index a429859de4..34200f0d8c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -57,6 +57,14 @@ internal class SdkTransactionHistoryItemConverter( is SdkTransactionHistoryItem.TransactionType.ContractMethod, is SdkTransactionHistoryItem.TransactionType.ContractMethodName, -> mapToInteractionAddressType(destinationType) + TransactionHistoryItem.TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> + mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] + TransactionHistoryItem.TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> + mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] + TransactionHistoryItem.TransactionType.TronStakingTransactionType.VoteWitnessContract -> + mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] + TransactionHistoryItem.TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> + mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt index c0ebfa9a66..e061f80d14 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -15,6 +15,15 @@ internal class SdkTransactionTypeConverter( getTransactionType(methodName = smartContractMethods[value.id]?.name) is TransactionHistoryItem.TransactionType.ContractMethodName -> getTransactionType(methodName = value.name) is TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer + TransactionHistoryItem.TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> + TxHistoryItem.TransactionType.Transfer // TODO map correct [REDACTED_JIRA] + TransactionHistoryItem.TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> + TxHistoryItem.TransactionType.Transfer // TODO map correct [REDACTED_JIRA] + TransactionHistoryItem.TransactionType.TronStakingTransactionType.VoteWitnessContract -> + TxHistoryItem + .TransactionType.Transfer // TODO map correct [REDACTED_JIRA] + TransactionHistoryItem.TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> + TxHistoryItem.TransactionType.Transfer // TODO map correct [REDACTED_JIRA] } } diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index 848616a5b2..b5c78a407e 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -62,7 +62,11 @@ sealed class ManagedCryptoCurrency { val typeName: String get() = when (this) { - is Main -> MAIN_NETWORK_TYPE_NAME + is Main -> if (isL2Network) { + MAIN_NETWORK_L2_TYPE_NAME + } else { + MAIN_NETWORK_TYPE_NAME + } is Default -> when (network.standardType) { is Network.StandardType.BEP2, is Network.StandardType.BEP20, @@ -76,6 +80,7 @@ sealed class ManagedCryptoCurrency { data class Main( override val network: Network, override val decimals: Int, + val isL2Network: Boolean, ) : SourceNetwork() data class Default( @@ -86,6 +91,7 @@ sealed class ManagedCryptoCurrency { private companion object { const val MAIN_NETWORK_TYPE_NAME = "MAIN" + const val MAIN_NETWORK_L2_TYPE_NAME = "MAIN L2" } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt index 09be6a8976..c9ec3f67cb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt @@ -31,7 +31,7 @@ internal class BlockchainRowUMConverter( return BlockchainRowUM( id = network.networkId, name = blockchainInfo.name, - type = if (isMainNetwork) "MAIN" else blockchainInfo.protocolName, + type = getNetworkType(network, blockchainInfo), iconResId = if (isEnabled) { if (isSelected) { getActiveIconRes(blockchainInfo.blockchainId) @@ -46,4 +46,21 @@ internal class BlockchainRowUMConverter( isEnabled = isEnabled, ) } + + private fun getNetworkType( + network: TokenMarketInfo.Network, + blockchainInfo: BlockchainUtils.BlockchainInfo, + ): String { + val isMainNetwork = network.contractAddress == null + return when { + BlockchainUtils.isL2Network(networkId = network.networkId) -> MAIN_NETWORK_L2_TYPE_NAME + isMainNetwork -> MAIN_NETWORK_TYPE_NAME + else -> blockchainInfo.protocolName + } + } + + private companion object { + const val MAIN_NETWORK_TYPE_NAME = "MAIN" + const val MAIN_NETWORK_L2_TYPE_NAME = "MAIN L2" + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 74c59e4344..aa6d57aaf2 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-765" +tangemBlockchainSdk = "develop-769" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-381" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt index 1248e3dedd..963d14bf12 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -internal val l2BlockchainsList = listOf( +val l2BlockchainsList = listOf( Blockchain.Optimism, Blockchain.Arbitrum, Blockchain.ZkSyncEra, diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index ada8b81979..4634daf76c 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -2,6 +2,7 @@ package com.tangem.lib.crypto import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.l2BlockchainsList import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter @@ -98,6 +99,11 @@ object BlockchainUtils { ) } + fun isL2Network(networkId: String): Boolean { + val blockchain = Blockchain.fromNetworkId(networkId) ?: return false + return l2BlockchainsList.contains(blockchain) + } + private fun getNetworkStandardName(blockchain: Blockchain): String { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20" From 06cf8cad513e947427b39898dd4fbf30672244d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Sep 2024 20:52:37 +0300 Subject: [PATCH 175/197] Updated on 2026-08-14 --- .../presentation/state/converters/YieldBalancesConverter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 d948580b77..ae45ca8d1f 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 @@ -50,9 +50,11 @@ internal class YieldBalancesConverter( } } - private fun List.mapBalances() = sortedBy { it.type.order } + private fun List.mapBalances() = asSequence() .filterNot { it.amount.isZero() || it.type == BalanceType.REWARDS } .mapNotNull(balanceItemConverter::convert) + .sortedByDescending { it.cryptoDecimal } + .sortedBy { it.type.order } .toPersistentList() private fun getRewardBlockType(): RewardBlockType { From 06d139362eabdf251bb9dbf721f70da85cb6991b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 12:28:51 +0300 Subject: [PATCH 176/197] Updated on 2026-08-14 --- .../com/tangem/core/ui/utils/BigDecimalFormatter.kt | 13 ++++++++++++- .../SetConfirmationStateLoadingTransformer.kt | 1 + .../state/transformers/SetTitleTransformer.kt | 13 +++++++------ .../presentation/ui/StakingConfirmationContent.kt | 4 ++-- .../impl/presentation/ui/block/StakingFeeBlock.kt | 1 + 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index fc916ea18a..16386f4fa3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -4,6 +4,7 @@ import android.icu.text.CompactDecimalFormat import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.LOWER_SIGN +import com.tangem.utils.StringsSigns.TILDE_SIGN import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -167,6 +168,7 @@ object BigDecimalFormatter { fiatCurrencySymbol: String, decimals: Int = FIAT_MARKET_DEFAULT_DIGITS, locale: Locale = Locale.getDefault(), + withApproximateSign: Boolean = false, ): String { if (fiatAmount == null) return EMPTY_BALANCE_SIGN @@ -187,8 +189,17 @@ object BigDecimalFormatter { ) } } else { - formatter.format(fiatAmount) + val formattedAmount = formatter.format(fiatAmount) .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + + if (withApproximateSign) { + buildString { + append(TILDE_SIGN) + append(formattedAmount) + } + } else { + formattedAmount + } } } 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 0428216d5e..076c5577a7 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 @@ -64,6 +64,7 @@ internal class SetConfirmationStateLoadingTransformer( fiatAmount = potentialReward, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, + withApproximateSign = true, ) return if (isEnterAction && amountDecimal != null && potentialReward != null) { resourceReference( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index d296d29596..578af70403 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -41,19 +41,20 @@ internal object SetTitleTransformer : Transformer { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data val title = confirmationState?.pendingAction?.type?.getPendingActionTitle() - title?.takeIf { it.isNullOrEmpty() } + title.takeIf { !it.isNullOrEmpty() } ?: resourceReference( - R.string.staking_title_stake, - wrappedList(prevState.cryptoCurrencyName), + id = R.string.staking_title_stake, + formatArgs = wrappedList(prevState.cryptoCurrencyName), ) } } } } - val subtitle = when (currentStep) { - StakingStep.Confirmation -> stringReference(prevState.walletName) - else -> null + val subtitle = if (currentStep == StakingStep.Confirmation && actionType == StakingActionCommonType.ENTER) { + stringReference(prevState.walletName) + } else { + null } return prevState.copy( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index df10d96663..8fc799719a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -59,8 +59,8 @@ internal fun StakingConfirmationContent( } AmountBlock( amountState = amountState, - isClickDisabled = !isEnterAction, - isEditingDisabled = !isEnterAction || isTransactionSent, + isClickDisabled = !isEnterAction || isTransactionSent, + isEditingDisabled = !isEnterAction && state.innerState != InnerConfirmationStakingState.COMPLETED, onClick = clickIntents::onPrevClick, ) if (isEnterAction) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index cfb15b93e0..1a001e874d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -83,6 +83,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, paddingValues = PaddingValues(), + showDivider = false, ) FeeLoading(feeState) } From f18338ff443794f6ce73086d1458bf2c6df850f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 14:46:32 +0500 Subject: [PATCH 177/197] Updated on 2026-08-14 --- .../DefaultManageTokensRepository.kt | 16 ++++ .../CheckCurrencyUnsupportedUseCase.kt | 14 +++ .../repository/ManageTokensRepository.kt | 6 ++ features/markets/impl/build.gradle.kts | 1 + .../impl/DefaultMarketsEntryComponent.kt | 7 ++ .../model/AddToPortfolioBSContentUMFactory.kt | 3 +- .../impl/model/MarketsPortfolioModel.kt | 93 ++++++++++++++++++- .../impl/model/SelectNetworkUMConverter.kt | 5 +- 8 files changed, 139 insertions(+), 6 deletions(-) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index f1749f04d4..49b58cb102 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.managetokens import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.blockchainsdk.utils.toNetworkId @@ -193,6 +194,21 @@ internal class DefaultManageTokensRepository( } } + override suspend fun checkCurrencyUnsupportedState( + userWalletId: UserWalletId, + rawNetworkId: String, + isMainNetwork: Boolean, + ): CurrencyUnsupportedState? { + val userWallet = getUserWallet(userWalletId = userWalletId) + val blockchain = Blockchain.fromNetworkId(networkId = rawNetworkId) + ?: error("Can not create blockchain with given networkId -> $rawNetworkId") + return if (isMainNetwork) { + checkBlockchainUnsupportedState(userWallet, blockchain) + } else { + checkTokenUnsupportedState(userWallet, blockchain) + } + } + private fun checkBlockchainUnsupportedState( userWallet: UserWallet, blockchain: Blockchain, diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt index 40dbddf6fd..eef9093442 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckCurrencyUnsupportedUseCase.kt @@ -18,4 +18,18 @@ class CheckCurrencyUnsupportedUseCase( repository.checkCurrencyUnsupportedState(userWalletId, sourceNetwork) } } + + suspend operator fun invoke( + userWalletId: UserWalletId, + networkId: String, + isMainNetwork: Boolean, + ): Either { + return Either.catch { + repository.checkCurrencyUnsupportedState( + userWalletId = userWalletId, + rawNetworkId = networkId, + isMainNetwork = isMainNetwork, + ) + } + } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt index cfa44069fe..0d879955e8 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt @@ -22,4 +22,10 @@ interface ManageTokensRepository { userWalletId: UserWalletId, sourceNetwork: ManagedCryptoCurrency.SourceNetwork, ): CurrencyUnsupportedState? + + suspend fun checkCurrencyUnsupportedState( + userWalletId: UserWalletId, + rawNetworkId: String, + isMainNetwork: Boolean, + ): CurrencyUnsupportedState? } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 3ac39315a8..055d5172a9 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) + implementation(projects.domain.manageTokens) implementation(projects.domain.markets) implementation(projects.domain.staking.models) implementation(projects.domain.tokens) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index 7f96268b4c..ad932e8965 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -10,6 +10,8 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.message.EventMessageEffect import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.entry.BottomSheetState @@ -26,6 +28,7 @@ import dagger.assisted.AssistedInject internal class DefaultMarketsEntryComponent @AssistedInject constructor( @Assisted context: AppComponentContext, private val marketsEntryChildFactory: MarketsEntryChildFactory, + private val uiDependencies: UiDependencies, ) : MarketsEntryComponent, AppComponentContext by context { private val stackNavigation = StackNavigation() @@ -61,6 +64,10 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( stackState = stack.subscribeAsState(), modifier = modifier, ) + EventMessageEffect( + messageHandler = uiDependencies.eventMessageHandler, + snackbarHostState = uiDependencies.globalSnackbarHostState, + ) } @OptIn(ExperimentalDecomposeApi::class) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index c1b58bdbf6..3ce401a818 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.wallets.models.UserWallet @@ -28,7 +29,7 @@ internal class AddToPortfolioBSContentUMFactory( private val token: TokenMarketParams, private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, - private val onNetworkSwitchClick: (String, Boolean) -> Unit, + private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, private val onWalletSelect: (UserWalletId) -> Unit, private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set) -> Unit, ) { 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 d3291921c8..59a9e16805 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -1,18 +1,32 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable +import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.resolveReference +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.message.ContentMessage +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.HasMissedDerivationsUseCase +import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase +import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM @@ -31,6 +45,8 @@ internal class MarketsPortfolioModel @Inject constructor( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, tokenActionsIntentsFactory: TokenActionsHandler.Factory, override val dispatchers: CoroutineDispatcherProvider, + private val messageSender: UiMessageSender, + private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, @@ -150,7 +166,7 @@ internal class MarketsPortfolioModel @Inject constructor( } } - private fun onNetworkSwitchClick(networkId: String, isChecked: Boolean) { + private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) { val selectedWalletId = selectedMultiWalletIdFlow.value if (selectedWalletId == null) { @@ -159,12 +175,83 @@ internal class MarketsPortfolioModel @Inject constructor( } if (isChecked) { - addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = networkId) + modelScope.launch { + val unsupportedState = checkCurrencyUnsupportedState( + userWalletId = selectedWalletId, + rawNetworkId = blockchainRowUM.id, + isMainNetwork = blockchainRowUM.isMainNetwork, + ) + if (unsupportedState != null) { + showUnsupportedWarning(unsupportedState) + } else { + addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) + } + } } else { - addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = networkId) + addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) } } + private suspend fun checkCurrencyUnsupportedState( + userWalletId: UserWalletId, + rawNetworkId: String, + isMainNetwork: Boolean, + ): CurrencyUnsupportedState? { + return checkCurrencyUnsupportedUseCase( + userWalletId = userWalletId, + networkId = rawNetworkId, + isMainNetwork = isMainNetwork, + ).getOrElse { + Timber.e( + it, + """ + Failed to check currency unsupported state + |- User wallet ID: $userWalletId + |- Network ID: $rawNetworkId + |- Is main network: $isMainNetwork + """.trimIndent(), + ) + + val message = SnackbarMessage( + message = it.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + null + } + } + + private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { + val message = ContentMessage { onDismiss -> + BasicDialog( + title = resourceReference(R.string.common_warning).resolveReference(), + message = when (unsupportedState) { + is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( + id = R.string.alert_manage_tokens_unsupported_curve_message, + formatArgs = wrappedList(unsupportedState.networkName), + ) + }.resolveReference(), + confirmButton = DialogButtonUM( + title = stringResource(R.string.common_ok), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) + } + + messageSender.send(message) + } + private fun onWalletSelect(userWalletId: UserWalletId) { selectedMultiWalletIdFlow.update { prevUserWalletId -> prevUserWalletId?.let(addToPortfolioManager::removeAllChanges) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt index d7d84b6bcb..05b33043d7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.impl.model +import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM @@ -18,7 +19,7 @@ import kotlinx.collections.immutable.toImmutableList internal class SelectNetworkUMConverter( private val networksWithToggle: Map, private val alreadyAddedNetworks: Set, - private val onNetworkSwitchClick: (String, Boolean) -> Unit, + private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, ) : Converter { override fun convert(value: TokenMarketParams): SelectNetworkUM { @@ -30,7 +31,7 @@ internal class SelectNetworkUMConverter( networks = BlockchainRowUMConverter(alreadyAddedNetworks) .convertList(networksWithToggle.toList()) .toImmutableList(), - onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um.id, isChecked) }, + onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) }, ) } } \ No newline at end of file From 9f08cdbd072cdd055d29f2dbee603d4461fd2aa0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 11:58:13 +0300 Subject: [PATCH 178/197] Updated on 2026-08-14 --- .../firebase/FirebaseAppInstanceIdProvider.kt | 7 +++- .../preview/PreviewDetailsComponent.kt | 5 +-- .../features/details/model/DetailsModel.kt | 21 ++++++++++ .../features/details/utils/ItemsBuilder.kt | 41 +++++++------------ 4 files changed, 42 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt index 8d33e1522e..5a8a85c072 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt @@ -19,6 +19,11 @@ internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider { } override fun getAppInstanceIdSync(): String? { - return Firebase.analytics.appInstanceId.result + return try { + Firebase.analytics.appInstanceId.result + } catch (e: IllegalStateException) { + Timber.e(e, "getAppInstanceIdSync") + null + } } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 1a15b85fc5..9a4ec03248 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -2,7 +2,6 @@ package com.tangem.features.details.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.tangem.core.analytics.DummyAppInstanceIdProvider import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.features.details.component.DetailsComponent @@ -18,9 +17,7 @@ internal class PreviewDetailsComponent : DetailsComponent { private val previewBlocks = runBlocking { ItemsBuilder( router = DummyRouter(), - urlOpener = DummyUrlOpener(), - appInstanceIdProvider = DummyAppInstanceIdProvider(), - ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}) + ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}, onBuyClick = {}) } private val previewFooter = DetailsFooterUM( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index fd592c79b8..884ee4f78a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -1,10 +1,12 @@ package com.tangem.features.details.model import arrow.core.getOrElse +import com.tangem.core.analytics.AppInstanceIdProvider 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.navigation.url.UrlOpener import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase @@ -35,6 +37,8 @@ internal class DetailsModel @Inject constructor( private val appVersionProvider: AppVersionProvider, private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val router: Router, + private val urlOpener: UrlOpener, + private val appInstanceIdProvider: AppInstanceIdProvider, paramsContainer: ParamsContainer, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val appStateHolder: ReduxStateHolder, @@ -74,6 +78,7 @@ internal class DetailsModel @Inject constructor( items.value = itemsBuilder.buildAll( isWalletConnectAvailable = isWalletConnectAvailable, onSupportClick = ::sendFeedback, + onBuyClick = ::onBuyClick, ) } @@ -86,6 +91,12 @@ internal class DetailsModel @Inject constructor( } } + private fun onBuyClick() { + modelScope.launch { + urlOpener.openUrl(buildBuyLink()) + } + } + private fun updateState(items: ImmutableList) { state.update { prevState -> prevState.copy(items = items) @@ -93,4 +104,14 @@ internal class DetailsModel @Inject constructor( } private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})" + + private suspend fun buildBuyLink(): String { + return appInstanceIdProvider.getAppInstanceId()?.let { + "$BUY_TANGEM_URL&app_instance_id=$it" + } ?: BUY_TANGEM_URL + } + + private companion object { + const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 2f1093804f..70d3936e99 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -1,10 +1,8 @@ package com.tangem.features.details.utils import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.AppInstanceIdProvider import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -17,20 +15,19 @@ import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @ComponentScoped -internal class ItemsBuilder @Inject constructor( - private val router: Router, - private val urlOpener: UrlOpener, - private val appInstanceIdProvider: AppInstanceIdProvider, -) { +internal class ItemsBuilder @Inject constructor(private val router: Router) { - fun buildAll(isWalletConnectAvailable: Boolean, onSupportClick: () -> Unit): ImmutableList = - buildList { - buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) - buildUserWalletListBlock().let(::add) - buildShopBlock().let(::add) - buildSettingsBlock().let(::add) - buildSupportBlock(onSupportClick).let(::add) - }.toImmutableList() + fun buildAll( + isWalletConnectAvailable: Boolean, + onSupportClick: () -> Unit, + onBuyClick: () -> Unit, + ): ImmutableList = buildList { + buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) + buildUserWalletListBlock().let(::add) + buildShopBlock(onBuyClick).let(::add) + buildSettingsBlock().let(::add) + buildSupportBlock(onSupportClick).let(::add) + }.toImmutableList() private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { return if (isWalletConnectAvailable) { @@ -44,7 +41,7 @@ internal class ItemsBuilder @Inject constructor( private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.UserWalletList - private fun buildShopBlock(): DetailsItemUM = DetailsItemUM.Basic( + private fun buildShopBlock(onBuyClick: () -> Unit): DetailsItemUM = DetailsItemUM.Basic( id = "shop", items = persistentListOf( DetailsItemUM.Basic.Item( @@ -52,7 +49,7 @@ internal class ItemsBuilder @Inject constructor( block = BlockUM( text = resourceReference(R.string.details_buy_wallet), iconRes = R.drawable.ic_tangem_24, - onClick = { urlOpener.openUrl(buildBuyLink()) }, + onClick = onBuyClick, ), ), ), @@ -104,14 +101,4 @@ internal class ItemsBuilder @Inject constructor( ), ), ) - - private fun buildBuyLink(): String { - return appInstanceIdProvider.getAppInstanceIdSync()?.let { - "$BUY_TANGEM_URL&app_instance_id=$it" - } ?: BUY_TANGEM_URL - } - - private companion object { - const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" - } } \ No newline at end of file From 4196caea3481ab4538307d9c99af8e29eb8a8719 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Sep 2024 17:48:29 +0500 Subject: [PATCH 179/197] Updated on 2026-08-14 --- .../com/tangem/data/staking/DefaultStakingRepository.kt | 6 +++--- .../staking/impl/presentation/state/StakingNotification.kt | 4 +++- .../transformers/AddStakingNotificationsTransformer.kt | 6 ++++++ .../src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt | 5 +++++ 4 files changed, 17 insertions(+), 4 deletions(-) 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 f1c4732032..6d22478790 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 @@ -608,11 +608,11 @@ internal class DefaultStakingRepository( val integrationIdMap = mapOf( 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.Tron.run { id + toCoinId() } to TRON_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.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, // Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID, 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 65968aeafa..c989a34d8c 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,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state +import androidx.annotation.StringRes import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.* @@ -69,10 +70,11 @@ internal object StakingNotification { data class Unstake( val cooldownPeriodDays: Int, + @StringRes val subtitleRes: Int, ) : StakingNotification.Info( title = resourceReference(R.string.common_unstake), subtitle = resourceReference( - R.string.staking_notification_unstake_text, + subtitleRes, wrappedList( pluralReference( id = R.plurals.common_days, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 7132cb9280..d9f2c9725f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -32,6 +32,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.BlockchainUtils.isCosmos import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero @@ -216,6 +217,11 @@ internal class AddStakingNotificationsTransformer( add( StakingNotification.Info.Unstake( cooldownPeriodDays = yield.metadata.cooldownPeriod.days, + subtitleRes = if (isCosmos(cryptoCurrencyStatusProvider().currency.network.id.value)) { + R.string.staking_notification_unstake_cosmos_text + } else { + R.string.staking_notification_unstake_text + }, ), ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 4634daf76c..1c340dabc3 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -83,6 +83,11 @@ object BlockchainUtils { return blockchain == Blockchain.Polkadot || blockchain == Blockchain.PolkadotTestnet } + fun isCosmos(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Cosmos || blockchain == Blockchain.CosmosTestnet + } + data class BlockchainInfo( val blockchainId: String, val name: String, From 82d1a7cfd906123136debff33b116d03df1834d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 13:18:57 +0500 Subject: [PATCH 180/197] Updated on 2026-08-14 --- .../com/tangem/features/details/ui/UserWalletListBlock.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 98383d104d..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 @@ -54,11 +54,7 @@ private fun AddWalletButton( Row( modifier = Modifier .fillMaxWidth() - .padding( - start = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), + .padding(all = TangemTheme.dimens.spacing12), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { From bcdfbe03caa7caf289d371fcc22a71a66f509fa8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 14:44:36 +0300 Subject: [PATCH 181/197] Updated on 2026-08-14 --- .../kotlin/com/tangem/plugin/configuration/model/BuildType.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 53a1c18637..27271c7cea 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -69,7 +69,7 @@ internal enum class BuildType( appIdSuffix = "internal", versionSuffix = "internal", configFields = listOf( - BuildConfigField.Environment(value = "dev"), + BuildConfigField.Environment(value = "prod"), BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), From 7e3556f0df1fe38ddd43a42903035e7b8b586ec5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 18:33:28 +0300 Subject: [PATCH 182/197] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 4 ++-- ...tUseCase.kt => GetNodlTokenListUseCase.kt} | 12 ++++++++-- .../CurrenciesStatusesOperations.kt | 22 ++++++++----------- .../SingleWalletWithTokenContentLoader.kt | 6 ++--- ...ngleWalletWithTokenContentLoaderFactory.kt | 6 ++--- .../SingleWalletWithTokenListSubscriber.kt | 8 +++---- 6 files changed, 31 insertions(+), 27 deletions(-) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCardTokensListUseCase.kt => GetNodlTokenListUseCase.kt} (88%) 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 63b8864a62..343d1e9a8e 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 @@ -75,8 +75,8 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, - ): GetCardTokensListUseCase { - return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository) + ): GetNodlTokenListUseCase { + return GetNodlTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository) } @Provides diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt similarity index 88% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt index 698c3be4f5..ae25bf7114 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt @@ -18,7 +18,15 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest -class GetCardTokensListUseCase( +/** + * Use case for getting a list of tokens for NODL card + * + * @property currenciesRepository currencies repository + * @property quotesRepository quotes repository + * @property networksRepository networks repository + * @property stakingRepository staking repository + */ +class GetNodlTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, @@ -48,7 +56,7 @@ class GetCardTokensListUseCase( stakingRepository = stakingRepository, ) - return operations.getCardCurrenciesStatusesFlow() + return operations.getNodlCurrencyStatusesFlow() .map { maybeCurrenciesStatuses -> maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) } 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 1a9304db56..a5cf2318cd 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 @@ -115,7 +115,8 @@ internal class CurrenciesStatusesOperations( return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances) } - fun getCardCurrenciesStatusesFlow(): Flow>> { + /** Get flow of currency statuses for NODL card */ + fun getNodlCurrencyStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( block = { getCurrenciesFromCard(userWalletId) }, @@ -146,9 +147,13 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - getYieldBalances(nonEmptyCurrencies), - ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> - createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances) + ) { maybeQuotes, maybeNetworksStatuses -> + createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworksStatuses, + maybeYieldBalances = null, + ) } emitAll(currenciesFlow) @@ -388,15 +393,6 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyNetworksStatuses.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( cryptoCurrencies: List, ): Either { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 655c6255a3..f3829783da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.GetNodlTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -23,7 +23,7 @@ internal class SingleWalletWithTokenContentLoader( private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getNodlTokenListUseCase: GetNodlTokenListUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -36,7 +36,7 @@ internal class SingleWalletWithTokenContentLoader( clickIntents = clickIntents, tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, - getCardTokensListUseCase = getCardTokensListUseCase, + getNodlTokenListUseCase = getNodlTokenListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index e00b6fe74c..5d820846c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.GetNodlTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -19,7 +19,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getNodlTokenListUseCase: GetNodlTokenListUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, @@ -33,7 +33,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - getCardTokensListUseCase = getCardTokensListUseCase, + getNodlTokenListUseCase = getNodlTokenListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index 886cc1495e..c2c561afd8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -3,10 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.toLce -import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.GetNodlTokenListUseCase +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.map @Suppress("LongParameterList") internal class SingleWalletWithTokenListSubscriber( private val userWallet: UserWallet, - private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getNodlTokenListUseCase: GetNodlTokenListUseCase, stateHolder: WalletStateController, clickIntents: WalletClickIntents, tokenListAnalyticsSender: TokenListAnalyticsSender, @@ -34,6 +34,6 @@ internal class SingleWalletWithTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(): LceFlow = getCardTokensListUseCase(userWallet.walletId) + override fun tokenListFlow(): LceFlow = getNodlTokenListUseCase(userWallet.walletId) .map { it.toLce() } } \ No newline at end of file From 46b69881f96b6b9d1fd61359c6ed5868d8cc164d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 18:42:30 +0300 Subject: [PATCH 183/197] Updated on 2026-08-14 --- .../converters/TokenMarketInfoConverter.kt | 21 ++++++++++++------- .../impl/model/converters/MetricsConverter.kt | 6 +++--- .../converters/PricePerformanceConverter.kt | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index d77d7618fe..1aa7d1e9af 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenQuotes import com.tangem.utils.converter.Converter +import java.math.BigDecimal internal object TokenMarketInfoConverter : Converter { @@ -28,16 +29,22 @@ internal object TokenMarketInfoConverter : Converter BigDecimal?, + ): BigDecimal? { + return (intervalProvider() ?: allTime)?.movePointLeft(2) + } + @JvmName("convertNetwork") private fun List.convert(): List { return map { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt index c6c57682f7..5cd11e6cd3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -112,10 +112,10 @@ internal class MetricsConverter( } private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { + if (this == null) return StringsSigns.DASH_SIGN + return if (crypto) { - BigDecimalFormatter.formatCompactAmount( - amount = this ?: BigDecimal.ZERO, - ) + BigDecimalFormatter.formatCompactAmount(amount = this) } else { val currency = appCurrency() BigDecimalFormatter.formatCompactFiatAmount( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt index 38ee8682bf..29b04a9541 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt @@ -24,7 +24,7 @@ internal class PricePerformanceConverter( } private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value { - if (this == null) { + if (this == null || this.low == null || this.high == null) { return PricePerformanceUM.Value( low = StringsSigns.DASH_SIGN, high = StringsSigns.DASH_SIGN, From a8185c24985bb0461618cf1598ba9ff75c7bf0c0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Sep 2024 20:56:48 +0500 Subject: [PATCH 184/197] Updated on 2026-08-14 --- .../field/AmountFieldChangeTransformer.kt | 4 +- .../converters/field/AmountFieldConverter.kt | 4 +- .../amountScreen/models/AmountFieldModel.kt | 1 + .../preview/AmountStatePreviewData.kt | 1 + .../common/ui/amountScreen/ui/AmountField.kt | 21 +++- core/res/src/main/res/values-de/strings.xml | 25 ++-- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 13 +++ core/res/src/main/res/values-ru/strings.xml | 4 + .../src/main/res/values-uk-rUA/strings.xml | 15 ++- core/res/src/main/res/values/strings.xml | 8 +- .../amount/AmountChangeStateTransformer.kt | 14 +-- .../amount/AmountMaxValueStateTransformer.kt | 17 +-- .../AmountRequirementStateTransformer.kt | 109 +++++++++++++----- .../amount/AmountRoundToIntegerTransformer.kt | 40 +++++++ .../state/utils/FeeCalculation.kt | 13 ++- .../viewmodel/StakingViewModel.kt | 12 +- 17 files changed, 228 insertions(+), 75 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt 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 528d288023..8de99b1fc5 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 @@ -9,6 +9,7 @@ import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getCryptoValue import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -62,7 +63,8 @@ class AmountFieldChangeTransformer( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, - error = resourceReference(R.string.send_validation_amount_exceeds_balance), + error = resourceReference(R.string.send_validation_amount_exceeds_balance).takeIf { isExceedBalance } + ?: TextReference.EMPTY, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index bfe5259127..d460c0ca5d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.text.KeyboardActions 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.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference @@ -62,7 +61,8 @@ class AmountFieldConverter( cryptoAmount = cryptoAmount, fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), isError = false, - error = TextReference.Res(R.string.send_validation_amount_exceeds_balance), + isWarning = false, + error = TextReference.EMPTY, isFiatUnavailable = fiatRate == null, isValuePasted = false, onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt index 572239504d..995852df94 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt @@ -35,5 +35,6 @@ data class AmountFieldModel( val isValuePasted: Boolean, val onValuePastedTriggerDismiss: () -> Unit, val isError: Boolean, + val isWarning: Boolean, val error: TextReference, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 91951b4220..a622f752c9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -53,6 +53,7 @@ object AmountStatePreviewData { fiatValue = "123.123", isFiatUnavailable = false, isError = false, + isWarning = false, error = TextReference.EMPTY, isValuePasted = false, onValuePastedTriggerDismiss = {}, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index f190cea1db..d8347d67af 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -112,6 +115,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri ) AmountFieldError( isError = amountField.isError, + isWarning = amountField.isWarning, error = amountField.error, modifier = Modifier .align(BottomCenter) @@ -124,17 +128,24 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri } @Composable -private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { +private fun AmountFieldError( + isError: Boolean, + isWarning: Boolean, + error: TextReference, + modifier: Modifier = Modifier, +) { AnimatedVisibility( - visible = isError, + visible = isError || isWarning, enter = fadeIn(), exit = fadeOut(), modifier = modifier, ) { + val errorText = remember(this) { error } + val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention Text( - text = error.resolveReference(), + text = errorText.resolveReference(), style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.warning, + color = color, textAlign = TextAlign.Center, ) } diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index eee487a5c1..ef2d858a71 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -297,6 +297,7 @@ Unbegrenzt Karte bestellen Karte scannen + Diese Information ist KI generiert Um den Zugangscode zu ändern, halte die Karte wie oben gezeigt an das Gerät und entferne sie erst am Ende des Vorgangs. Um den Passcode zu ändern, halte die Karte wie oben gezeigt an das Gerät und entferne sie erst am Ende des Vorgangs. Um die Wallet zu erstellen, halte die Karte wie oben gezeigt an das Gerät und entferne sie erst am Ende des Vorgangs. @@ -350,6 +351,7 @@ Mein Portfolio Markt Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen + Um Token hinzuzufügen, rufe dies auf oder tippe auf die Suchleiste Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s Die Daten konnten nicht geladen werden... Keine Daten @@ -388,7 +390,6 @@ Vollständig verwässerte Bewertung Der theoretische Gesamtwert einer Kryptowährung, wenn alle Coins, die existieren könnten, im Umlauf sind, einschließlich derjenigen, die derzeit nicht im Umlauf sind Entstehungsdatum - Leer Hoch Inhaber/ Halter Die Änderung der Anzahl der Token-Inhaber innerhalb eines bestimmten Zeitraums @@ -399,8 +400,8 @@ Liquiditätsindex Leer Niedrig - Marktkapitalisierung - Das MCap eines Assets, errechnet sich durch Multiplikation des aktuellen Preises mit der Gesamtzahl der im Umlauf befindlichen Coins + MarketCap + Der Gesamtmarktwert einer Kryptowährung, berechnet durch Multiplikation des aktuellen Preises des Coins mit der Gesamtzahl der im Umlauf befindlichen Coins. Marktbewertung Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung Maximale Versorgung @@ -410,7 +411,6 @@ Preisleistung Aufbewahrungsort Sicherheitsbewertung - Leer Soziales Gesamtangebot Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können @@ -462,12 +462,12 @@ Backups anlegen Lese mehr über die Seed-Phrase - leer + Schreibe diese %d-Wörter in der unten angegebenen Reihenfolge auf und bewahre sie an einem sicheren und geheimen Ort auf. Deine Seed-Phrase - leer + %d Wörter Um deine Wallets zu importieren, gib bitte deine Seed-Phrase in das folgende Feld ein @@ -637,8 +637,8 @@ Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s Du sendest **%1$s** und %2$s Du sendest **%1$s** - Die Netzwerkgebühr wird durch die Nutzung von %1$s %2$s gedeckt. - Die Netzwerkgebühr wird durch das Ausgeben von %1$s %2$s reduziert. + Die Netzwerkgebühr wird durch die Nutzung von %1$s Energieträgern gedeckt. + Die Netzwerkgebühr wird durch das Ausgeben von %1$s Energieträgern reduziert. inklusive einer Netzgebühr von %1$s Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. @@ -648,6 +648,7 @@ Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name Die Anzahl der zu stakenden Krypros muss mindesten %s betragen + Der Stakingbetrag wird aufgrund der Netzwerkregeln auf %1$s TRX aufgerundet. Nicht gestakte beanspruche Jährliche prozentuale Rendite Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. @@ -675,17 +676,23 @@ Gesperrt Migrieren Natives Staking - Verdiente Prämien werden direkt an Deine Adresse gesendet. Sobald Du diese eingefordert hast, stehen Dir die Mittel zur Verfügung. + Verdiente Belohnungen werden an deine Wallet gesendet und stehen sofort zur Verwendung zur Verfügung + Sicher staken und tägliche Belohnungen verdienen. + Sicher staken und stündliche Belohnungen verdienen. + Sicher staken und monatliche Belohnungen verdienen. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat. Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. + Sicher staken und wöchentliche Belohnungen verdienen. Verdiene Staking-Belohnungen Beim Staking im %1$s -Netzwerk mit einem neuen Validator werden alle zuvor eingesetzten Kryptos automatisch an diesen Validator übertragen Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. + Entsperre dein Geld, um es aus dem Staking-Prozess abzuheben. Das Freischalten nimmt %s. Nach Ablauf der 21-tägigen Bindungsfrist kannst Du über Dein Guthaben verfügen. Die Prämie wird zusammen mit dem ungestaketen Guthaben abgehoben. Deine Assets steht Dir nach Ablauf der Frist für die Aufhebung der Bindung %s zur Verfügung. Du kannst Dein Guthaben jetzt abheben. Es steht Dir sofort zur Verfügung. + Wenn du im Tron-Netzwerk mit einem neuen Validator stakest, werden alle zuvor eingesetzten Assets automatisch an diesen Validator übertragen. Vorbereitung Bereit zum Abheben Erneut binden diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index ca8c7cf4f2..be874b7bdb 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -363,7 +363,7 @@ 24 heures 3 mois 6 mois - 7 jours + 7j Tout Acheteurs expérimentés Évaluation diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 3d5a19d86e..9c10dc67c2 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -295,6 +295,7 @@ 無制限 カードを注文 スキャン + この情報はAIによって生成されました アクセスコードを変更するには、上図のようにカードをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 ウォレットを作成するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -624,6 +625,10 @@ %sを送金する **%1$s** を送金する (ネットワーク手数料%2$sを含む) **%1$s** と %2$s を送金しています。 + ** %1$s ** を送信しています + ネットワーク手数料は%1$sエネルギーを使用してカバーされます + %1$sを使用するとネットワーク手数料が減額されます + ネットワーク手数料%1$sを含む 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 取引が送信されました @@ -660,14 +665,22 @@ 移行 ネイティブステーキング 獲得した報酬はあなたのアドレスに直接送られ、すぐ使用可能です。 + 安全にステーキングして、報酬を毎日獲得しましょう + 安全にステーキングして、報酬を毎時間獲得しましょう + 安全にステーキングして、報酬を毎月獲得しましょう ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎時間受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 + 安全にステーキングして、報酬を毎週獲得しましょう ステーキング報酬を獲得 + 新しいバリデーターで%1$sネットワークにステーキングすると、以前にステーキングされた資金はすべてこのバリデーターに自動的に転送されます。 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 + 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 + 資金は、21日間のロック解除期間後に使用可能になります。報酬は、ロック解除後の資金とともに引き出されます。 %sのステーキング解除期間後、資金はすぐ利用可能となります。 資金を引き出すことができ、すぐに使用できます + Tronネットワークに新しいバリデーターでステーキングすると、以前にステーキングされた資金はすべてこのバリデーターに自動的に転送されます。 準備中 引き出し準備完了 再結束 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 567496ce4d..ce509bfb41 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -177,6 +177,7 @@ Адрес контракта Адрес контракта некорректен Пожалуйста, выберите сеть + Токен уже существует Десятичное число должно быть действительным целым числом, до %d Своя деривация Например m/00\'/0000\'/0\'/0/0 @@ -642,6 +643,8 @@ Отправка %s Вы отправляете **%1$s**, включая комиссию сети %2$s Вы отправляете **%1$s** и %2$s + Вы отправляете **%1$s** + включая комиссию сети в размере %1$s Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. Транзакция отправлена @@ -682,6 +685,7 @@ Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый месяц. Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг + Разблокируйте свои средства, чтобы вывести их из стейкинга. Разблокировка займёт %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 f0517e2467..9ef0e3fb24 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -301,6 +301,7 @@ Необмежено Купити Сканувати + Ця інформація була створена за допомогою ШІ Щоб змінити код доступу, прикладіть картку, як показано вище, і не прибирайте її до закінчення операції Щоб змінити пароль, прикладіть картку, як показано вище, і не прибирайте її до закінчення операції Щоб створити гаманець, прикладіть картку, як показано вище, і не прибирайте її до завершення операції @@ -371,7 +372,7 @@ 24 год. 3 міс. 6 міс. - 7 днів + Увесь Досвідчені покупці За рейтингом @@ -648,6 +649,10 @@ Надіслати %s Ви надсилаєте **%1$s**, включно з комісію мережі %2$s Ви надсилаєте **%1$s** і %2$s + Ви надсилаєте **%1$s** + комісія мережі буде покрита, використовуючи %1$s енергію + комісія мережі буде зменшена, витрачаючи %1$s + включно з комісією мережі %1$s Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. Трансакцію надіслано @@ -684,14 +689,22 @@ Перемістити Нативний стейкінг Зароблені винагороди будуть надіслані на ваш гаманець і доступні для використання відразу + Стейкайте безпечно та почніть отримувати винагороди щоденно + Стейкайте безпечно та почніть отримувати винагороди щогодини + Стейкайте безпечно та почніть отримувати винагороди щомісяця Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щодня. Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються кожну годину. Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щомісяця. Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щотиждня. + Стейкайте безпечно та почніть отримувати винагороди щотижня Отримуйте винагороду за стейкінг + Стейкінг в мережі %1$s з новим валідатором автоматично переведе всі раніше застейкані кошти до цього валідатора Реінвестуйте зароблені винагороди у суму стейкінгу, щоб збільшити потенційний прибуток. + Розблокуйте свої кошти, щоб вивести їх зі стейкінгу. Розблокування займе %s. + Ваші кошти будуть доступні для використання після закінчення 21-денного періоду розблокування. Винагорода буде отримана разом з вашими незастейканими коштами. Ваші кошти будуть доступні після %s періоду розблокування. Тепер ви можете вивести свої кошти, вони будуть доступні для використання відразу + Стейкінг в мережі Tron з новим валідатором автоматично переведе всі раніше застейкані кошти до цього валідатора Підготовка Готова до виведення Зʼєднати ще раз diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 03805641a9..9582c22d5f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -175,7 +175,7 @@ Contract address Contract address is invalid Please select the network - This token has already been added to the list + This token has already been added to the list Token already exists Decimal must be a valid integer, up to %d Custom derivation @@ -351,6 +351,7 @@ My portfolio Market To generate addresses for selected networks, you must scan your Tangem Wallet card + To add tokens pull this up or tap the search bar This section’s data is sourced from the following networks: %s Unable to load the data… No data @@ -634,8 +635,8 @@ You are sending **%1$s** including a network fee of %2$s You are sending **%1$s** and %2$s You are sending **%1$s** - network fee will be covered by using %1$s %2$s - network fee will be reduced by spending %1$s %2$s + network fee will be covered by using %1$s energy + network fee will be reduced by spending %1$s energy including a network fee of %1$s Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. @@ -645,6 +646,7 @@ This will remove the wallet from the application. The wallet itself can be added again. Name The amount to stake must be at least %s + Staking amount will be rounded to %1$s TRX due to network rules. Claim unstaked Annual percentage rate The annual percentage return you can earn from participating in staking. 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 098e959f7c..f91d878456 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 @@ -9,15 +9,9 @@ import com.tangem.utils.transformer.Transformer internal class AmountChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val value: String, - yield: Yield, + private val yield: Yield, ) : Transformer { - private val amountRequirementStateTransformer = AmountRequirementStateTransformer( - cryptoCurrencyStatus, - yield, - value, - ) - override fun transform(prevState: StakingUiState): StakingUiState { val updatedAmountState = AmountFieldChangeTransformer( cryptoCurrencyStatus, @@ -25,7 +19,11 @@ internal class AmountChangeStateTransformer( ).transform(prevState.amountState) return prevState.copy( - amountState = amountRequirementStateTransformer.transform(updatedAmountState), + amountState = AmountRequirementStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + yield = yield, + actionType = prevState.actionType, + ).transform(updatedAmountState), ) } } \ No newline at end of file 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 6314597d0d..088528794d 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 @@ -1,7 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState @@ -9,21 +8,17 @@ import com.tangem.utils.transformer.Transformer internal class AmountMaxValueStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - yield: Yield, + private val yield: Yield, ) : Transformer { - private val amountRequirementStateTransformer = AmountRequirementStateTransformer( - cryptoCurrencyStatus = cryptoCurrencyStatus, - yield = yield, - value = cryptoCurrencyStatus.value.amount - ?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals) - .orEmpty(), - ) - override fun transform(prevState: StakingUiState): StakingUiState { val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState) return prevState.copy( - amountState = amountRequirementStateTransformer.transform(updatedAmountState), + amountState = AmountRequirementStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + yield = yield, + actionType = prevState.actionType, + ).transform(updatedAmountState), ) } } \ No newline at end of file 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 59d4adfaf7..d669d5a7e9 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 @@ -3,60 +3,91 @@ 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.TextReference +import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R +import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal +import java.math.RoundingMode internal class AmountRequirementStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val yield: Yield, - private val value: String, + private val actionType: StakingActionCommonType, ) : Transformer { override fun transform(prevState: AmountState): AmountState { val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] - return if (prevState !is AmountState.Data || amountRequirements == null) { - prevState - } else { - updateWithError(prevState, amountRequirements) - } - } - - private fun updateWithError(prevState: AmountState.Data, amountRequirements: AddressArgument): AmountState { - val isRequirementError = isRequirementError(prevState, amountRequirements) - return if (isRequirementError) { - prevState.copy( - isPrimaryButtonEnabled = false, - amountTextField = prevState.amountTextField.copy( - isError = true, - error = resourceReference( - R.string.staking_amount_requirement_error, - wrappedList( - BigDecimalFormatter.formatCryptoAmount( - amountRequirements.minimum, - cryptoCurrencyStatus.currency.symbol, - cryptoCurrencyStatus.currency.decimals, - ), - ), - ), - keyboardOptions = prevState.amountTextField.keyboardOptions.copy( - imeAction = ImeAction.None, - ), - ), + return if (prevState is AmountState.Data && amountRequirements != null) { + updateWithError( + prevState, + actionType, + amountRequirements, ) } else { prevState } } + private fun updateWithError( + amountState: AmountState.Data, + actionType: StakingActionCommonType, + amountRequirements: AddressArgument, + ): AmountState { + val isRequirementError = isRequirementError(amountState, amountRequirements) + val isIntegerOnlyError = isIntegerOnlyError(amountState, actionType) + + val cryptoAmount = amountState.amountTextField.cryptoAmount + val roundedDownCrypto = cryptoAmount.value?.setScale(0, RoundingMode.DOWN) + val value = roundedDownCrypto?.parseBigDecimal(0).orEmpty() + + val errorText = when { + amountState.amountTextField.isError -> amountState.amountTextField.error + isRequirementError -> resourceReference( + R.string.staking_amount_requirement_error, + wrappedList( + BigDecimalFormatter.formatCryptoAmount( + amountRequirements.minimum, + cryptoCurrencyStatus.currency.symbol, + cryptoCurrencyStatus.currency.decimals, + ), + ), + ) + isIntegerOnlyError -> resourceReference( + R.string.staking_amount_tron_integer_error, + wrappedList(value), + ) + else -> TextReference.EMPTY + } + val isError = amountState.amountTextField.isError || isRequirementError + return if (!errorText.isNullOrEmpty()) { + amountState.copy( + isPrimaryButtonEnabled = !isError, + amountTextField = amountState.amountTextField.copy( + isError = isError, + isWarning = isIntegerOnlyError, + error = errorText, + keyboardOptions = amountState.amountTextField.keyboardOptions.copy( + imeAction = ImeAction.None, + ), + ), + ) + } else { + amountState + } + } + private fun isRequirementError(prevState: AmountState.Data, amountRequirements: AddressArgument): Boolean { - val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals) + val amountDecimal = prevState.amountTextField.cryptoAmount.value ?: return false val isAlreadyErrorState = prevState.amountTextField.isError val isAmountRequired = amountRequirements.required @@ -67,4 +98,20 @@ internal class AmountRequirementStateTransformer( return !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState } + + private fun isIntegerOnlyError(amountState: AmountState.Data, actionType: StakingActionCommonType): Boolean { + val cryptoAmountValue = amountState.amountTextField.cryptoAmount.value ?: return false + + val isEnter = actionType == StakingActionCommonType.ENTER + val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) + + val isIntegerOnly = cryptoAmountValue.isZero() || cryptoAmountValue.remainder(BigDecimal.ONE).isZero() + + return isEnter && isTron && !isIntegerOnly + } + + data class Data( + val amountState: AmountState, + val actionType: StakingActionCommonType, + ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt new file mode 100644 index 0000000000..d2b2e7fe50 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt @@ -0,0 +1,40 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import java.math.RoundingMode + +internal class AmountRoundToIntegerTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val amountState = prevState.amountState as? AmountState.Data ?: return prevState + val amountTextField = amountState.amountTextField + if (amountTextField.value.isEmpty()) return prevState + + val cryptoAmount = amountState.amountTextField.cryptoAmount + val fiatAmount = amountState.amountTextField.fiatAmount + val fiatDecimals = fiatAmount.decimals + + val roundedDownCrypto = cryptoAmount.value?.setScale(0, RoundingMode.DOWN) + val roundedDownFiat = roundedDownCrypto?.multiply(cryptoCurrencyStatus.value.fiatRate) + + val value = roundedDownCrypto?.parseBigDecimal(0).orEmpty() + val fiatValue = roundedDownFiat?.parseBigDecimal(fiatDecimals, RoundingMode.HALF_UP).orEmpty() + + return prevState.copy( + amountState = amountState.copy( + amountTextField = amountState.amountTextField.copy( + cryptoAmount = cryptoAmount.copy(value = roundedDownCrypto), + fiatAmount = fiatAmount.copy(value = roundedDownFiat), + value = value, + fiatValue = fiatValue, + isWarning = false, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt index 21e95ed465..0f1857c60b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt @@ -1,7 +1,10 @@ package com.tangem.features.staking.impl.presentation.state.utils import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.lib.crypto.BlockchainUtils.isTron import java.math.BigDecimal +import java.math.MathContext +import java.math.RoundingMode /** * Check and calculates subtracted amount @@ -14,15 +17,21 @@ internal fun checkAndCalculateSubtractedAmount( reduceAmountBy: BigDecimal, ): BigDecimal { val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) + val feeValueRounded = if (isTron) { + feeValue.round(MathContext(0, RoundingMode.UP)) + } else { + feeValue + } val isFeeCoverage = checkFeeCoverage( isSubtractAvailable = isAmountSubtractAvailable, balance = balance, amountValue = amountValue, - feeValue = feeValue, + feeValue = feeValueRounded, reduceAmountBy = reduceAmountBy, ) return if (isFeeCoverage) { - balance.minus(reduceAmountBy).minus(feeValue) + balance.minus(reduceAmountBy).minus(feeValueRounded) } else { amountValue } 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 966e5d3693..b283511af9 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 @@ -197,7 +197,17 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) onRefreshSwipe(isRefreshing = false) } - isAssentState() -> getFee(pendingAction) + isAssentState() -> { + getFee(pendingAction) + val amountState = value.amountState as? AmountState.Data + if (amountState?.amountTextField?.isWarning == true) { + stateController.update( + AmountRoundToIntegerTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + ), + ) + } + } } } From 6474fa0a8b9eab6799d64e265044ef3858ec729f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Sep 2024 21:40:10 +0500 Subject: [PATCH 185/197] Updated on 2026-08-14 --- .../analytics/utils/StakingAnalyticSender.kt | 42 +- .../impl/presentation/state/StakingUiState.kt | 1 + .../state/helpers/StakingBalanceUpdater.kt | 102 ++++ .../helpers/StakingFeeTransactionLoader.kt | 238 ++++++++ .../state/helpers/StakingTransactionSender.kt | 271 ++++++++++ .../ConfirmationStatePreviewData.kt | 1 + .../state/stub/StakingClickIntentsStub.kt | 11 +- .../AddStakingNotificationsTransformer.kt | 7 +- .../DismissBottomSheetStateTransformer.kt | 2 +- .../SetActionToExecuteTransformer.kt | 25 + .../SetConfirmationStateAssentTransformer.kt | 3 + ...etConfirmationStateCompletedTransformer.kt | 23 +- .../SetConfirmationStateLoadingTransformer.kt | 1 + .../SetInitialDataStateTransformer.kt | 1 + .../state/utils/StakingPendingActionUtils.kt | 9 + .../presentation/ui/block/StakingFeeBlock.kt | 19 +- .../viewmodel/StakingClickIntents.kt | 9 +- .../viewmodel/StakingViewModel.kt | 508 +++++------------- gradle/dependencies.toml | 2 +- 19 files changed, 842 insertions(+), 433 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 42dc192fbb..013bf2e49f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -42,13 +42,7 @@ internal class StakingAnalyticSender( StakingAnalyticsEvents.ConfirmationScreenOpened( token = value.cryptoCurrencySymbol, validator = validatorName, - action = when (value.actionType) { - StakingActionCommonType.ENTER -> StakingActionType.STAKE - StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> confirmationState.pendingAction?.type ?: StakingActionType.UNKNOWN - }, + action = getStakingActionType(value), ), ) } @@ -83,12 +77,24 @@ internal class StakingAnalyticSender( ) } - fun sendTransactionStakingAnalytics(cryptoCurrency: CryptoCurrency, validator: String) { + fun sendTransactionStakingAnalytics(value: StakingUiState) { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorName = validatorState?.chosenValidator?.name ?: return + + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonAction( + action = getStakingActionType(value).name, + token = value.cryptoCurrencyName, + validator = validatorName, + ), + ) + analyticsEventHandler.send( Basic.TransactionSent( sentFrom = AnalyticsParam.TxSentFrom.Staking( - blockchain = cryptoCurrency.network.name, - token = cryptoCurrency.symbol, + blockchain = value.cryptoCurrencyName, + token = value.cryptoCurrencySymbol, feeType = AnalyticsParam.FeeType.Normal, ), memoType = Basic.TransactionSent.MemoType.Null, @@ -96,9 +102,21 @@ internal class StakingAnalyticSender( ) analyticsEventHandler.send( StakingAnalyticsEvents.StakeInProgressScreenOpened( - validator = validator, - token = cryptoCurrency.symbol, + validator = validatorName, + token = value.cryptoCurrencySymbol, ), ) } + + private fun getStakingActionType(value: StakingUiState): StakingActionType { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + + return when (value.actionType) { + StakingActionCommonType.ENTER -> StakingActionType.STAKE + StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE + StakingActionCommonType.PENDING_REWARDS, + StakingActionCommonType.PENDING_OTHER, + -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 202219cb3b..9e4db06b4e 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 @@ -92,6 +92,7 @@ internal sealed class StakingStates { val feeState: FeeState, val validatorState: ValidatorState, val pendingAction: PendingAction?, + val pendingActions: ImmutableList?, val notifications: ImmutableList, val footerText: TextReference, val transactionDoneState: TransactionDoneState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt new file mode 100644 index 0000000000..55446f2303 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -0,0 +1,102 @@ +package com.tangem.features.staking.impl.presentation.state.helpers + +import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.coroutines.DelayedWork +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.* + +@Suppress("LongParameterList") +internal class StakingBalanceUpdater @AssistedInject constructor( + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, + @DelayedWork private val coroutineScope: CoroutineScope, + @Assisted private val userWallet: UserWallet, + @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) { + 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() + } + } + + suspend fun instantUpdate() { + coroutineScope { + listOf( + async { + updateStakeBalance() + }, + async { + updateNetworkStatuses(delay = 0) + }, + ).awaitAll() + } + } + + private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) { + updateDelayedNetworkStatusUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + delayMillis = delay, + refresh = true, + ) + } + + private suspend fun updateStakeBalance() { + stakingYieldBalanceUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + refresh = true, + ) + } + + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, + ) + + txHistoryItemsCountEither.onRight { + getTxHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, + refresh = true, + ) + } + } + + @AssistedFactory + interface Factory { + fun create(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet): StakingBalanceUpdater + } + + private companion object { + const val BALANCE_UPDATE_DELAY = 11_000L + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt new file mode 100644 index 0000000000..0fdda4ab88 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -0,0 +1,238 @@ +package com.tangem.features.staking.impl.presentation.state.helpers + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.isZero +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.staking.EstimateGasUseCase +import com.tangem.domain.staking.model.StakingApproval +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.staking.impl.presentation.state.StakingStateController +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.utils.extensions.orZero +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class StakingFeeTransactionLoader @AssistedInject constructor( + private val stateController: StakingStateController, + private val getAllowanceUseCase: GetAllowanceUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val estimateGasUseCase: EstimateGasUseCase, + @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, + @Assisted private val userWallet: UserWallet, + @Assisted private val yield: Yield, + @Assisted private val stakingApproval: StakingApproval, +) { + + suspend fun getFee( + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + onFeeError: (GetFeeError) -> Unit, + onStakingFee: (Fee) -> Unit, + onApprovalFee: (TransactionFee) -> Unit, + ) { + val state = stateController.value + val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + + val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + + val validatorAddress = validatorState.chosenValidator.address + + val approval = stakingApproval as? StakingApproval.Needed + if (approval != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + + if (allowance < amount) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + onApprovalFee = onApprovalFee, + onApprovalFeeError = onFeeError, + ) + } else { + estimateGas( + pendingAction = pendingAction, + pendingActions = pendingActions, + amount = amount, + validatorAddress = validatorAddress, + onFeeError = onFeeError, + onStakingFee = onStakingFee, + ) + } + } else { + estimateGas( + pendingAction = pendingAction, + pendingActions = pendingActions, + amount = amount, + validatorAddress = validatorAddress, + onFeeError = onFeeError, + onStakingFee = onStakingFee, + ) + } + } + + private suspend fun estimateGas( + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + amount: BigDecimal, + validatorAddress: String, + onFeeError: (GetFeeError) -> Unit, + onStakingFee: (Fee) -> Unit, + ) { + val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address") + + val gasEstimate = if (isSolanaWithdraw(cryptoCurrencyStatus.currency.network.id.value, pendingActions)) { + val result = coroutineScope { + pendingActions?.map { action -> + async { + // Simultaneous or quick api calls can sometimes return ZERO fee + estimateFeeRetry { + estimateFee( + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + action = action, + ) + }.getOrElse { + onFeeError(GetFeeError.DataError(Throwable(it.toString()))) + null + } + } + }?.awaitAll()?.filterNotNull() + } + + if (result.isNullOrEmpty()) { + onFeeError(GetFeeError.UnknownError) + return + } + + val totalAmount = result.sumOf { it.amount } + val totalGasLimit = result.sumOf { it.gasLimit?.toBigDecimalOrNull().orZero() } + StakingGasEstimate( + amount = totalAmount, + token = result.first().token, + gasLimit = totalGasLimit.toPlainString().orEmpty(), + ) + } else { + estimateFee( + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + action = pendingAction, + ).getOrElse { + onFeeError(GetFeeError.DataError(Throwable(it.toString()))) + return + } + } + + onStakingFee( + Fee.Common( + Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ), + ), + ) + } + + private suspend fun estimateFee( + amount: BigDecimal, + sourceAddress: String, + validatorAddress: String, + action: PendingAction?, + ) = estimateGasUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + params = ActionParams( + actionCommonType = stateController.value.actionType, + integrationId = yield.id, + amount = amount, + address = sourceAddress, + validatorAddress = validatorAddress, + token = yield.token, + passthrough = action?.passthrough, + type = action?.type, + ), + ) + + private suspend fun getApproveFee( + amount: BigDecimal, + validatorAddress: String, + onApprovalFee: (TransactionFee) -> Unit, + onApprovalFeeError: (GetFeeError) -> Unit, + ) { + getFeeUseCase( + amount = amount, + destination = validatorAddress, + userWallet = userWallet, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).fold( + ifRight = { fee -> + onApprovalFee(fee) + }, + ifLeft = { error -> + onApprovalFeeError(error) + }, + ) + } + + private suspend fun estimateFeeRetry( + times: Int = 3, + delay: Long = 1000, + block: suspend () -> Either, + ): Either { + repeat(times - 1) { + val feeResult = block() + feeResult.fold( + ifLeft = { return feeResult }, + ifRight = { + if (!it.amount.isZero()) return feeResult + }, + ) + delay(delay) + } + return block() + } + + @AssistedFactory + interface Factory { + fun create( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWallet: UserWallet, + yield: Yield, + stakingApproval: StakingApproval, + ): StakingFeeTransactionLoader + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt new file mode 100644 index 0000000000..e6e0b2b6e0 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -0,0 +1,271 @@ +package com.tangem.features.staking.impl.presentation.state.helpers + +import arrow.core.getOrElse +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase +import com.tangem.domain.staking.GetStakingTransactionUseCase +import com.tangem.domain.staking.SaveUnsubmittedHashUseCase +import com.tangem.domain.staking.SubmitHashUseCase +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.StakingError +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.StakingTransaction +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.usecase.SendMultipleTransactionUseCase +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount +import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.utils.extensions.orZero +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import timber.log.Timber +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class StakingTransactionSender @AssistedInject constructor( + private val stateController: StakingStateController, + private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, + private val getStakingTransactionUseCase: GetStakingTransactionUseCase, + private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, + private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val submitHashUseCase: SubmitHashUseCase, + private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, + @Assisted private val userWallet: UserWallet, + @Assisted private val yield: Yield, + @Assisted private val isAmountSubtractAvailable: Boolean, +) { + + private val balanceUpdater: StakingBalanceUpdater + get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet) + + suspend fun constructAndSendTransactions( + onConstructSuccess: (List) -> Unit, + onConstructError: (StakingError) -> Unit, + onSendSuccess: (String) -> Unit, + onSendError: (SendTransactionError?) -> Unit, + ) { + val state = stateController.value + + val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee + ?: error("No fee provided") + + val stakingTransactions = getStakingTransactions( + state = state, + confirmationState = confirmationState, + onConstructError = onConstructError, + ) + + val fullTransactionsData = getConstructedTransactions( + stakingTransactions = stakingTransactions, + fee = fee, + onConstructError = onConstructError, + ) + + if (fullTransactionsData.isNullOrEmpty()) { + onConstructError(StakingError.UnknownError) + return + } + + onConstructSuccess(fullTransactionsData.map { it.stakeKitTransaction }) + + sendStakingTransaction( + fullTransactionsData = fullTransactionsData, + onSendSuccess = onSendSuccess, + onSendError = onSendError, + ) + } + + private suspend fun getStakingTransactions( + state: StakingUiState, + confirmationState: StakingStates.ConfirmationState.Data, + onConstructError: (StakingError) -> Unit, + ) = coroutineScope { + val isAllWithdrawAction = isSolanaWithdraw( + cryptoCurrencyStatus.currency.network.id.value, + confirmationState.pendingActions, + ) + if (isAllWithdrawAction) { + confirmationState.pendingActions?.map { action -> + async { + getStakingTransaction( + state = state, + action = action, + confirmationState = confirmationState, + onConstructError = onConstructError, + ) + } + }?.awaitAll()?.flatten() + } else { + getStakingTransaction( + state = state, + confirmationState = confirmationState, + onConstructError = onConstructError, + ) + } + } + + private suspend fun getConstructedTransactions( + stakingTransactions: List?, + fee: Fee, + onConstructError: (StakingError) -> Unit, + ) = coroutineScope { + stakingTransactions?.filterNot { it.type == StakingTransactionType.APPROVAL } + ?.map { transaction -> + async { + getConstructedStakingTransactionUseCase( + networkId = cryptoCurrencyStatus.currency.network.id.value, + fee = fee, + transactionId = transaction.id, + ).fold( + ifRight = { (constructedTransaction, transactionData) -> + FullTransactionData( + stakeKitTransaction = constructedTransaction, + tangemTransaction = transactionData, + ) + }, + ifLeft = { + onConstructError(it) + null + }, + ) + } + }?.awaitAll()?.filterNotNull() + } + + private suspend fun getStakingTransaction( + state: StakingUiState, + confirmationState: StakingStates.ConfirmationState.Data, + action: PendingAction? = confirmationState.pendingAction, + onConstructError: (StakingError) -> Unit, + ): List { + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee + ?: error("No fee provided") + val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address") + val amountState = state.amountState as? AmountState.Data + ?: error("No amount provided") + + val validatorAddress = validatorState.chosenValidator.address + val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy) + + return getStakingTransactionUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + params = ActionParams( + actionCommonType = state.actionType, + integrationId = yield.id, + amount = amount, + address = defaultAddress, + validatorAddress = validatorAddress, + token = yield.token, + passthrough = action?.passthrough, + type = action?.type, + ), + ).getOrElse { + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(state.cryptoCurrencyName)) + onConstructError(it) + return emptyList() + } + } + + private suspend fun sendStakingTransaction( + fullTransactionsData: List, + onSendSuccess: (txUrl: String) -> Unit, + onSendError: (SendTransactionError?) -> Unit, + ) { + sendMultipleTransactionUseCase( + txsData = fullTransactionsData.map { it.tangemTransaction }, + userWallet = userWallet, + network = cryptoCurrencyStatus.currency.network, + ).fold( + ifLeft = { error -> + onSendError(error) + }, + ifRight = { transactionHashes -> + submitHash( + transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, + transactionHashes = transactionHashes, + ) + val txUrl = getExplorerTransactionUrlUseCase( + txHash = transactionHashes.last(), + networkId = cryptoCurrencyStatus.currency.network.id, + ).getOrElse { "" } + + balanceUpdater.scheduleUpdates() + onSendSuccess(txUrl) + }, + ) + } + + private suspend fun submitHash(transactionIds: List, transactionHashes: List) { + transactionIds + .zip(transactionHashes) + .forEach { (transactionId, transactionHash) -> + submitHashUseCase.submitHash( + transactionId = transactionId, + transactionHash = transactionHash, + ) + .onLeft { + analyticsEventHandler.send( + StakingAnalyticsEvents.StakingError(stateController.value.cryptoCurrencyName), + ) + saveUnsubmittedHashUseCase.invoke( + transactionId = transactionId, + transactionHash = transactionHash, + ) + }.onRight { + Timber.d("Successful hash submission") + } + } + } + + private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal { + val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") + val feeValue = fee.amount.value ?: error("No fee value") + val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER + + return checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy.orZero(), + ) + } + + private data class FullTransactionData( + val stakeKitTransaction: StakingTransaction, + val tangemTransaction: TransactionData.Compiled, + ) + + @AssistedFactory + interface Factory { + fun create( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWallet: UserWallet, + yield: Yield, + isAmountSubtractAvailable: Boolean, + ): StakingTransactionSender + } +} \ 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 5e63fa4e32..b5b6e9061f 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 @@ -92,5 +92,6 @@ internal object ConfirmationStatePreviewData { pendingAction = null, isApprovalNeeded = false, reduceAmountBy = null, + pendingActions = null, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 4cc05fa7f2..b4c0e2a299 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.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -15,7 +16,12 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) {} + override fun onNextClick( + actionTypeToOverwrite: StakingActionCommonType?, + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + ) { + } override fun onActionClick() {} @@ -55,7 +61,8 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onActiveStake(activeStake: BalanceState) {} - override fun getFee(pendingAction: PendingAction?) {} + override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { + } override fun onAmountReduceByClick( reduceAmountBy: BigDecimal, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index d9f2c9725f..35e1219e3f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -91,7 +91,12 @@ internal class AddStakingNotificationsTransformer( prevState = prevState, feeError = feeError, sendingAmount = sendingAmount, - onReload = { prevState.clickIntents.getFee(confirmationState.pendingAction) }, + onReload = { + prevState.clickIntents.getFee( + confirmationState.pendingAction, + confirmationState.pendingActions, + ) + }, feeValue = feeValue, ) // warnings diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt index 251b237e9d..fe77e4e0c2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer -internal class DismissBottomSheetStateTransformer : Transformer { +internal object DismissBottomSheetStateTransformer : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy(bottomSheetConfig = null) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt new file mode 100644 index 0000000000..e3adab9f50 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +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.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class SetActionToExecuteTransformer( + private val actionTypeToOverwrite: StakingActionCommonType, + private val pendingAction: PendingAction?, + private val pendingActions: ImmutableList?, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + return prevState.copy( + actionType = actionTypeToOverwrite, + confirmationState = confirmationState?.copy( + pendingAction = pendingAction, + pendingActions = pendingActions, + ) ?: StakingStates.ConfirmationState.Empty(), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index ee19ad787a..196784785f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -10,12 +10,14 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, private val action: PendingAction?, + private val actions: ImmutableList?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -38,6 +40,7 @@ internal class SetConfirmationStateAssentTransformer( ), validatorState = validatorState.copySealed(isClickable = true), pendingAction = action, + pendingActions = actions, isPrimaryButtonEnabled = true, isApprovalNeeded = false, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 32936b8d7e..db27e0c2d9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,43 +1,26 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateCompletedTransformer( - private val appCurrencyProvider: Provider, - private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - private val fee: Fee, private val txUrl: String, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - confirmationState = prevState.confirmationState.copyWrapped(fee), + confirmationState = prevState.confirmationState.copyWrapped(), ) } - private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState { + private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { - val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( isPrimaryButtonEnabled = true, innerState = InnerConfirmationStakingState.COMPLETED, - feeState = FeeState.Content( - fee = fee, - rate = feeCryptoCurrencyStatus?.value?.fiatRate, - isFeeConvertibleToFiat = isFeeConvertibleToFiat, - appCurrency = appCurrencyProvider(), - isFeeApproximate = false, - ), - validatorState = validatorState.copySealed( - isClickable = false, - ), + validatorState = validatorState.copySealed(isClickable = false), footerText = TextReference.EMPTY, notifications = persistentListOf(), transactionDoneState = TransactionDoneState.Content( 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 076c5577a7..bd88fa6ffc 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 @@ -38,6 +38,7 @@ internal class SetConfirmationStateLoadingTransformer( footerText = getFooter(prevState), transactionDoneState = TransactionDoneState.Empty, pendingAction = possibleConfirmationState?.pendingAction, + pendingActions = possibleConfirmationState?.pendingActions, isApprovalNeeded = false, reduceAmountBy = null, ), 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 8779bf824e..58d6743dbd 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 @@ -224,6 +224,7 @@ internal class SetInitialDataStateTransformer( footerText = TextReference.EMPTY, transactionDoneState = TransactionDoneState.Empty, pendingAction = null, + pendingActions = null, isApprovalNeeded = isApprovalNeeded, reduceAmountBy = null, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index 0116e0d6d5..2822f01758 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -3,7 +3,10 @@ package com.tangem.features.staking.impl.presentation.state.utils import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.lib.crypto.BlockchainUtils.isSolana +import kotlinx.collections.immutable.ImmutableList @Suppress("CyclomaticComplexMethod") internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) { @@ -24,4 +27,10 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake) StakingActionType.UNKNOWN -> TextReference.EMPTY null -> TextReference.EMPTY +} + +internal fun isSolanaWithdraw(networkId: String, pendingActions: ImmutableList?): Boolean { + val isSolana = isSolana(networkId) + val isWithdraw = pendingActions?.all { it.type == StakingActionType.WITHDRAW } == true + return isSolana && isWithdraw } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 1a001e874d..e666de9908 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -30,17 +30,16 @@ import java.math.BigDecimal @Composable internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { + val backgroundColor = if (isTransactionSent) { + TangemTheme.colors.background.action + } else { + TangemTheme.colors.button.disabled + } Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background( - if (isTransactionSent) { - TangemTheme.colors.background.action - } else { - TangemTheme.colors.button.disabled - }, - ) + .background(backgroundColor) .padding(TangemTheme.dimens.spacing12), ) { Text( @@ -49,9 +48,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { color = TangemTheme.colors.text.tertiary, ) - Box( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - ) { + Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { when (feeState) { is FeeState.Content -> { val feeAmount = feeState.fee?.amount @@ -82,6 +79,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { SelectorRowItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, + isSelected = true, paddingValues = PaddingValues(), showDivider = false, ) @@ -91,6 +89,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { SelectorRowItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, + isSelected = true, paddingValues = PaddingValues(), ) FeeError(feeState) 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 ed0cfbffc4..c6fe6235d4 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 @@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType +import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -15,7 +16,11 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick(actionTypeToOverwrite: StakingActionCommonType? = null, pendingAction: PendingAction? = null) + fun onNextClick( + actionTypeToOverwrite: StakingActionCommonType? = null, + pendingAction: PendingAction? = null, + pendingActions: ImmutableList? = null, + ) fun onActionClick() @@ -27,7 +32,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onInfoClick(infoType: InfoType) - fun getFee(pendingAction: PendingAction?) + fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null) 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 b283511af9..e23576a2b7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -6,9 +6,6 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle @@ -26,24 +23,22 @@ 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.IsAnyTokenStakedUseCase +import com.tangem.domain.staking.IsApproveNeededUseCase import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction -import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.usecase.* -import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -55,6 +50,9 @@ 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.helpers.StakingBalanceUpdater +import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeTransactionLoader +import com.tangem.features.staking.impl.presentation.state.helpers.StakingTransactionSender import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.* import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer @@ -62,14 +60,14 @@ import com.tangem.features.staking.impl.presentation.state.transformers.approval import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer -import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount +import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem -import com.tangem.utils.extensions.orZero import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import java.util.concurrent.CopyOnWriteArrayList @@ -86,22 +84,9 @@ internal class StakingViewModel @Inject constructor( private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getStakingTransactionUseCase: GetStakingTransactionUseCase, - private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, - private val estimateGasUseCase: EstimateGasUseCase, private val sendTransactionUseCase: SendTransactionUseCase, - private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase, - private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, - private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, - private val submitHashUseCase: SubmitHashUseCase, - 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 vibratorHapticManager: VibratorHapticManager, private val feedbackManager: FeedbackManager, @@ -112,8 +97,10 @@ internal class StakingViewModel @Inject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase, + private val stakingTransactionLoader: StakingTransactionSender.Factory, + private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory, + private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, private val analyticsEventHandler: AnalyticsEventHandler, - @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -144,6 +131,31 @@ internal class StakingViewModel @Inject constructor( private var isInitialInfoAnalyticSent: Boolean = false + private val balanceUpdater by lazy(LazyThreadSafetyMode.NONE) { + stakingBalanceUpdater.create( + cryptoCurrencyStatus, + userWallet, + ) + } + + private val feeLoader by lazy(LazyThreadSafetyMode.NONE) { + stakingFeeTransactionLoader.create( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWallet = userWallet, + yield = yield, + stakingApproval = stakingApproval, + ) + } + + private val transactionSender by lazy(LazyThreadSafetyMode.NONE) { + stakingTransactionLoader.create( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWallet = userWallet, + yield = yield, + isAmountSubtractAvailable = isAmountSubtractAvailable, + ) + } + private val stakingEventFactory: StakingEventFactory get() = StakingEventFactory( stateController = stateController, @@ -180,16 +192,13 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) { + override fun onNextClick( + actionTypeToOverwrite: StakingActionCommonType?, + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + ) { if (actionTypeToOverwrite != null) { - stateController.update { it.copy(actionType = actionTypeToOverwrite) } - stateController.update { - val confirmationState = it.confirmationState as? StakingStates.ConfirmationState.Data - it.copy( - confirmationState = confirmationState?.copy(pendingAction = pendingAction) - ?: StakingStates.ConfirmationState.Empty(), - ) - } + stateController.update(SetActionToExecuteTransformer(actionTypeToOverwrite, pendingAction, pendingActions)) } stakingStateRouter.onNextClick() when { @@ -198,7 +207,7 @@ internal class StakingViewModel @Inject constructor( onRefreshSwipe(isRefreshing = false) } isAssentState() -> { - getFee(pendingAction) + getFee(pendingAction, pendingActions) val amountState = value.amountState as? AmountState.Data if (amountState?.amountTextField?.isWarning == true) { stateController.update( @@ -215,50 +224,40 @@ internal class StakingViewModel @Inject constructor( handleOnNextConfirmationClick() } - override fun getFee(pendingAction: PendingAction?) { + override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { + stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) viewModelScope.launch { - stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) - val cryptoCurrencyValue = cryptoCurrencyStatus.value - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content - ?: error("No validator provided") - - val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided") - val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value - ?: error("No available address") - val validatorAddress = validatorState.chosenValidator.address - - val approval = stakingApproval as? StakingApproval.Needed - if (approval != null) { - val allowance = getAllowanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - - if (allowance < amount) { - getApproveFee( - amount = amount, - validatorAddress = validatorAddress, + feeLoader.getFee( + pendingAction = pendingAction, + pendingActions = pendingActions, + onStakingFee = { gasEstimate -> + stateController.update( + SetConfirmationStateAssentTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = gasEstimate, + action = pendingAction, + actions = pendingActions, + ), ) - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, + updateNotifications() + }, + onFeeError = { error -> + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) + stateController.update(AddStakingErrorTransformer()) + updateNotifications(error) + }, + onApprovalFee = { fee -> + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = fee, + ), ) - } - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } + updateNotifications() + }, + ) } } @@ -266,139 +265,32 @@ internal class StakingViewModel @Inject constructor( if (isAssentState()) { viewModelScope.launch { stateController.update(SetConfirmationStateInProgressTransformer()) - - val confirmationState = - value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content - ?: error("No validator provided") - val amountState = value.amountState as? AmountState.Data ?: error("No amount provided") - - val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") - val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: error("No available address") - val pendingAction = confirmationState.pendingAction - - val stakingTransactions = getStakingTransactionUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - params = ActionParams( - actionCommonType = value.actionType, - integrationId = yield.id, - amount = getAmount(amountState, fee, confirmationState.reduceAmountBy), - address = defaultAddress, - validatorAddress = validatorState.chosenValidator.address, - token = yield.token, - passthrough = pendingAction?.passthrough, - type = pendingAction?.type, - ), - ).getOrElse { - Timber.e(it.toString()) - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stakingEventFactory.createStakingErrorAlert(it) - stateController.update(SetConfirmationStateResetAssentTransformer) - return@launch - } - - val fullTransactionsData = stakingTransactions - .filterNot { it.type == StakingTransactionType.APPROVAL } - .map { transaction -> - val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase( - networkId = cryptoCurrencyStatus.currency.network.id.value, - fee = fee, - transactionId = transaction.id, - ).getOrElse { - Timber.e(it.toString()) - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stakingEventFactory.createStakingErrorAlert(it) - stateController.update(SetConfirmationStateResetAssentTransformer) - return@launch - } - - FullTransactionData( - stakeKitTransaction = constructedTransaction, - tangemTransaction = transactionData, - ) - } - - transactionsInProgress.addAll(fullTransactionsData.map { it.stakeKitTransaction }) - - sendStakingTransaction( - fullTransactionsData = fullTransactionsData, - fee = fee, - pendingAction = confirmationState.pendingAction, - validator = validatorState.chosenValidator.name, + transactionSender.constructAndSendTransactions( + onConstructSuccess = { constructedTransactions -> + transactionsInProgress.addAll(constructedTransactions) + }, + onConstructError = { error -> + Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) + stakingEventFactory.createStakingErrorAlert(error) + stateController.update(SetConfirmationStateResetAssentTransformer) + }, + onSendSuccess = { txUrl -> + stakingAnalyticSender.sendTransactionStakingAnalytics(stateController.value) + transactionsInProgress.clear() + stateController.update(SetConfirmationStateCompletedTransformer(txUrl)) + }, + onSendError = { error -> + Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) + stakingEventFactory.createSendTransactionErrorAlert(error) + stateController.update(SetConfirmationStateResetAssentTransformer) + }, ) } } } - private suspend fun estimateGas( - pendingAction: PendingAction?, - amount: BigDecimal, - sourceAddress: String, - validatorAddress: String, - ) { - val gasEstimate = estimateGasUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - params = ActionParams( - actionCommonType = value.actionType, - integrationId = yield.id, - amount = amount, - address = sourceAddress, - validatorAddress = validatorAddress, - token = yield.token, - passthrough = pendingAction?.passthrough, - type = pendingAction?.type, - ), - ).getOrElse { - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stateController.update(AddStakingErrorTransformer()) - updateNotifications(GetFeeError.UnknownError) - return - } - stateController.update( - SetConfirmationStateAssentTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = Fee.Common( - Amount( - currencySymbol = gasEstimate.token.symbol, - value = gasEstimate.amount, - decimals = gasEstimate.token.decimals, - ), - ), - action = pendingAction, - ), - ) - updateNotifications() - } - - private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) { - getFeeUseCase( - amount = amount, - destination = validatorAddress, - userWallet = userWallet, - cryptoCurrency = cryptoCurrencyStatus.currency, - ).fold( - ifRight = { fee -> - stateController.update( - SetConfirmationStateAssentApprovalTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, - ), - ) - updateNotifications() - }, - ifLeft = { - analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) - stateController.update(AddStakingErrorTransformer()) - updateNotifications(it) - }, - ) - } - override fun onPrevClick() { if (value.currentStep == StakingStep.Confirmation) { when ((value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState) { @@ -422,10 +314,7 @@ internal class StakingViewModel @Inject constructor( override fun onRefreshSwipe(isRefreshing: Boolean) { stateController.update(SetInitialLoadingStateTransformer(isRefreshing)) viewModelScope.launch { - val balancesUpdate = async { updateStakeBalance() } - val networkUpdate = async { updateNetworkStatuses(0) } - - awaitAll(balancesUpdate, networkUpdate) + balanceUpdater.instantUpdate() }.saveIn(updateBalancesJobHolder) .invokeOnCompletion { stateController.update(SetInitialLoadingStateTransformer(false)) @@ -440,7 +329,7 @@ internal class StakingViewModel @Inject constructor( override fun onInfoClick(infoType: InfoType) { stateController.update( ShowInfoBottomSheetStateTransformer(infoType) { - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) }, ) } @@ -500,11 +389,16 @@ internal class StakingViewModel @Inject constructor( } override fun onActiveStake(activeStake: BalanceState) { - if (activeStake.pendingActions.size > 1) { + val isAllWithdrawActions = isSolanaWithdraw( + cryptoCurrencyStatus.currency.network.id.value, + activeStake.pendingActions, + ) + val isMultiplePendingActions = activeStake.pendingActions.isSingleItem().not() + if (isMultiplePendingActions && !isAllWithdrawActions) { stateController.update( ShowActionSelectorBottomSheetTransformer( pendingActions = activeStake.pendingActions, - onActionSelect = { + onActionSelect = { action -> stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) stateController.update( AmountChangeStateTransformer( @@ -513,19 +407,25 @@ internal class StakingViewModel @Inject constructor( yield, ), ) - onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, pendingAction = it) - stateController.update(DismissBottomSheetStateTransformer()) - }, - onDismiss = { - stateController.update(DismissBottomSheetStateTransformer()) + onNextClick( + actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, + pendingAction = action, + ) + stateController.update(DismissBottomSheetStateTransformer) }, + onDismiss = { stateController.update(DismissBottomSheetStateTransformer) }, ), ) } else { stateController.update(ActionTypeActiveStakeTransformer(cryptoCurrencyStatus, activeStake)) stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - onNextClick(null, activeStake.pendingActions.firstOrNull()) + + onNextClick( + actionTypeToOverwrite = null, + pendingAction = activeStake.pendingActions.firstOrNull(), + pendingActions = activeStake.pendingActions.takeIf { isAllWithdrawActions }, + ) } } @@ -545,7 +445,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) { - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) }, ) } @@ -554,7 +454,7 @@ internal class StakingViewModel @Inject constructor( viewModelScope.launch { stateController.update( SetApprovalBottomSheetInProgressTransformer { - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) }, ) @@ -614,7 +514,7 @@ internal class StakingViewModel @Inject constructor( ifRight = { stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency) stateController.update(SetApprovalInProgressTransformer) - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) awaitForAllowance(confirmationState.pendingAction) }, ) @@ -720,7 +620,7 @@ internal class StakingViewModel @Inject constructor( val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") if (allowance >= amount) { - getFee(pendingAction) + getFee(pendingAction = pendingAction, pendingActions = null) allowanceTaskScheduler.cancelTask() } }, @@ -884,146 +784,6 @@ internal class StakingViewModel @Inject constructor( .launchIn(viewModelScope) } - private suspend fun sendStakingTransaction( - fullTransactionsData: List, - fee: Fee, - pendingAction: PendingAction?, - validator: String, - ) { - val cryptoCurrency = cryptoCurrencyStatus.currency - val action = when (value.actionType) { - StakingActionCommonType.ENTER -> StakingActionType.STAKE - StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> pendingAction?.type - } - action?.let { - analyticsEventHandler.send( - StakingAnalyticsEvents.ButtonAction( - action = it.name, - token = value.cryptoCurrencyName, - validator = validator, - ), - ) - } - sendMultipleTransactionUseCase( - txsData = fullTransactionsData.map { it.tangemTransaction }, - userWallet = userWallet, - network = cryptoCurrency.network, - ).fold( - ifLeft = { error -> - Timber.e(error.toString()) - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stateController.update( - SetConfirmationStateAssentTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, - action = pendingAction, - ), - ) - stakingEventFactory.createSendTransactionErrorAlert(error) - stateController.update(SetConfirmationStateResetAssentTransformer) - }, - ifRight = { transactionHashes -> - stakingAnalyticSender.sendTransactionStakingAnalytics(cryptoCurrency, validator) - transactionsInProgress.clear() - submitHash( - transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, - transactionHashes = transactionHashes, - ) - scheduleUpdates() - val txUrl = getExplorerTransactionUrlUseCase( - txHash = transactionHashes.last(), - networkId = cryptoCurrency.network.id, - ).getOrElse { "" } - stateController.update( - SetConfirmationStateCompletedTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, - txUrl = txUrl, - ), - ) - }, - ) - } - - private suspend fun submitHash(transactionIds: List, transactionHashes: List) { - transactionIds - .zip(transactionHashes) - .forEach { (transactionId, transactionHash) -> - submitHashUseCase.submitHash( - transactionId = transactionId, - transactionHash = transactionHash, - ) - .onLeft { - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - saveUnsubmittedHashUseCase.invoke( - transactionId = transactionId, - transactionHash = transactionHash, - ) - }.onRight { - Timber.d("Successful hash submission") - } - } - } - - 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(delay: Long = BALANCE_UPDATE_DELAY) { - updateDelayedNetworkStatusUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - delayMillis = 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, - currency = cryptoCurrencyStatus.currency, - refresh = true, - ) - } - } - private fun isAssentState(): Boolean { return value.currentStep == StakingStep.Confirmation && (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == @@ -1039,28 +799,8 @@ internal class StakingViewModel @Inject constructor( .getOrElse { false } } - private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal { - val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") - val feeValue = fee.amount.value ?: error("No fee value") - val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER - - return checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction, - cryptoCurrencyStatus = cryptoCurrencyStatus, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = reduceAmountBy.orZero(), - ) - } - - private data class FullTransactionData( - val stakeKitTransaction: StakingTransaction, - val tangemTransaction: TransactionData.Compiled, - ) - private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L - const val BALANCE_UPDATE_DELAY = 11_000L } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index aa6d57aaf2..775424bde0 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-769" +tangemBlockchainSdk = "develop-770" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-381" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 70419c49a2351eb094176e0c01a5390441a8354e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 13:40:42 +0500 Subject: [PATCH 186/197] Updated on 2026-08-14 --- .../converters/TokenMarketInfoConverter.kt | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index 1aa7d1e9af..535b4aca6c 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -1,5 +1,8 @@ package com.tangem.data.markets.converters +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenQuotes @@ -47,13 +50,27 @@ internal object TokenMarketInfoConverter : Converter.convert(): List { - return map { - TokenMarketInfo.Network( - networkId = it.networkId, - exchangeable = it.exchangeable, - contractAddress = it.contractAddress, - decimalCount = it.decimalCount, - ) + return mapNotNull { network -> + val blockchain = Blockchain.fromNetworkId(network.networkId) + when { + network.contractAddress.isNullOrEmpty() -> { + TokenMarketInfo.Network( + networkId = network.networkId, + exchangeable = network.exchangeable, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + } + blockchain != null && blockchain.isSupportedInApp() && blockchain.canHandleTokens() -> { + TokenMarketInfo.Network( + networkId = network.networkId, + exchangeable = network.exchangeable, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + } + else -> null + } } } From 7f6f605186d7a330274cd179f22e0bf9a6c55ad3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 12:09:24 +0300 Subject: [PATCH 187/197] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 2 +- .../ui/AddCustomTokenTestContent.kt | 2 +- .../GiveTxPermisssionBottomSheet.kt | 3 +- .../response/TokenMarketInfoResponse.kt | 8 +- core/res/src/main/res/values-es/strings.xml | 23 +- core/res/src/main/res/values-fr/strings.xml | 23 +- core/res/src/main/res/values-ja/strings.xml | 15 +- .../com/tangem/core/ui/components/Keyboard.kt | 39 +- .../tangem/core/ui/components/TangemSwitch.kt | 4 +- .../atoms/radiobutton/TangemRadioButton.kt | 4 +- .../bottomsheets/TangemBottomSheet.kt | 8 +- .../components/inputrow/InputRowBestRate.kt | 4 +- .../ui/components/inputrow/InputRowDefault.kt | 4 +- .../ui/components/inputrow/InputRowEnter.kt | 4 +- .../inputrow/InputRowEnterAmount.kt | 4 +- .../ui/components/inputrow/InputRowImage.kt | 4 +- .../inputrow/InputRowImageSelector.kt | 4 +- .../components/inputrow/inner/PasteButton.kt | 6 +- .../NotificationWithBackground.kt | 4 +- .../notifications/OkxPromoNotification.kt | 4 +- .../progressbar/LinearProgressIndicator.kt | 126 +++++++ .../ui/components/rows/RoundableCornersRow.kt | 4 +- .../sheetscaffold/DraggableAnchorsUtils.kt | 122 +++++++ .../TangemBottomSheetScaffold.kt | 334 +++++++++++++++++ .../sheetscaffold/TangemSheetState.kt | 339 ++++++++++++++++++ .../core/ui/reordarable/ReorderableItem.kt | 102 ++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 2 - .../ui/components/PricePerformanceBlock.kt | 3 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 5 +- .../ui/components/MarketsListLazyColumn.kt | 5 +- .../referral/ui/ReferralBottomSheet.kt | 11 +- .../ui/StakingInitialInfoContent.kt | 8 +- .../presentation/ui/block/ValidatorBlock.kt | 4 +- .../tangem/feature/swap/ui/TransactionCard.kt | 6 +- .../components/staking/StakingBalanceBlock.kt | 4 +- .../organizetokens/OrganizeTokensScreen.kt | 2 +- .../presentation/wallet/ui/WalletScreen.kt | 177 +++------ gradle/dependencies.toml | 8 +- 38 files changed, 1217 insertions(+), 214 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/DraggableAnchorsUtils.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemSheetState.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/reordarable/ReorderableItem.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5979dc8c9a..4a84a2b29f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,7 +55,7 @@ android:launchMode="singleTop" android:screenOrientation="portrait" android:theme="@style/SplashTheme" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustNothing"> diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt index 3bb17cddb9..f5b7759a82 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt @@ -43,7 +43,7 @@ import kotlinx.coroutines.launch internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) { val coroutineScope = rememberCoroutineScope() val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed), + bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current), ) BackHandler( 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 f10c86f973..52398bd178 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 @@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -166,7 +165,7 @@ private fun AmountItem( .clickable( enabled = onChangeApproveType != null, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), onClick = { isExpandSelector = true }, ), ) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 21b38fdfb0..13e4a315e1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -104,13 +104,13 @@ data class TokenMarketInfoResponse( data class Links( @Json(name = "official_links") - val officialLinks: List?, + val officialLinks: List? = null, @Json(name = "social") - val social: List?, + val social: List? = null, @Json(name = "repository") - val repository: List?, + val repository: List? = null, @Json(name = "blockchain_site") - val blockchainSite: List?, + val blockchainSite: List? = null, ) data class Link( diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b44332a6c6..3c8fb3e199 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -117,6 +117,7 @@ Importe En progreso Más tarde + %1$s quedan Bloqueado Red principal Tarifa de la red @@ -295,6 +296,7 @@ Ilimitado Ordenar Escanee + Esta información fue generada con IA Toque para cambiar la contraseña Toque para cambiar la contraseña Para crear la billetera, toque la tarjeta como se muestra arriba y no la retire hasta el final de la operación @@ -348,6 +350,7 @@ Mi portafolio Mercado Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem + Para agregar tokens, abra esta página o toque la barra de búsqueda Los datos de este apartado proceden de las siguientes redes: %s No se pueden cargar los datos… Sin datos @@ -358,12 +361,12 @@ Sin resultado Seleccione una red Seleccione una billetera - 1 mes + 1 mo 1 año - 24 horas - 3 meses - 6 meses - 7 días + 24h + 3mos + 6mos + 7d Todo Compradores experimentados Evaluación @@ -395,7 +398,7 @@ El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado Índice de liquidez Bajo - Capitalización de mercado + Capital. de mercado El valor total de una criptomoneda calculado multiplicando su precio por la cantidad de monedas en circulación Evaluación de mercado Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado @@ -412,6 +415,7 @@ La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado. Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens + NFC no está disponible en su dispositivo Deba configurar un único código de acceso para proteger todas sus tarjetas Proteger Puede configurar un código de acceso individual en cada tarjeta más tarde @@ -637,6 +641,7 @@ Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. Nombre El monto del staking debe ser al menos %s + El monto del staking se redondeará a %1$s TRX debido a las reglas de la red. Unstaking de la reclamación Porcentaje de rendimiento anual El porcentaje de rendimiento anual que podrá ganar como participante en el staking. @@ -660,6 +665,7 @@ El período que debes esperar después de solicitar el retiro de fondos del staking antes de que los tokens estén disponibles. Periodo de calentamiento El tiempo permitido para activar la participación en la apuesta. + Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor Bloqueado Migrar Native staking @@ -671,6 +677,7 @@ Gane recompensas por staking Sus fondos estarán disponibles para su uso después del período de desvinculación de %s. Ya puede retirar sus fondos, estarán disponibles para usar de inmediato. + Hacer staking en la red Tron con un nuevo validador transferirá automáticamente todos los fondos de staking a este validador. Listo para retirar Reunir Haga el staking de nuevo @@ -690,8 +697,11 @@ Recompensas El stake está bloqueado Hacer más staking + Toque para desbloquear + Toque para retirar Stake %s El unstaking de %s + ¡La transacción se está procesando! La validación está en curso en la cadena de bloques. Esto puede tardar unos minutos. Desunión Desbloquear Sin staking @@ -701,6 +711,7 @@ Votar Voto bloqueado Retirar + Sus stakes Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta Billetera de hardware revolucionaria Hasta 3 tarjetas físicas por billetera diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index be874b7bdb..d2745aa359 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -117,6 +117,7 @@ Importez En cours Plus tard + Il reste %1$s Verrouillé Réseau principal Commissions du réseau @@ -295,6 +296,7 @@ Illimité Commandez Scannez + Ces informations ont été générées avec l\'IA Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe Pour créer le portefeuille, appuyez sur la carte comme indiqué ci-dessus et ne la retirez pas jusqu\'à la fin de l\'opération @@ -348,6 +350,7 @@ Mon portfolio Marché Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem + Pour ajouter des jetons, faites-le apparaître ou appuyez sur la barre de recherche Les données de cette section proviennent des réseaux suivants : %s Impossible de charger les données… Aucune donnée @@ -358,11 +361,11 @@ Aucun résultat Sélectionnez un réseau Sélectionnez un portefeuille - 1 mois + 1 mo 1 an - 24 heures - 3 mois - 6 mois + 24h + 3mos + 6mos 7j Tout Acheteurs expérimentés @@ -395,8 +398,8 @@ Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée Indice de liquidité Faible - Capitalisation boursière - La valeur totale d\'une crypto-monnaie calculée en multipliant son prix actuel par le nbre de pièces en circulation + Cap. boursière + La valeur d\'une crypto-monnaie est calculée en multipliant son prix par le nombre de pièces en circulation Évaluation du marché Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière Approvisionnement maximal @@ -412,6 +415,7 @@ Le montant total d\'une crypto-monnaie qui a été échangé au cours des dernières 24 heures, indiquant le niveau d\'activité et de liquidité du marché Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché Ajouter des jetons + NFC n\'est pas disponible sur votre appareil Vous devez définir un seul code d\'accès pour protéger toutes vos cartes Protéger Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard @@ -637,6 +641,7 @@ Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Nom Le montant à staker doit être au moins %s + Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. Réclamation déstakée Pourcentage de rendement annuel Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. @@ -660,6 +665,7 @@ La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles. Période d\'échauffement Le temps imparti pour activer la participation au staking. + En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur Bloqué Migrer Native staking @@ -671,6 +677,7 @@ Gagnez des récompenses de staking Vos fonds seront disponibles pour utilisation après la période de désengagement %s. Vous pouvez désormais retirer vos fonds, ils seront disponibles à l\'utilisation immédiatement + Le staking dans le réseau Tron avec un nouveau validateur transférera automatiquement tous les fonds précédemment stakés vers ce validateur Prêt à retirer Réassocier Restakez @@ -690,8 +697,11 @@ Récompenses Stake verrouillé Staker plus + Appuyez pour déverrouiller + Appuyez pour retirer Stake %s déstaker %s + La transaction est en cours de traitement ! La validation est actuellement en cours dans la blockchain. Cela peut prendre quelques minutes. Dissociation Débloquer Non-staké @@ -701,6 +711,7 @@ Voter Vote bloqué Retirer + Vos stakes Stockez vos actifs crypto en toute sécurité tout en conservant les clés privées contenues dans votre carte Portefeuille matériel révolutionnaire Jusqu\'à 3 cartes physiques pour un portefeuille diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 9c10dc67c2..61aa69c13e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -358,13 +358,13 @@ 検索結果はありません ネットワークを選択 ウォレットを選択 - 1ヶ月 - 1年 - 24時間 - 3ヶ月 - 6ヶ月 - 7日 - すべて + 1m + 1y + 24h + 3m + 6m + 7d + 全部 経験豊富な買い手 格付け 並べ替え @@ -637,6 +637,7 @@ これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 名前 ステーキング金額は %s 以上である必要があります + ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ステーキング解除分を請求する 年率 ステーキングに参加することで得られる年間収益率。 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt index 72a9f3a11e..74029fd218 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt @@ -1,15 +1,14 @@ package com.tangem.core.ui.components +import android.os.Build import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.ime -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +@Immutable sealed interface Keyboard { val height: Dp @@ -30,6 +29,34 @@ val Keyboard.isOpened: Boolean @Composable fun keyboardAsState(): State { val bottom = WindowInsets.ime.getBottom(LocalDensity.current) - val isImeVisible = bottom > 0 - return rememberUpdatedState(if (isImeVisible) Keyboard.Opened(bottom.dp) else Keyboard.Closed) + val bottomDp = with(LocalDensity.current) { bottom.toDp() } + + val keyboardStateInternal by rememberUpdatedState( + if (bottom > 0) Keyboard.Opened(bottomDp) else Keyboard.Closed, + ) + + val keyboardState = remember { mutableStateOf(keyboardStateInternal) } + + LaunchedEffect(keyboardStateInternal) { + val falsePositive = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && + keyboardStateInternal is Keyboard.Opened && + keyboardStateInternal.height < 50.dp + // FIX android <=10 devices can randomly send ime paddings, + // which leads to a false positive keyboard opening ([REDACTED_TASK_KEY]) + if (falsePositive) return@LaunchedEffect + + keyboardState.value = keyboardStateInternal + } + + return keyboardState +} + +@Composable +fun rememberIsKeyboardVisible(): State { + val keyboard by keyboardAsState() + return remember(keyboard) { + derivedStateOf { + keyboard.isOpened + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index 7b3c924e95..972f347106 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -8,7 +8,7 @@ import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -47,7 +47,7 @@ fun TangemSwitch( modifier = modifier .clickable( interactionSource = interactionSource, - indication = rememberRipple( + indication = ripple( bounded = false, color = Color.Transparent, ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt index 777062e8bd..31d56ce3c5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon +import androidx.compose.material3.ripple import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.Stroke @@ -39,7 +39,7 @@ fun TangemRadioButton( modifier = modifier.clickable( enabled = isEnabled, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16), + indication = ripple(bounded = false, radius = TangemTheme.dimens.size16), onClick = onClick, ), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 6ef3e53d1f..d9b884cb75 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.bottomsheets +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -14,7 +15,6 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible -import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import kotlinx.coroutines.coroutineScope @@ -150,17 +150,15 @@ inline fun BasicBottomSheet( ) { val model = config.content as? T ?: return - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } ModalBottomSheet( - // FIXME temporary solution to fix height of the bottom sheet - modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight), + modifier = modifier.statusBarsPadding(), onDismissRequest = config.onDismissRequest, sheetState = sheetState, containerColor = containerColor, shape = TangemTheme.shapes.bottomSheetLarge, - windowInsets = WindowInsetsZero, + contentWindowInsets = { WindowInsetsZero }, dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, ) { Column( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index 0a17c85b13..ef0308abd3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -6,9 +6,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -86,7 +86,7 @@ fun InputRowBestRate( .padding(vertical = TangemTheme.dimens.spacing10) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), onClick = onIconClick, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 3ef8fefbc8..cf5d0d6153 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterVertically @@ -94,7 +94,7 @@ fun InputRowDefault( .clickable( enabled = onIconClick != null, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), ) { onIconClick?.invoke() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt index 6a1b07ab53..6b8f360c17 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt @@ -6,9 +6,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -98,7 +98,7 @@ fun InputRowEnter( ) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), ) { onIconClick?.invoke() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt index a4759a11e2..1e15b0b4dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt @@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -104,7 +104,7 @@ fun InputRowEnterAmount( ) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), ) { onIconClick?.invoke() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt index d816d77bf6..90abca3970 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -5,9 +5,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterVertically @@ -111,7 +111,7 @@ fun InputRowImage( .align(CenterVertically) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), ) { onIconClick?.invoke() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt index e1aa081b50..6fcc6ef997 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -64,7 +64,7 @@ fun InputRowImageSelector( .clickable( onClick = onSelect, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), ) .padding(TangemTheme.dimens.spacing12), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index af8a6761b3..79c75c96e6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -9,9 +9,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -65,7 +65,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi ) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius8), + indication = ripple(radius = TangemTheme.dimens.radius8), enabled = isPasteEnabled, onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) @@ -91,7 +91,7 @@ fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) { .size(TangemTheme.dimens.size24) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius12), + indication = ripple(radius = TangemTheme.dimens.radius12), onClick = { onClick("") }, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 13a8aac488..a6031f054b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -125,7 +125,7 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = } .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), ) { config.onCloseClick?.invoke() }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt index 19d96e409b..2888045227 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt @@ -5,9 +5,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -95,7 +95,7 @@ private fun Content(config: NotificationConfig) { .size(TangemTheme.dimens.size20) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius10), + indication = ripple(radius = TangemTheme.dimens.radius10), onClick = it, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt new file mode 100644 index 0000000000..41d6020243 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt @@ -0,0 +1,126 @@ +@file:Suppress("UnnecessaryParentheses") + +package com.tangem.core.ui.components.progressbar + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.layout +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset +import com.tangem.core.ui.res.TangemTheme +import kotlin.math.abs + +/** + * Progress indicators express an unspecified wait time or display the length of a process. + * + * By default there is no animation between [progress] values. You can use + * + * @param progress The progress of this progress indicator, where 0.0 represents no progress and 1.0 + * represents full progress. Values outside of this range are coerced into the range. + * @param modifier the [Modifier] to be applied to this progress indicator + * @param color The color of the progress indicator. + * @param backgroundColor The color of the background behind the indicator, visible when the + * progress has not reached that area of the overall indicator yet. + * @param strokeCap stroke cap to use for the ends of this progress indicator + */ +@Composable +fun LinearProgressIndicator( + progress: () -> Float, + modifier: Modifier = Modifier, + color: Color = TangemTheme.colors.text.accent, + backgroundColor: Color = TangemTheme.colors.background.tertiary, + strokeCap: StrokeCap = StrokeCap.Round, +) { + val coercedProgress = { progress().coerceIn(0f, 1f) } + Canvas( + modifier + .increaseSemanticsBounds() + .semantics(mergeDescendants = true) { + progressBarRangeInfo = ProgressBarRangeInfo(coercedProgress(), 0f..1f) + } + .size(height = 4.dp, width = 24.dp), + ) { + val strokeWidth = size.height + drawLinearIndicatorBackground(backgroundColor, strokeWidth, strokeCap) + drawLinearIndicator(0f, coercedProgress(), color, strokeWidth, strokeCap) + } +} + +internal fun Modifier.increaseSemanticsBounds(): Modifier { + val padding = 10.dp + return this + .layout { measurable, constraints -> + val paddingPx = padding.roundToPx() + // We need to add vertical padding to the semantics bounds in other to meet + // screenreader green box minimum size, but we also want to + // preserve a visual appearance and layout size below that minimum + // in order to maintain backwards compatibility. This custom + // layout effectively implements "negative padding". + val newConstraint = constraints.offset(0, paddingPx * 2) + val placeable = measurable.measure(newConstraint) + + // But when actually placing the placeable, create the layout without additional + // space. Place the placeable where it would've been without any extra padding. + val height = placeable.height - paddingPx * 2 + val width = placeable.width + layout(width, height) { + placeable.place(0, -paddingPx) + } + } + .semantics(mergeDescendants = true) {} + .padding(vertical = padding) +} + +private fun DrawScope.drawLinearIndicator( + startFraction: Float, + endFraction: Float, + color: Color, + strokeWidth: Float, + strokeCap: StrokeCap, +) { + val width = size.width + val height = size.height + // Start drawing from the vertical center of the stroke + val yOffset = height / 2 + + val isLtr = layoutDirection == LayoutDirection.Ltr + val barStart = (if (isLtr) startFraction else 1f - endFraction) * width + val barEnd = (if (isLtr) endFraction else 1f - startFraction) * width + + // if there isn't enough space to draw the stroke caps, fall back to StrokeCap.Butt + if (strokeCap == StrokeCap.Butt || height > width) { + // Progress line + drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth) + } else { + // need to adjust barStart and barEnd for the stroke caps + val strokeCapOffset = strokeWidth / 2 + val coerceRange = strokeCapOffset..(width - strokeCapOffset) + val adjustedBarStart = barStart.coerceIn(coerceRange) + val adjustedBarEnd = barEnd.coerceIn(coerceRange) + + if (abs(endFraction - startFraction) > 0) { + // Progress line + drawLine( + color, + Offset(adjustedBarStart, yOffset), + Offset(adjustedBarEnd, yOffset), + strokeWidth, + strokeCap, + ) + } + } +} + +private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) = + drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index df80c7d5b3..79b1cdb350 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -6,10 +6,10 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -67,7 +67,7 @@ fun RoundableCornersRow( .size(TangemTheme.dimens.size16) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.radius10), + indication = ripple(bounded = false, radius = TangemTheme.dimens.radius10), onClick = iconClick, ), painter = painterResource(id = R.drawable.ic_alert_24), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/DraggableAnchorsUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/DraggableAnchorsUtils.kt new file mode 100644 index 0000000000..f40ba67a78 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/DraggableAnchorsUtils.kt @@ -0,0 +1,122 @@ +@file:Suppress("all") + +package com.tangem.core.ui.components.sheetscaffold + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntSize +import kotlin.math.roundToInt + +/** + * This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout + * node's size and offsetting it. It considers lookahead and reports the appropriate size and + * measurement for the appropriate phase. + * + * @param state The state the anchors should be attached to + * @param orientation The orientation the component should be offset in + * @param anchors Lambda to calculate the anchors based on this layout's size and the incoming + * constraints. These can be useful to avoid subcomposition. + */ +@OptIn(ExperimentalFoundationApi::class) +internal fun Modifier.draggableAnchors( + state: AnchoredDraggableState, + orientation: Orientation, + anchors: (size: IntSize, constraints: Constraints) -> Pair, T>, +) = this then DraggableAnchorsElement(state, anchors, orientation) + +@OptIn(ExperimentalFoundationApi::class) +private class DraggableAnchorsElement( + private val state: AnchoredDraggableState, + private val anchors: (size: IntSize, constraints: Constraints) -> Pair, T>, + private val orientation: Orientation, +) : ModifierNodeElement>() { + + @OptIn(ExperimentalFoundationApi::class) + override fun create() = DraggableAnchorsNode(state, anchors, orientation) + + override fun update(node: DraggableAnchorsNode) { + node.state = state + node.anchors = anchors + node.orientation = orientation + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + if (other !is DraggableAnchorsElement<*>) return false + + if (state != other.state) return false + if (anchors !== other.anchors) return false + if (orientation != other.orientation) return false + + return true + } + + override fun hashCode(): Int { + var result = state.hashCode() + result = 31 * result + anchors.hashCode() + result = 31 * result + orientation.hashCode() + return result + } + + override fun InspectorInfo.inspectableProperties() { + debugInspectorInfo { + properties["state"] = state + properties["anchors"] = anchors + properties["orientation"] = orientation + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +private class DraggableAnchorsNode( + var state: AnchoredDraggableState, + var anchors: (size: IntSize, constraints: Constraints) -> Pair, T>, + var orientation: Orientation, +) : Modifier.Node(), LayoutModifierNode { + private var didLookahead: Boolean = false + + override fun onDetach() { + didLookahead = false + } + + @OptIn(ExperimentalFoundationApi::class) + override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult { + val placeable = measurable.measure(constraints) + // If we are in a lookahead pass, we only want to update the anchors here and not in + // post-lookahead. If there is no lookahead happening (!isLookingAhead && !didLookahead), + // update the anchors in the main pass. + if (!isLookingAhead || !didLookahead) { + val size = IntSize(placeable.width, placeable.height) + val newAnchorResult = anchors(size, constraints) + state.updateAnchors(newAnchorResult.first, newAnchorResult.second) + } + didLookahead = isLookingAhead || didLookahead + return layout(placeable.width, placeable.height) { + // In a lookahead pass, we use the position of the current target as this is where any + // ongoing animations would move. If the component is in a settled state, lookahead + // and post-lookahead will converge. + val offset = if (isLookingAhead) { + state.anchors.positionOf(state.targetValue) + } else { + state.requireOffset() + } + + val xOffset = if (orientation == Orientation.Horizontal) offset else 0f + val yOffset = if (orientation == Orientation.Vertical) offset else 0f + + placeable.place(xOffset.roundToInt(), yOffset.roundToInt()) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt new file mode 100644 index 0000000000..aa68f71190 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt @@ -0,0 +1,334 @@ +@file:Suppress("all") + +package com.tangem.core.ui.components.sheetscaffold + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMap +import androidx.compose.ui.util.fastMaxOfOrNull +import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.* +import com.tangem.core.ui.res.TangemTheme +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +/** + * Material Design standard bottom sheet scaffold. + * + * Standard bottom sheets co-exist with the screen’s main UI region and allow for simultaneously + * viewing and interacting with both regions. They are commonly used to keep a feature or secondary + * content visible on screen when content in main UI region is frequently scrolled or panned. + * + * ![Bottom sheet + * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png) + * + * This component provides API to put together several material components to construct your screen, + * by ensuring proper layout strategy for them and collecting necessary data so these components + * will work together correctly. + * + * @param sheetContent the content of the bottom sheet + * @param modifier the [Modifier] to be applied to this scaffold + * @param scaffoldState the state of the bottom sheet scaffold + * @param sheetPeekHeight the height of the bottom sheet when it is collapsed + * @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take. Pass in + * [Dp.Unspecified] for a sheet that spans the entire screen width. + * @param sheetShape the shape of the bottom sheet + * @param sheetContainerColor the background color of the bottom sheet + * @param sheetContentColor the preferred content color provided by the bottom sheet to its + * children. Defaults to the matching content color for [sheetContainerColor], or if that is not a + * color from the theme, this will keep the same content color set above the bottom sheet. + * @param sheetTonalElevation when [sheetContainerColor] is [ColorScheme.surface], a translucent + * primary color overlay is applied on top of the container. A higher tonal elevation value will + * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. + * @param sheetShadowElevation the shadow elevation of the bottom sheet + * @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's + * input + * @param topBar top app bar of the screen, typically a [SmallTopAppBar] + * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via + * [SnackbarHostState.showSnackbar], typically a [SnackbarHost] + * @param containerColor the color used for the background of this scaffold. Use [Color.Transparent] + * to have no color. + * @param contentColor the preferred color for content inside this scaffold. Defaults to either the + * matching content color for [containerColor], or to the current [LocalContentColor] if + * [containerColor] is not a color from the theme. + * @param content content of the screen. The lambda receives a [PaddingValues] that should be + * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to + * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to + * the child of the scroll, and not on the scroll itself. + */ +@Composable +fun TangemBottomSheetScaffold( + sheetContent: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + scaffoldState: TangemBottomSheetScaffoldState = rememberTangemBottomSheetScaffoldState(), + sheetPeekHeight: Dp, + sheetMaxWidth: Dp = 640.dp, + sheetShape: Shape = TangemTheme.shapes.bottomSheetLarge, + sheetContainerColor: Color = Color.White, // FIXME + sheetContentColor: Color = contentColorFor(sheetContainerColor), + sheetTonalElevation: Dp = 0.dp, + sheetShadowElevation: Dp = 1.dp, + sheetSwipeEnabled: Boolean = true, + topBar: @Composable (() -> Unit)? = null, + snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, + containerColor: Color = TangemTheme.colors.background.secondary, + contentColor: Color = contentColorFor(containerColor), + content: @Composable (PaddingValues) -> Unit, +) { + BottomSheetScaffoldLayout( + modifier = modifier, + topBar = topBar, + body = { content(PaddingValues(bottom = sheetPeekHeight)) }, + snackbarHost = { snackbarHost(scaffoldState.snackbarHostState) }, + sheetOffset = { scaffoldState.bottomSheetState.requireOffset() }, + sheetState = scaffoldState.bottomSheetState, + containerColor = containerColor, + contentColor = contentColor, + bottomSheet = { + StandardBottomSheet( + state = scaffoldState.bottomSheetState, + peekHeight = sheetPeekHeight, + sheetMaxWidth = sheetMaxWidth, + sheetSwipeEnabled = sheetSwipeEnabled, + shape = sheetShape, + containerColor = sheetContainerColor, + contentColor = sheetContentColor, + tonalElevation = sheetTonalElevation, + shadowElevation = sheetShadowElevation, + content = sheetContent, + ) + }, + ) +} + +/** + * State of the [TangemBottomSheetScaffold] composable. + * + * @param bottomSheetState the state of the persistent bottom sheet + * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold + */ +@Stable +class TangemBottomSheetScaffoldState( + val bottomSheetState: TangemSheetState, + val snackbarHostState: SnackbarHostState, +) + +/** + * Create and [remember] a [TangemBottomSheetScaffoldState]. + * + * @param bottomSheetState the state of the standard bottom sheet. See + * [rememberTangemStandardBottomSheetState] + * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold + */ +@Composable +fun rememberTangemBottomSheetScaffoldState( + bottomSheetState: TangemSheetState = rememberTangemStandardBottomSheetState(), + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, +): TangemBottomSheetScaffoldState { + return remember(bottomSheetState, snackbarHostState) { + TangemBottomSheetScaffoldState( + bottomSheetState = bottomSheetState, + snackbarHostState = snackbarHostState, + ) + } +} + +/** + * Create and [remember] a [TangemSheetState] for [TangemBottomSheetScaffold]. + * + * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or + * [Expanded] if [skipHiddenState] is true + * @param confirmValueChange optional callback invoked to confirm or veto a pending state change + * @param [skipHiddenState] whether Hidden state is skipped for [TangemBottomSheetScaffold] + */ +@Composable +fun rememberTangemStandardBottomSheetState( + initialValue: TangemSheetValue = PartiallyExpanded, + confirmValueChange: (TangemSheetValue) -> Boolean = { true }, + skipHiddenState: Boolean = true, +) = rememberSheetState( + confirmValueChange = confirmValueChange, + initialValue = initialValue, + skipHiddenState = skipHiddenState, +) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun StandardBottomSheet( + state: TangemSheetState, + peekHeight: Dp, + sheetMaxWidth: Dp, + sheetSwipeEnabled: Boolean, + shape: Shape, + containerColor: Color, + contentColor: Color, + tonalElevation: Dp, + shadowElevation: Dp, + content: @Composable ColumnScope.() -> Unit, +) { + val scope = rememberCoroutineScope() + val orientation = Orientation.Vertical + + val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() } + val nestedScroll = + if (sheetSwipeEnabled) { + Modifier.nestedScroll( + remember(state.anchoredDraggableState) { + consumeSwipeWithinBottomSheetBoundsNestedScrollConnection( + sheetState = state, + orientation = orientation, + onFling = { scope.launch { state.settle(it) } }, + ) + }, + ) + } else { + Modifier + } + + Surface( + modifier = Modifier + .widthIn(max = sheetMaxWidth) + .fillMaxWidth() + .requiredHeightIn(min = peekHeight) + .then(nestedScroll) + .draggableAnchors( + state = state.anchoredDraggableState, + orientation = orientation, + anchors = { sheetSize, constraints -> + val layoutHeight = constraints.maxHeight.toFloat() + val sheetHeight = sheetSize.height.toFloat() + + val newAnchors = DraggableAnchors { + if (!state.skipPartiallyExpanded) { + PartiallyExpanded at (layoutHeight - peekHeightPx) + } + if (sheetHeight != peekHeightPx) { + Expanded at maxOf(layoutHeight - sheetHeight, 0f) + } + if (!state.skipHiddenState) { + Hidden at layoutHeight + } + } + val newTarget = + when (val oldTarget = state.anchoredDraggableState.targetValue) { + Hidden -> if (newAnchors.hasAnchorFor(Hidden)) Hidden else oldTarget + PartiallyExpanded -> + when { + newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded + newAnchors.hasAnchorFor(Expanded) -> Expanded + newAnchors.hasAnchorFor(Hidden) -> Hidden + else -> oldTarget + } + Expanded -> + when { + newAnchors.hasAnchorFor(Expanded) -> Expanded + newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded + newAnchors.hasAnchorFor(Hidden) -> Hidden + else -> oldTarget + } + } + newAnchors to newTarget + }, + ) + .anchoredDraggable( + state = state.anchoredDraggableState, + orientation = orientation, + enabled = sheetSwipeEnabled, + ), + shape = shape, + color = containerColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + shadowElevation = shadowElevation, + ) { + Column(Modifier.fillMaxWidth()) { + content() + } + } +} + +@Composable +private fun BottomSheetScaffoldLayout( + modifier: Modifier, + topBar: @Composable (() -> Unit)?, + body: @Composable () -> Unit, + bottomSheet: @Composable () -> Unit, + snackbarHost: @Composable () -> Unit, + sheetOffset: () -> Float, + sheetState: TangemSheetState, + containerColor: Color, + contentColor: Color, +) { + Layout( + contents = + listOf<@Composable () -> Unit>( + topBar ?: {}, + { + Surface( + modifier = modifier, + color = containerColor, + contentColor = contentColor, + content = body, + ) + }, + bottomSheet, + snackbarHost, + ), + ) { + (topBarMeasurables, bodyMeasurables, bottomSheetMeasurables, snackbarHostMeasurables), + constraints, + -> + val layoutWidth = constraints.maxWidth + val layoutHeight = constraints.maxHeight + val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + + val sheetPlaceables = bottomSheetMeasurables.fastMap { it.measure(looseConstraints) } + + val topBarPlaceables = topBarMeasurables.fastMap { it.measure(looseConstraints) } + val topBarHeight = topBarPlaceables.fastMaxOfOrNull { it.height } ?: 0 + + val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight) + val bodyPlaceables = bodyMeasurables.fastMap { it.measure(bodyConstraints) } + + val snackbarPlaceables = snackbarHostMeasurables.fastMap { it.measure(looseConstraints) } + + layout(layoutWidth, layoutHeight) { + val sheetWidth = sheetPlaceables.fastMaxOfOrNull { it.width } ?: 0 + val sheetOffsetX = Integer.max(0, (layoutWidth - sheetWidth) / 2) + + val snackbarWidth = snackbarPlaceables.fastMaxOfOrNull { it.width } ?: 0 + val snackbarHeight = snackbarPlaceables.fastMaxOfOrNull { it.height } ?: 0 + val snackbarOffsetX = (layoutWidth - snackbarWidth) / 2 + val snackbarOffsetY = + when (sheetState.currentValue) { + PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight + Expanded, + Hidden, + -> layoutHeight - snackbarHeight + } + + // Placement order is important for elevation + bodyPlaceables.fastForEach { it.placeRelative(0, topBarHeight) } + topBarPlaceables.fastForEach { it.placeRelative(0, 0) } + sheetPlaceables.fastForEach { it.placeRelative(sheetOffsetX, 0) } + snackbarPlaceables.fastForEach { it.placeRelative(snackbarOffsetX, snackbarOffsetY) } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemSheetState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemSheetState.kt new file mode 100644 index 0000000000..64dcea2dcb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemSheetState.kt @@ -0,0 +1,339 @@ +@file:Suppress("all") + +package com.tangem.core.ui.components.sheetscaffold + +import androidx.compose.animation.core.* +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.* +import kotlinx.coroutines.CancellationException + +/** + * State of a sheet composable + * + * Contains states relating to its swipe position as well as animations between state values. + * + * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large enough, + * should be skipped. If true, the sheet will always expand to the [Expanded] state and move to + * the [Hidden] state if available when hiding the sheet, either programmatically or by user + * interaction. + * @param initialValue The initial value of the state. + * @param density The density that this state can use to convert values to and from dp. + * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. + * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always + * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either + * programmatically or by user interaction. + */ +@Stable +@OptIn(ExperimentalFoundationApi::class) +class TangemSheetState( + internal val skipPartiallyExpanded: Boolean, + density: Density, + initialValue: TangemSheetValue = Hidden, + confirmValueChange: (TangemSheetValue) -> Boolean = { true }, + internal val skipHiddenState: Boolean = false, +) { + init { + if (skipPartiallyExpanded) { + require(initialValue != PartiallyExpanded) { + "The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " + + "is set to true." + } + } + if (skipHiddenState) { + require(initialValue != Hidden) { + "The initial value must not be set to Hidden if skipHiddenState is set to true." + } + } + } + + /** + * The current value of the state. + * + * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is + * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet + * was in before the swipe or animation started. + */ + val currentValue: TangemSheetValue + get() = anchoredDraggableState.currentValue + + /** + * The target value of the bottom sheet state. + * + * If a swipe is in progress, this is the value that the sheet would animate to if the swipe + * finishes. If an animation is running, this is the target value of that animation. Finally, if + * no swipe or animation is in progress, this is the same as the [currentValue]. + */ + val targetValue: TangemSheetValue + get() = anchoredDraggableState.targetValue + + /** Whether the modal bottom sheet is visible. */ + val isVisible: Boolean + get() = anchoredDraggableState.currentValue != Hidden + + /** + * Require the current offset (in pixels) of the bottom sheet. + * + * The offset will be initialized during the first measurement phase of the provided sheet + * content. + * + * These are the phases: Composition { -> Effects } -> Layout { Measurement -> Placement } -> + * Drawing + * + * During the first composition, an [IllegalStateException] is thrown. In subsequent + * compositions, the offset will be derived from the anchors of the previous pass. Always prefer + * accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next + * frame, after layout. + * + * @throws IllegalStateException If the offset has not been initialized yet + */ + fun requireOffset(): Float = anchoredDraggableState.requireOffset() + + /** Whether the sheet has an expanded state defined. */ + val hasExpandedState: Boolean + get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded) + + /** Whether the modal bottom sheet has a partially expanded state defined. */ + val hasPartiallyExpandedState: Boolean + get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded) + + /** + * Fully expand the bottom sheet with animation and suspend until it is fully expanded or + * animation has been cancelled. + * * + * + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun expand() { + anchoredDraggableState.animateTo(Expanded) + } + + /** + * Animate the bottom sheet and suspend until it is partially expanded or animation has been + * cancelled. + * + * @throws [CancellationException] if the animation is interrupted + * @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true + */ + suspend fun partialExpand() { + check(!skipPartiallyExpanded) { + "Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" + + " skipPartiallyExpanded to false to use this function." + } + animateTo(PartiallyExpanded) + } + + /** + * Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined + * else [Expanded]. + * + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun show() { + val targetValue = + when { + hasPartiallyExpandedState -> PartiallyExpanded + else -> Expanded + } + animateTo(targetValue) + } + + /** + * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has + * been cancelled. + * + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun hide() { + check(!skipHiddenState) { + "Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" + + " to false to use this function." + } + animateTo(Hidden) + } + + /** + * Animate to a [targetValue]. If the [targetValue] is not in the set of anchors, the + * [currentValue] will be updated to the [targetValue] without updating the offset. + * + * @param targetValue The target value of the animation + * @throws CancellationException if the interaction interrupted by another interaction like a + * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] + * call. + */ + internal suspend fun animateTo( + targetValue: TangemSheetValue, + velocity: Float = anchoredDraggableState.lastVelocity, + ) { + anchoredDraggableState.animateToWithDecay(targetValue, velocity) + } + + /** + * Snap to a [targetValue] without any animation. + * + * @param targetValue The target value of the animation + * @throws CancellationException if the interaction interrupted by another interaction like a + * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] + * call. + */ + internal suspend fun snapTo(targetValue: TangemSheetValue) { + anchoredDraggableState.snapTo(targetValue) + } + + /** + * Find the closest anchor taking into account the velocity and settle at it with an animation. + */ + internal suspend fun settle(velocity: Float) { + anchoredDraggableState.settle(velocity) + } + + internal var anchoredDraggableState = + AnchoredDraggableState( + initialValue = initialValue, + snapAnimationSpec = BottomSheetAnimationSpec, + decayAnimationSpec = exponentialDecay( + frictionMultiplier = 10f, + absVelocityThreshold = 0.5f, + ), + confirmValueChange = confirmValueChange, + positionalThreshold = { with(density) { 56.dp.toPx() } }, + velocityThreshold = { with(density) { 125.dp.toPx() } }, + ) + + internal val offset: Float + get() = anchoredDraggableState.offset + + companion object { + /** The default [Saver] implementation for [TangemSheetState]. */ + fun Saver( + skipPartiallyExpanded: Boolean, + confirmValueChange: (TangemSheetValue) -> Boolean, + density: Density, + skipHiddenState: Boolean, + ) = Saver( + save = { it.currentValue }, + restore = { savedValue -> + TangemSheetState( + skipPartiallyExpanded, + density, + savedValue, + confirmValueChange, + skipHiddenState, + ) + }, + ) + } +} + +/** Possible values of [TangemSheetState]. */ +enum class TangemSheetValue { + /** The sheet is not visible. */ + Hidden, + + /** The sheet is visible at full height. */ + Expanded, + + /** The sheet is partially visible. */ + PartiallyExpanded, +} + +@OptIn(ExperimentalFoundationApi::class) +internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection( + sheetState: TangemSheetState, + orientation: Orientation, + onFling: (velocity: Float) -> Unit, +): NestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.UserInput) { + sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset() + } else { + Offset.Zero + } + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return if (source == NestedScrollSource.UserInput) { + sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset() + } else { + Offset.Zero + } + } + + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = available.toFloat() + val currentOffset = sheetState.requireOffset() + val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor() + return if (toFling < 0 && currentOffset > minAnchor) { + onFling(toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + onFling(available.toFloat()) + return available + } + + private fun Float.toOffset(): Offset = Offset( + x = if (orientation == Orientation.Horizontal) this else 0f, + y = if (orientation == Orientation.Vertical) this else 0f, + ) + + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y + + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y +} + +@Composable +internal fun rememberSheetState( + skipPartiallyExpanded: Boolean = false, + confirmValueChange: (TangemSheetValue) -> Boolean = { true }, + initialValue: TangemSheetValue = Hidden, + skipHiddenState: Boolean = false, +): TangemSheetState { + val density = LocalDensity.current + return rememberSaveable( + skipPartiallyExpanded, + confirmValueChange, + skipHiddenState, + saver = + TangemSheetState.Saver( + skipPartiallyExpanded = skipPartiallyExpanded, + confirmValueChange = confirmValueChange, + density = density, + skipHiddenState = skipHiddenState, + ), + ) { + TangemSheetState( + skipPartiallyExpanded, + density, + initialValue, + confirmValueChange, + skipHiddenState, + ) + } +} + +/** The default animation spec used by [TangemSheetState]. */ +private val BottomSheetAnimationSpec: AnimationSpec = + spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium, + ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/reordarable/ReorderableItem.kt b/core/ui/src/main/java/com/tangem/core/ui/reordarable/ReorderableItem.kt new file mode 100644 index 0000000000..7df813a2e5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/reordarable/ReorderableItem.kt @@ -0,0 +1,102 @@ +package com.tangem.core.ui.reordarable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.grid.LazyGridItemScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.zIndex +import org.burnoutcrew.reorderable.ReorderableState + +/** + * Fixed version of ReorderableItem from reorderable library. + * The original version of ReorderableItem is somehow causing crashes. + * Probably because of compose compiler's bug and new foundation `animateItem` API. + * @see [org.burnoutcrew.reorderable.ReorderableItem] + */ +@Suppress("ComposableFunctionName") +@Composable +inline fun LazyItemScope.ReorderableItem( + reorderableState: ReorderableState<*>, + key: Any?, + modifier: Modifier = Modifier, + index: Int? = null, + orientationLocked: Boolean = true, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit, +) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), orientationLocked, index, content) + +/** + * Fixed version of ReorderableItem from reorderable library. + * The original version of ReorderableItem is somehow causing crashes. + * Probably because of compose compiler's bug and new foundation `animateItem` API. + * @see [org.burnoutcrew.reorderable.ReorderableItem] + */ +@Suppress("ComposableFunctionName") +@Composable +inline fun LazyGridItemScope.ReorderableItem( + reorderableState: ReorderableState<*>, + key: Any?, + modifier: Modifier = Modifier, + index: Int? = null, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit, +) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), false, index, content) + +/** + * Fixed version of ReorderableItem from reorderable library. + * The original version of ReorderableItem is somehow causing crashes. + * Probably because of compose compiler's bug and new foundation `animateItem` API. + * @see [org.burnoutcrew.reorderable.ReorderableItem] + */ +@Composable +inline fun ReorderableItem( + state: ReorderableState<*>, + key: Any?, + modifier: Modifier = Modifier, + defaultDraggingModifier: Modifier = Modifier, + orientationLocked: Boolean = true, + index: Int? = null, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit, +) { + val isDragging = if (index != null) { + index == state.draggingItemIndex + } else { + key == state.draggingItemKey + } + val draggingModifier = + if (isDragging) { + Modifier + .zIndex(1f) + .graphicsLayer { + translationX = if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f + translationY = if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f + } + } else { + val cancel = if (index != null) { + index == state.dragCancelledAnimation.position?.index + } else { + key == state.dragCancelledAnimation.position?.key + } + if (cancel) { + Modifier.zIndex(1f) + .graphicsLayer { + translationX = if (!orientationLocked || !state.isVerticalScroll) { + state.dragCancelledAnimation.offset.x + } else { + 0f + } + translationY = if (!orientationLocked || state.isVerticalScroll) { + state.dragCancelledAnimation.offset.y + } else { + 0f + } + } + } else { + defaultDraggingModifier + } + } + Box(modifier = modifier.then(draggingModifier)) { + content(isDragging) + } +} \ 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 275a6427e7..bce07d98c0 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 @@ -36,7 +36,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.disableNestedScroll import com.tangem.domain.markets.PriceChangeInterval import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart @@ -105,7 +104,6 @@ private fun Content( SpacerH4() LazyColumn( - modifier = Modifier.disableNestedScroll(), contentPadding = PaddingValues(bottom = bottomBarHeight), ) { item("header") { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 4e947ff908..e1b5444e2e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.progressbar.LinearProgressIndicator import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme @@ -119,7 +120,7 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi .fillMaxWidth(), progress = { animatedIndicatorFraction }, color = TangemTheme.colors.text.accent, - trackColor = TangemTheme.colors.background.tertiary, + backgroundColor = TangemTheme.colors.background.tertiary, strokeCap = StrokeCap.Round, ) Row( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 375d444943..9c52cb6bdb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -87,7 +87,7 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi .padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, + bottom = 8.dp, ) .onGloballyPositioned { if (it.size.height > 0) { @@ -95,7 +95,8 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi onHeaderSizeChange(it.size.height.toDp()) } } - }, + } + .padding(bottom = 4.dp), state = state.searchBar, ) Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) { 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 2674b90648..5d70d6e85e 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 @@ -17,7 +17,6 @@ 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 -import com.tangem.core.ui.utils.disableNestedScroll import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import kotlinx.coroutines.launch @@ -52,7 +51,7 @@ internal fun MarketsListLazyColumn( if (state is ListUM.Loading) { LazyColumn( - modifier = Modifier.disableNestedScroll(), + modifier = modifier, state = rememberLazyListState(), contentPadding = PaddingValues(bottom = bottomBarHeight), userScrollEnabled = false, @@ -63,7 +62,7 @@ internal fun MarketsListLazyColumn( } } else { LazyColumn( - modifier = modifier.disableNestedScroll(), + modifier = modifier, state = lazyListState, contentPadding = PaddingValues(bottom = bottomBarHeight), userScrollEnabled = true, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt index 678775213a..0a3d43118a 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt @@ -1,9 +1,6 @@ package com.tangem.feature.referral.ui -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -11,7 +8,6 @@ import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity -import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.referral.models.ReferralStateHolder @@ -24,19 +20,18 @@ internal fun ReferralBottomSheet( onDismissRequest: () -> Unit, config: ReferralStateHolder.ReferralInfoState, ) { - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } if (isVisible) { ModalBottomSheet( - modifier = Modifier.height(LocalWindowSize.current.height - statusBarHeight), + modifier = Modifier.statusBarsPadding(), onDismissRequest = onDismissRequest, sheetState = sheetState, shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, topEnd = TangemTheme.dimens.radius16, ), - windowInsets = WindowInsetsZero, + contentWindowInsets = { WindowInsetsZero }, containerColor = TangemTheme.colors.background.primary, ) { AgreementBottomSheetContent( 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 b9b3c42672..6fa945af4f 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 @@ -15,8 +15,8 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -196,7 +196,7 @@ private fun BannerBlock(onClick: () -> Unit) { .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), onClick = onClick, ), ) { @@ -258,7 +258,7 @@ private fun StakingRewardBlock( .background(TangemTheme.colors.background.action) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), enabled = rewardBlockType == RewardBlockType.Rewards, onClick = onRewardsClick, ), @@ -286,7 +286,7 @@ private fun ActiveStakingBlock( .background(TangemTheme.colors.background.action) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), enabled = balance.isClickable, onClick = { onAnalytic() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index abb57b51a7..be7909007e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -28,7 +28,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) .clickable( enabled = validatorState.isClickable, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), onClick = onClick, ), ) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 9baf09e90a..e6866c7ba9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -14,7 +14,7 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -103,7 +103,7 @@ fun TransactionCard( .height(TangemTheme.dimens.size116) .width(TangemTheme.dimens.size102) .clickable( - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), interactionSource = remember { MutableInteractionSource() }, ) { onChangeTokenClick() }, ) @@ -164,7 +164,7 @@ fun TransactionCardEmpty( .height(TangemTheme.dimens.size116) .width(TangemTheme.dimens.size102) .clickable( - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), interactionSource = remember { MutableInteractionSource() }, ) { onChangeTokenClick() }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 50ef80a078..1107eba6ae 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -5,9 +5,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -42,7 +42,7 @@ internal fun StakingBalanceBlock( .background(TangemTheme.colors.background.primary) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), onClick = state.onStakeClicked, ) .padding(TangemTheme.dimens.spacing12), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index b7df4b8f3e..17dca9d4fe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -34,6 +34,7 @@ import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.reordarable.ReorderableItem import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero @@ -43,7 +44,6 @@ import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkT import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import org.burnoutcrew.reorderable.ReorderableItem import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.rememberReorderableLazyListState import org.burnoutcrew.reorderable.reorderable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 106556de2d..fc8d3b6572 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* +import androidx.compose.material3.BottomSheetDefaults import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -44,9 +45,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.tangem.core.ui.components.BottomFade -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -54,7 +53,7 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.components.sheetscaffold.* import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -146,7 +145,7 @@ private fun WalletContent( val listState = rememberLazyListState() - val scaffoldContent: @Composable () -> Unit = { + val scaffoldContent: @Composable (PaddingValues?) -> Unit = { paddingValues -> val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) val lazyTxHistoryItems = (selectedWallet as? TxHistoryStateHolder)?.let { walletState -> @@ -165,12 +164,20 @@ private fun WalletContent( val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val marketHintAproxHeight = with(LocalDensity.current) { + TangemTheme.typography.caption2.lineHeight.toDp() * 2 + } + 40.dp + + val contentPadding = paddingValues?.let { + PaddingValues( + bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp, + ) + } ?: PaddingValues(bottom = TangemTheme.dimens.spacing92 + bottomBarHeight) + LazyColumn( modifier = Modifier.testTag(TestTags.MAIN_SCREEN), state = listState, - contentPadding = PaddingValues( - bottom = 184.dp + bottomBarHeight, - ), + contentPadding = contentPadding, horizontalAlignment = Alignment.CenterHorizontally, ) { item( @@ -262,7 +269,7 @@ private fun WalletContent( state = state, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, - content = scaffoldContent, + content = { scaffoldContent(null) }, ) } } @@ -345,29 +352,13 @@ private inline fun BaseScaffoldWithMarkets( crossinline bottomSheetContent: @Composable () -> Unit, alertConfig: WalletAlertState?, noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, - crossinline content: @Composable () -> Unit, + crossinline content: @Composable (PaddingValues) -> Unit, ) { - // show the bottom sheet if there is at least one multicurrency wallet - val showManageTokensBottomSheet = remember(state.wallets) { - state.wallets.any { it is WalletState.MultiCurrency } - } - val bottomSheetState = rememberSheetStateEnhanced( - initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden, - confirmValueChange = remember(showManageTokensBottomSheet) { - { sheetValue -> - when { - sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false - sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false - else -> true - } - } - }, - skipHiddenState = showManageTokensBottomSheet, - ) + val bottomSheetState = rememberTangemStandardBottomSheetState() - val keyboardShown = keyboardAsState() + val isKeyboardVisible by rememberIsKeyboardVisible() - val scaffoldState = rememberBottomSheetScaffoldState( + val scaffoldState = rememberTangemBottomSheetScaffoldState( bottomSheetState = bottomSheetState, snackbarHostState = snackbarHostState, ) @@ -398,13 +389,11 @@ private inline fun BaseScaffoldWithMarkets( BottomSheetStateEffects( bottomSheetState = bottomSheetState, - showManageTokensBottomSheet = showManageTokensBottomSheet, alertConfig = alertConfig, - keyboardShown = keyboardShown, onBottomSheetStateChange = onBottomSheetStateChange, ) - BottomSheetScaffold( + TangemBottomSheetScaffold( snackbarHost = { WalletSnackbarHost( snackbarHostState = it, @@ -418,27 +407,26 @@ private inline fun BaseScaffoldWithMarkets( sheetContainerColor = backgroundColor.value, scaffoldState = scaffoldState, sheetPeekHeight = peekHeight, - sheetDragHandle = { - Hand(modifier = Modifier.background(color = backgroundColor.value)) - }, sheetTonalElevation = 8.dp, sheetShadowElevation = 8.dp, + sheetShape = TangemTheme.shapes.bottomSheetLarge, sheetContent = { - Box( - modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight - handComposableComponentHeight), - ) { - bottomSheetContent() - } - // hide bottom sheet when back pressed BackHandler( - keyboardShown.value is Keyboard.Closed && - bottomSheetState.currentValue == SheetValue.Expanded, + isKeyboardVisible.not() && + bottomSheetState.currentValue == TangemSheetValue.Expanded, ) { coroutineScope.launch { bottomSheetState.partialExpand() } } + + Column( + modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight), + ) { + Hand(Modifier.drawBehind { drawRect(backgroundColor.value) }) + bottomSheetContent() + } }, - content = { _ -> + content = { paddingValues -> val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, onRefresh = { @@ -456,13 +444,11 @@ private inline fun BaseScaffoldWithMarkets( ) Column { - WalletTopBar( - config = state.topBarConfig, - ) + WalletTopBar(config = state.topBarConfig) Box( modifier = Modifier.pullRefresh(pullRefreshState), ) { - content() + content(paddingValues) WalletPullToRefreshIndicator( isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, @@ -474,7 +460,8 @@ private inline fun BaseScaffoldWithMarkets( BottomSheetScrim( color = BottomSheetDefaults.ScrimColor, - visible = bottomSheetState.targetValue == SheetValue.Expanded || state.showMarketsOnboarding, + visible = bottomSheetState.targetValue == TangemSheetValue.Expanded || + state.showMarketsOnboarding, onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, ) @@ -492,7 +479,7 @@ private inline fun BaseScaffoldWithMarkets( ) LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) { - if (state.showMarketsOnboarding && bottomSheetState.targetValue == SheetValue.Expanded) { + if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) { state.onDismissMarketsOnboarding() } } @@ -503,7 +490,7 @@ private inline fun BaseScaffoldWithMarkets( @Composable private fun MarketsTooltip( availableHeight: Dp, - bottomSheetState: SheetState, + bottomSheetState: TangemSheetState, isVisible: Boolean, modifier: Modifier = Modifier, ) { @@ -532,7 +519,7 @@ private fun MarketsTooltip( val slideOffset = 40.dp.toPx() AnimatedVisibility( - modifier = modifier.absoluteOffset(y = tooltipOffset), + modifier = modifier.offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) }, visible = visible, enter = slideIn( animationSpec = spring( @@ -560,7 +547,7 @@ internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "To add tokens pull this up or tap the search bar", + text = stringResource(R.string.markets_hint), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, @@ -647,70 +634,46 @@ private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: ( } } -@OptIn(ExperimentalMaterial3Api::class) @Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod") @Composable private fun BottomSheetStateEffects( - bottomSheetState: SheetState, - showManageTokensBottomSheet: Boolean, + bottomSheetState: TangemSheetState, alertConfig: WalletAlertState?, - keyboardShown: State, onBottomSheetStateChange: (BottomSheetState) -> Unit, ) { - // Bottom sheet during initialization internally expand partially after its content was remeasured, - // therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected - // so we have to manually restrict expansion in this case - LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) { - if (!showManageTokensBottomSheet && - (bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden) - ) { - bottomSheetState.hide() - } - } - // react to changes in wallet list - LaunchedEffect(showManageTokensBottomSheet) { - when { - showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> { - bottomSheetState.partialExpand() - } - !showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> { - bottomSheetState.hide() - } - } - } - val systemUiController = rememberSystemUiController() val navigationBarColor = TangemTheme.colors.background.primary LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) { when (bottomSheetState.targetValue) { - SheetValue.Hidden, - SheetValue.Expanded, + TangemSheetValue.Hidden, + TangemSheetValue.Expanded, -> systemUiController.setNavigationBarColor( color = Color.Transparent, darkIcons = navigationBarColor.luminance() > 0.5f, navigationBarContrastEnforced = true, ) - SheetValue.PartiallyExpanded, + TangemSheetValue.PartiallyExpanded, -> systemUiController.setNavigationBarColor(navigationBarColor) } } - DisposableEffect(showManageTokensBottomSheet) { + // make navigation bar transparent when leaving the screen + DisposableEffect(Unit) { onDispose { - if (showManageTokensBottomSheet) { - systemUiController.setNavigationBarColor( - color = Color.Transparent, - darkIcons = navigationBarColor.luminance() > 0.5f, - navigationBarContrastEnforced = false, - ) - } + systemUiController.setNavigationBarColor( + color = Color.Transparent, + darkIcons = navigationBarColor.luminance() > 0.5f, + navigationBarContrastEnforced = false, + ) } } // expand bottom sheet when keyboard appears - LaunchedEffect(keyboardShown.value is Keyboard.Opened) { - if (keyboardShown.value is Keyboard.Opened && alertConfig == null) { + val isKeyboardVisible by rememberIsKeyboardVisible() + + LaunchedEffect(isKeyboardVisible) { + if (isKeyboardVisible && alertConfig == null) { bottomSheetState.expand() } } @@ -719,8 +682,8 @@ private fun BottomSheetStateEffects( // hide keyboard when bottom sheet is about to be hidden LaunchedEffect(Unit) { snapshotFlow { - bottomSheetState.currentValue == SheetValue.Expanded && - bottomSheetState.targetValue == SheetValue.PartiallyExpanded + bottomSheetState.currentValue == TangemSheetValue.Expanded && + bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded }.collect { sheetHasBeenHidden -> if (sheetHasBeenHidden) { keyboardController?.hide() @@ -728,7 +691,7 @@ private fun BottomSheetStateEffects( } } - val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded + val isSheetHidden = bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded LaunchedEffect(isSheetHidden) { onBottomSheetStateChange( if (isSheetHidden) { @@ -740,30 +703,6 @@ private fun BottomSheetStateEffects( } } -/** - * Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718 - * Current material3 version: 1.2.0 - */ -@Composable -@ExperimentalMaterial3Api -private fun rememberSheetStateEnhanced( - skipPartiallyExpanded: Boolean = false, - confirmValueChange: (SheetValue) -> Boolean = { true }, - initialValue: SheetValue = SheetValue.Hidden, - skipHiddenState: Boolean = false, -): SheetState { - val density = LocalDensity.current - return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) { - SheetState( - skipPartiallyExpanded = skipPartiallyExpanded, - density = density, - initialValue = initialValue, - confirmValueChange = confirmValueChange, - skipHiddenState = skipHiddenState, - ) - } -} - @Composable private fun WalletSnackbarHost( snackbarHostState: SnackbarHostState, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index aa6d57aaf2..7dcb445a2f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -26,10 +26,10 @@ androidxWindowManager = "1.3.0" # region Compose compose-compiler = "1.5.9" -compose-runtime = "1.6.1" -compose-foundation = "1.6.1" -compose-material = "1.6.4" -compose-material3 = "1.2.0" +compose-runtime = "1.7.1" +compose-foundation = "1.7.1" +compose-material = "1.7.1" +compose-material3 = "1.3.0" compose-constraint = "1.0.1" compose-navigation = "2.7.7" compose-accompanist = "0.30.1" From bc7cb0abe858dd38b2be8295fe1cd6c17721e579 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 14:31:21 +0500 Subject: [PATCH 188/197] Updated on 2026-08-14 --- features/markets/impl/build.gradle.kts | 1 + .../impl/model/MarketsPortfolioModel.kt | 16 +++------ .../impl/model/TokenActionsHandler.kt | 35 +++++++++++++++++++ .../portfolio/impl/ui/WarningDialog.kt | 27 ++++++++++++++ 4 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WarningDialog.kt diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 055d5172a9..a6e62fdf7c 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) + implementation(projects.domain.demo) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) implementation(projects.domain.staking.models) 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 59a9e16805..fa57001e94 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -1,16 +1,12 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable -import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -29,6 +25,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader +import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -225,8 +222,7 @@ internal class MarketsPortfolioModel @Inject constructor( private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { val message = ContentMessage { onDismiss -> - BasicDialog( - title = resourceReference(R.string.common_warning).resolveReference(), + WarningDialog( message = when (unsupportedState) { is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( id = R.string.alert_manage_tokens_unsupported_message, @@ -240,12 +236,8 @@ internal class MarketsPortfolioModel @Inject constructor( id = R.string.alert_manage_tokens_unsupported_curve_message, formatArgs = wrappedList(unsupportedState.networkName), ) - }.resolveReference(), - confirmButton = DialogButtonUM( - title = stringResource(R.string.common_ok), - onClick = onDismiss, - ), - onDismissDialog = onDismiss, + }, + onDismiss = onDismiss, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 8b8d8ffa82..b43dc3b16f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -9,13 +9,17 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData +import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.utils.Provider import dagger.assisted.Assisted @@ -34,9 +38,18 @@ internal class TokenActionsHandler @AssistedInject constructor( private val currentAppCurrency: Provider, @Assisted("updateTokenReceiveBSConfig") private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val messageSender: UiMessageSender, ) { + private val disabledActionsInDemoMode = setOf( + TokenActionsBSContentUM.Action.Buy, + TokenActionsBSContentUM.Action.Sell, + ) + fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + if (handleDemoMode(action, cryptoCurrencyData.userWallet)) return + when (action) { TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData) TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData) @@ -48,6 +61,28 @@ internal class TokenActionsHandler @AssistedInject constructor( } } + private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet): Boolean { + val demoCard = isDemoCardUseCase.invoke(userWallet.cardId) + val needShowDemoWarning = demoCard && disabledActionsInDemoMode.contains(action) + + if (needShowDemoWarning) { + showDemoModeWarning() + } + + return needShowDemoWarning + } + + private fun showDemoModeWarning() { + val message = ContentMessage { onDismiss -> + WarningDialog( + message = resourceReference(R.string.alert_demo_feature_disabled), + onDismiss = onDismiss, + ) + } + + messageSender.send(message) + } + private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { val cryptoCurrencyStatus = cryptoCurrencyData.status val currency = cryptoCurrencyStatus.currency diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WarningDialog.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WarningDialog.kt new file mode 100644 index 0000000000..70e52d864c --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WarningDialog.kt @@ -0,0 +1,27 @@ +package com.tangem.features.markets.portfolio.impl.ui + +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.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R + +@Composable +internal fun WarningDialog( + message: TextReference, + onDismiss: () -> Unit, + title: TextReference = resourceReference(R.string.common_warning), +) { + BasicDialog( + title = title.resolveReference(), + message = message.resolveReference(), + confirmButton = DialogButtonUM( + title = stringResource(R.string.common_ok), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file From 90dfc43a903610d34ad3e2408438d13bc990e27d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 15:40:11 +0500 Subject: [PATCH 189/197] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/main/MainViewModel.kt | 2 +- .../java/com/tangem/datasource/local/logs/AppLogsStore.kt | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 375a326b2b..6df235a481 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -123,7 +123,7 @@ internal class MainViewModel @Inject constructor( private fun fetchStakingTokens() { viewModelScope.launch(dispatchers.main) { - fetchStakingTokensUseCase() + fetchStakingTokensUseCase(true) .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } .onRight { Timber.d("Staking token list was fetched successfully") } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index d3fa302f26..6bfe043b34 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -3,7 +3,9 @@ package com.tangem.datasource.local.logs import android.content.Context import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -30,7 +32,10 @@ class AppLogsStore @Inject constructor( dispatchers: CoroutineDispatcherProvider, ) { - private val scope = CoroutineScope(dispatchers.io) + private val scope = CoroutineScope( + context = SupervisorJob() + dispatchers.io + + CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") }, + ) private val mutex = Mutex() private val file = File(applicationContext.filesDir, LOG_FILE_NAME) From 272b7e245a539b64575a4643f84c7aa7bc5d0d93 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 19:14:51 +0500 Subject: [PATCH 190/197] Updated on 2026-08-14 --- .../common/ui/notifications/NotificationUM.kt | 37 +++++++++++++++- .../ui/notifications/NotificationsFactory.kt | 9 ++-- core/res/src/main/res/values-ja/strings.xml | 7 ++-- core/res/src/main/res/values-ru/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 2 + .../AddStakingNotificationsTransformer.kt | 42 +++++++++++++++++-- .../ui/block/NotificationsBlock.kt | 1 + 7 files changed, 89 insertions(+), 11 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 79c6c304fb..accc615136 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -62,7 +62,7 @@ sealed class NotificationUM(val config: NotificationConfig) { ), ) - data class ExceedsBalance( + data class TokenExceedsBalance( val networkIconId: Int, val currencyName: String, val feeName: String, @@ -97,6 +97,41 @@ sealed class NotificationUM(val config: NotificationConfig) { }, ) + data class ExceedsBalance( + val networkIconId: Int, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val mergeFeeNetworkName: Boolean = false, + val onClick: (() -> Unit)? = null, + ) : Error( + title = resourceReference( + id = R.string.warning_blocked_funds_for_fee_title, + wrappedList(feeName), + ), + subtitle = resourceReference( + id = R.string.warning_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName), + ), + iconResId = networkIconId, + buttonState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference( + R.string.common_buy_currency, + wrappedList( + if (mergeFeeNetworkName) { + "$currencyName ($feeSymbol)" + } else { + feeName + }, + ), + ), + onClick = onClick, + ) + }, + ) + data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error( title = resourceReference(R.string.send_notification_existential_deposit_title), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 526007b989..37e29e0335 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.utils.extensions.orZero import java.math.BigDecimal @Suppress("LargeClass") @@ -44,12 +45,12 @@ object NotificationsFactory { sendingAmount: BigDecimal, isSubtractionAvailable: Boolean, cryptoCurrencyStatus: CryptoCurrencyStatus, + minimumRequirement: BigDecimal? = null, ) { val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - if (!isSubtractionAvailable) return - val showNotification = sendingAmount + feeAmount > balance + val showNotification = sendingAmount + feeAmount > balance - minimumRequirement.orZero() if (showNotification) { add(NotificationUM.Error.TotalExceedsBalance) } @@ -202,7 +203,7 @@ object NotificationsFactory { when (cryptoCurrencyWarning) { is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { add( - NotificationUM.Error.ExceedsBalance( + NotificationUM.Error.TokenExceedsBalance( networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId, networkName = cryptoCurrencyWarning.coinCurrency.name, currencyName = cryptoCurrencyStatus.currency.name, @@ -219,7 +220,7 @@ object NotificationsFactory { is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { val currency = cryptoCurrencyWarning.feeCurrency add( - NotificationUM.Error.ExceedsBalance( + NotificationUM.Error.TokenExceedsBalance( networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, currencyName = cryptoCurrencyWarning.currency.name, feeName = cryptoCurrencyWarning.feeCurrencyName, diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 61aa69c13e..ace34d86b3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -348,6 +348,7 @@ 私のポートフォリオ マーケット 選択したネットワークのアドレスを生成するには、Tangemカードをスキャンする必要があります。 + トークンを追加するには、これをスワイプするか、検索バーをタップしてください。 このセクションのデータは、次のネットワークから取得されています: %s データを読み込めません… データなし @@ -627,7 +628,7 @@ **%1$s** と %2$s を送金しています。 ** %1$s ** を送信しています ネットワーク手数料は%1$sエネルギーを使用してカバーされます - %1$sを使用するとネットワーク手数料が減額されます + %1$sエネルギーを使用するとネットワーク手数料が減額されます ネットワーク手数料%1$sを含む 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 @@ -680,7 +681,7 @@ 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 資金は、21日間のロック解除期間後に使用可能になります。報酬は、ロック解除後の資金とともに引き出されます。 %sのステーキング解除期間後、資金はすぐ利用可能となります。 - 資金を引き出すことができ、すぐに使用できます + 資金を引き出して、すぐに使用できます Tronネットワークに新しいバリデーターでステーキングすると、以前にステーキングされた資金はすべてこのバリデーターに自動的に転送されます。 準備中 引き出し準備完了 @@ -709,7 +710,7 @@ %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 ce509bfb41..fff0190f6d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -853,6 +853,8 @@ Ошибка активации По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain. Отключение сети BNB Beacon Chain + Внесите немного %1$s, чтобы покрыть комиссию сети + Недостаточно средств для оплаты комиссии сети Можно лучше Нравится Понятно! diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9582c22d5f..12c3f39762 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -862,6 +862,8 @@ Activation error According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network. BNB Beacon Chain will shut down + Please deposit some %1$s to cover the network fee + Insufficient funds to cover the network fee Could be better Like it Ok, Got it! diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 35e1219e3f..ee0c1fe9a8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -3,7 +3,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification -import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification @@ -11,6 +10,7 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachable import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -20,6 +20,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -139,18 +140,20 @@ internal class AddStakingNotificationsTransformer( onReload = onReload, ) } - addExceedBalanceNotification( + addStakeExceedBalanceNotification( feeAmount = feeValue, sendingAmount = sendingAmount, + actionType = prevState.actionType, isSubtractionAvailable = isSubtractAvailable, cryptoCurrencyStatus = cryptoCurrencyStatus, + onClick = prevState.clickIntents::openTokenDetails, ) addExceedsBalanceNotification( cryptoCurrencyWarning = currencyWarning, cryptoCurrencyStatus = cryptoCurrencyStatus, shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), onClick = prevState.clickIntents::openTokenDetails, - onAnalyticsEvent = { /* [REDACTED_TODO_COMMENT] */ }, + onAnalyticsEvent = { /* no-op */ }, ) if (!BlockchainUtils.isCardano(network.id.value)) { addDustWarningNotification( @@ -210,6 +213,39 @@ internal class AddStakingNotificationsTransformer( ) } + private fun MutableList.addStakeExceedBalanceNotification( + feeAmount: BigDecimal, + sendingAmount: BigDecimal, + actionType: StakingActionCommonType, + isSubtractionAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + onClick: (CryptoCurrency) -> Unit, + ) { + val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + if (!isSubtractionAvailable) return + + val showNotification = sendingAmount + feeAmount > balance - minimumRequirement.orZero() + if (showNotification) { + val notification = if (actionType == StakingActionCommonType.ENTER) { + NotificationUM.Error.TotalExceedsBalance + } else { + with(cryptoCurrencyStatus.currency) { + NotificationUM.Error.ExceedsBalance( + networkIconId = networkIconResId, + networkName = name, + currencyName = name, + feeName = name, + feeSymbol = symbol, + mergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), + onClick = { onClick(this) }, + ) + } + } + add(notification) + } + } + private fun MutableList.addInfoNotifications(prevState: StakingUiState) { when (prevState.actionType) { StakingActionCommonType.EXIT -> addExitInfoNotifications() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index a46918a130..6b88e7ae2f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -38,6 +38,7 @@ internal fun NotificationsBlock(notifications: ImmutableList) { -> TangemTheme.colors.icon.accent is StakingNotification.Warning, + is NotificationUM.Error.TokenExceedsBalance, is NotificationUM.Error.ExceedsBalance, is NotificationUM.Warning, -> null From 243327603252c1a9b08c359dced6d7b4078766b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 19:15:40 +0500 Subject: [PATCH 191/197] Updated on 2026-08-14 --- .../staking/impl/presentation/ui/StakingScreen.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 d5ae448888..5c2e5ceb51 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 @@ -48,7 +48,7 @@ internal fun StakingScreen(uiState: StakingUiState) { .systemBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { - SendAppBar( + StakingAppBar( uiState = uiState, ) StakingScreenContent( @@ -85,25 +85,25 @@ fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { } @Composable -private fun SendAppBar(uiState: StakingUiState) { - val backIcon = when (uiState.currentStep) { +private fun StakingAppBar(uiState: StakingUiState) { + val (backIcon, click) = when (uiState.currentStep) { StakingStep.Amount, - StakingStep.Validators, StakingStep.Confirmation, -> { - R.drawable.ic_close_24 + R.drawable.ic_close_24 to uiState.clickIntents::onBackClick } + StakingStep.Validators, StakingStep.RewardsValidators, StakingStep.InitialInfo, -> { - R.drawable.ic_back_24 + R.drawable.ic_back_24 to uiState.clickIntents::onPrevClick } } AppBarWithBackButtonAndIcon( text = uiState.title.resolveReference(), subtitle = uiState.subtitle?.resolveReference(), backIconRes = backIcon, - onBackClick = uiState.clickIntents::onBackClick, + onBackClick = click, backgroundColor = TangemTheme.colors.background.secondary, modifier = Modifier.height(TangemTheme.dimens.size56), ) From 901e46800e90291a7277a4e124e516af61bd1e09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 19:15:46 +0500 Subject: [PATCH 192/197] Updated on 2026-08-14 --- .../state/transformers/AddStakingNotificationsTransformer.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index ee0c1fe9a8..f4f4da10b4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -116,7 +116,9 @@ internal class AddStakingNotificationsTransformer( confirmationState = confirmationState.copy( notifications = notifications.toImmutableList(), isPrimaryButtonEnabled = notifications.none { - it is StakingNotification.Error || it is NotificationUM.Error + it is StakingNotification.Error || + it is NotificationUM.Error || + it is NotificationUM.Warning.NetworkFeeUnreachable }, ), ) From cb9e07b46deecdcf19b77199cd253cc22e1e089c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 19:15:51 +0500 Subject: [PATCH 193/197] Updated on 2026-08-14 --- .../list/RoundedListWithDividers.kt | 10 +- .../SetInitialDataStateTransformer.kt | 1 + .../viewmodel/StakingViewModel.kt | 135 +++++++++--------- 3 files changed, 78 insertions(+), 68 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt index 87640bcc2a..afa890cb01 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.rows.CornersToRound @@ -64,6 +65,11 @@ fun LazyListScope.roundedListWithDividersItems( endText = row.endText.orMaskWithStars(hideEndText && row.isEndTextHideable).resolveReference(), cornersToRound = getCornersToRound(index, rows.size), iconClick = row.iconClick, + endTextColor = if (row.isEndTextHighlighted) { + TangemTheme.colors.text.accent + } else { + TangemTheme.colors.text.tertiary + }, showDivider = index < rows.lastIndex, ) } @@ -81,6 +87,7 @@ private fun InitialInfoContentRow( endText: String, cornersToRound: CornersToRound, showDivider: Boolean, + endTextColor: Color = TangemTheme.colors.text.tertiary, iconClick: (() -> Unit)? = null, ) { Box { @@ -89,7 +96,7 @@ private fun InitialInfoContentRow( startTextColor = TangemTheme.colors.text.primary1, startTextStyle = TangemTheme.typography.body2, endText = endText, - endTextColor = TangemTheme.colors.text.tertiary, + endTextColor = endTextColor, endTextStyle = TangemTheme.typography.body2, cornersToRound = cornersToRound, iconResId = R.drawable.ic_information_24, @@ -126,6 +133,7 @@ data class RoundedListWithDividersItemData( val id: Int, val startText: TextReference, val endText: TextReference, + val isEndTextHighlighted: Boolean = false, val iconClick: (() -> Unit)? = null, val isEndTextHideable: Boolean = false, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 58d6743dbd..c2a2c681a5 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 @@ -118,6 +118,7 @@ internal class SetInitialDataStateTransformer( startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), endText = getAprRange(validators), iconClick = { clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE) }, + isEndTextHighlighted = true, ) } 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 e23576a2b7..cebace0c6e 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 @@ -66,6 +66,7 @@ import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -101,6 +102,7 @@ internal class StakingViewModel @Inject constructor( private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory, private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, private val analyticsEventHandler: AnalyticsEventHandler, + @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -174,7 +176,8 @@ internal class StakingViewModel @Inject constructor( private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() private var approvalJobHolder: JobHolder = JobHolder() - private var updateBalancesJobHolder: JobHolder = JobHolder() + private var feeJobHolder: JobHolder = JobHolder() + private var sendTransactionJobHolder = JobHolder() init { subscribeOnSelectedAppCurrency() @@ -184,8 +187,9 @@ internal class StakingViewModel @Inject constructor( override fun onCleared() { super.onCleared() - updateBalancesJobHolder.cancel() approvalJobHolder.cancel() + feeJobHolder.cancel() + sendTransactionJobHolder.cancel() } override fun onBackClick() { @@ -258,7 +262,7 @@ internal class StakingViewModel @Inject constructor( updateNotifications() }, ) - } + }.saveIn(feeJobHolder) } private fun handleOnNextConfirmationClick() { @@ -287,7 +291,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateResetAssentTransformer) }, ) - } + }.saveIn(sendTransactionJobHolder) } } @@ -313,12 +317,11 @@ internal class StakingViewModel @Inject constructor( override fun onRefreshSwipe(isRefreshing: Boolean) { stateController.update(SetInitialLoadingStateTransformer(isRefreshing)) - viewModelScope.launch { + coroutineScope.launch { balanceUpdater.instantUpdate() - }.saveIn(updateBalancesJobHolder) - .invokeOnCompletion { - stateController.update(SetInitialLoadingStateTransformer(false)) - } + }.invokeOnCompletion { + stateController.update(SetInitialLoadingStateTransformer(false)) + } } override fun onInitialInfoBannerClick() { @@ -393,7 +396,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatus.currency.network.id.value, activeStake.pendingActions, ) - val isMultiplePendingActions = activeStake.pendingActions.isSingleItem().not() + val isMultiplePendingActions = activeStake.pendingActions.size > 1 if (isMultiplePendingActions && !isAllWithdrawActions) { stateController.update( ShowActionSelectorBottomSheetTransformer( @@ -701,65 +704,63 @@ internal class StakingViewModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - viewModelScope.launch { - getUserWalletUseCase(userWalletId).fold( - ifRight = { wallet -> - userWallet = wallet - }, - ifLeft = { - Timber.e(it.toString()) - stakingEventFactory.createGenericErrorAlert(it.toString()) - stateController.update(SetConfirmationStateResetAssentTransformer) - }, - ) - getCurrencyStatusUpdatesUseCase(userWalletId, cryptoCurrencyId, false) - .conflate() - .distinctUntilChanged() - .filter { value.currentStep == StakingStep.InitialInfo } - .onEach { maybeStatus -> - maybeStatus.fold( - ifRight = { status -> - if (!isInitialInfoAnalyticSent) { - isInitialInfoAnalyticSent = true - val balances = status.value.yieldBalance as? YieldBalance.Data - analyticsEventHandler.send( - StakingAnalyticsEvents.StakingInfoScreenOpened( - validatorsCount = balances?.getValidatorsCount() ?: 0, - token = status.currency.symbol, - ), - ) - } - - feeCryptoCurrencyStatus = - getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() - cryptoCurrencyStatus = status - val isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false - - setupApprovalNeeded() - checkIfSubtractAvailable() - - stateController.update( - transformer = SetInitialDataStateTransformer( - clickIntents = this@StakingViewModel, - yield = yield, - isAnyTokenStaked = isAnyTokenStaked, - isApprovalNeeded = stakingApproval is StakingApproval.Needed, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, + getUserWalletUseCase(userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + }, + ifLeft = { + Timber.e(it.toString()) + stakingEventFactory.createGenericErrorAlert(it.toString()) + stateController.update(SetConfirmationStateResetAssentTransformer) + }, + ) + getCurrencyStatusUpdatesUseCase(userWalletId, cryptoCurrencyId, false) + .conflate() + .distinctUntilChanged() + .filter { value.currentStep == StakingStep.InitialInfo } + .onEach { maybeStatus -> + maybeStatus.fold( + ifRight = { status -> + if (!isInitialInfoAnalyticSent) { + isInitialInfoAnalyticSent = true + val balances = status.value.yieldBalance as? YieldBalance.Data + analyticsEventHandler.send( + StakingAnalyticsEvents.StakingInfoScreenOpened( + validatorsCount = balances?.getValidatorsCount() ?: 0, + token = status.currency.symbol, ), ) - }, - ifLeft = { error -> - Timber.e(error.toString()) - stakingEventFactory.createGenericErrorAlert(error.toString()) - stateController.update(SetConfirmationStateResetAssentTransformer) - }, - ) - } - .flowOn(dispatchers.main) - .launchIn(viewModelScope) - } + } + + feeCryptoCurrencyStatus = + getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() + cryptoCurrencyStatus = status + val isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false + + setupApprovalNeeded() + checkIfSubtractAvailable() + + stateController.update( + transformer = SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + isAnyTokenStaked = isAnyTokenStaked, + isApprovalNeeded = stakingApproval is StakingApproval.Needed, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + ), + ) + }, + ifLeft = { error -> + Timber.e(error.toString()) + stakingEventFactory.createGenericErrorAlert(error.toString()) + stateController.update(SetConfirmationStateResetAssentTransformer) + }, + ) + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) } private fun subscribeOnBalanceHiding() { From 2059c817fa32e9e28347e2e5c5cde3f8dc0abf68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 17:48:58 +0300 Subject: [PATCH 194/197] Updated on 2026-08-14 --- .../transactions/state/TransactionState.kt | 6 +- .../ic_transaction_history_staking.xml | 9 +++ .../ic_transaction_history_unstaking.xml | 10 +++ .../SdkTransactionHistoryItemConverter.kt | 12 ++-- .../utils/SdkTransactionTypeConverter.kt | 38 +++++++----- .../domain/txhistory/models/TxHistoryItem.kt | 22 ++++--- ...tailsTxHistoryTransactionStateConverter.kt | 61 +++++++++++++------ .../converter/TxHistoryItemStateConverter.kt | 61 +++++++++++++------ 8 files changed, 146 insertions(+), 73 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_transaction_history_staking.xml create mode 100644 core/ui/src/main/res/drawable/ic_transaction_history_unstaking.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt index 7df1982a29..57a1fd52ff 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt @@ -36,9 +36,9 @@ sealed interface TransactionState { ) : TransactionState { sealed class Status { - object Failed : Status() - object Confirmed : Status() - object Unconfirmed : Status() + data object Failed : Status() + data object Confirmed : Status() + data object Unconfirmed : Status() } enum class Direction { diff --git a/core/ui/src/main/res/drawable/ic_transaction_history_staking.xml b/core/ui/src/main/res/drawable/ic_transaction_history_staking.xml new file mode 100644 index 0000000000..008a7841c7 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_transaction_history_staking.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_transaction_history_unstaking.xml b/core/ui/src/main/res/drawable/ic_transaction_history_unstaking.xml new file mode 100644 index 0000000000..3b3961cf9a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_transaction_history_unstaking.xml @@ -0,0 +1,10 @@ + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 34200f0d8c..0df50f5899 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -54,17 +54,13 @@ internal class SdkTransactionHistoryItemConverter( } else { mapToInteractionAddressType(sourceType = sourceType) } + is SdkTransactionHistoryItem.TransactionType.TronStakingTransactionType -> { + TxHistoryItem.InteractionAddressType.Staking + } + is SdkTransactionHistoryItem.TransactionType.ContractMethod, is SdkTransactionHistoryItem.TransactionType.ContractMethodName, -> mapToInteractionAddressType(destinationType) - TransactionHistoryItem.TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> - mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] - TransactionHistoryItem.TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> - mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] - TransactionHistoryItem.TransactionType.TronStakingTransactionType.VoteWitnessContract -> - mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] - TransactionHistoryItem.TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> - mapToInteractionAddressType(destinationType) // todo map correct [REDACTED_JIRA] } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt index e061f80d14..e62cd5b7eb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -1,29 +1,37 @@ package com.tangem.domain.walletmanager.utils -import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem +import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem.TransactionType import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.SmartContractMethod import com.tangem.utils.converter.Converter internal class SdkTransactionTypeConverter( private val smartContractMethods: Map, -) : Converter { +) : Converter { - override fun convert(value: TransactionHistoryItem.TransactionType): TxHistoryItem.TransactionType { + override fun convert(value: TransactionType): TxHistoryItem.TransactionType { return when (value) { - is TransactionHistoryItem.TransactionType.ContractMethod -> + is TransactionType.ContractMethod -> { getTransactionType(methodName = smartContractMethods[value.id]?.name) - is TransactionHistoryItem.TransactionType.ContractMethodName -> getTransactionType(methodName = value.name) - is TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer - TransactionHistoryItem.TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> - TxHistoryItem.TransactionType.Transfer // TODO map correct [REDACTED_JIRA] - TransactionHistoryItem.TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> - TxHistoryItem.TransactionType.Transfer // TODO map correct [REDACTED_JIRA] - TransactionHistoryItem.TransactionType.TronStakingTransactionType.VoteWitnessContract -> - TxHistoryItem - .TransactionType.Transfer // TODO map correct [REDACTED_JIRA] - TransactionHistoryItem.TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> - TxHistoryItem.TransactionType.Transfer // TODO map correct [REDACTED_JIRA] + } + is TransactionType.ContractMethodName -> { + getTransactionType(methodName = value.name) + } + is TransactionType.Transfer -> { + TxHistoryItem.TransactionType.Transfer + } + is TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> { + TxHistoryItem.TransactionType.TronStakingTransactionType.Stake + } + is TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> { + TxHistoryItem.TransactionType.TronStakingTransactionType.Unstake + } + is TransactionType.TronStakingTransactionType.VoteWitnessContract -> { + TxHistoryItem.TransactionType.TronStakingTransactionType.Vote + } + is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> { + TxHistoryItem.TransactionType.TronStakingTransactionType.Withdraw + } } } diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt index beaa4a031b..68677bdd63 100644 --- a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt @@ -33,20 +33,28 @@ data class TxHistoryItem( } sealed interface TransactionType { - object Transfer : TransactionType - object Approve : TransactionType - object Swap : TransactionType - object UnknownOperation : TransactionType + data object Transfer : TransactionType + data object Approve : TransactionType + data object Swap : TransactionType + data object UnknownOperation : TransactionType data class Operation(val name: String) : TransactionType + + sealed interface TronStakingTransactionType : TransactionType { + data object Vote : TronStakingTransactionType + data object Withdraw : TronStakingTransactionType + data object Stake : TronStakingTransactionType + data object Unstake : TronStakingTransactionType + } } sealed class TransactionStatus { - object Failed : TransactionStatus() - object Unconfirmed : TransactionStatus() - object Confirmed : TransactionStatus() + data object Failed : TransactionStatus() + data object Unconfirmed : TransactionStatus() + data object Confirmed : TransactionStatus() } sealed class InteractionAddressType { + data object Staking : InteractionAddressType() data class User(val address: String) : InteractionAddressType() data class Contract(val address: String) : InteractionAddressType() data class Multiple(val addresses: List) : InteractionAddressType() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index 3aacd1221a..5eea986141 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory +import com.tangem.common.extensions.isZero import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Direction import com.tangem.core.ui.extensions.TextReference @@ -8,6 +9,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryItem.* import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.StringsSigns.MINUS @@ -42,34 +44,44 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( ) } - private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) { + private fun TxHistoryItem.extractIcon(): Int = if (status == TransactionStatus.Failed) { R.drawable.ic_close_24 } else { when (type) { - is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24 - is TxHistoryItem.TransactionType.Operation, - is TxHistoryItem.TransactionType.Swap, - is TxHistoryItem.TransactionType.Transfer, - is TxHistoryItem.TransactionType.UnknownOperation, + is TransactionType.Approve -> R.drawable.ic_doc_24 + is TransactionType.TronStakingTransactionType.Stake, + is TransactionType.TronStakingTransactionType.Vote, + -> R.drawable.ic_transaction_history_staking + is TransactionType.TronStakingTransactionType.Withdraw, + is TransactionType.TronStakingTransactionType.Unstake, + -> R.drawable.ic_transaction_history_unstaking + is TransactionType.Operation, + is TransactionType.Swap, + is TransactionType.Transfer, + is TransactionType.UnknownOperation, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } } private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) { - is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval) - is TxHistoryItem.TransactionType.Operation -> stringReference(type.name) - is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap) - is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) + is TransactionType.Approve -> resourceReference(R.string.common_approval) + is TransactionType.Operation -> stringReference(type.name) + is TransactionType.Swap -> resourceReference(R.string.common_swap) + is TransactionType.Transfer -> resourceReference(R.string.common_transfer) + is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) + is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) + is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) + is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) + is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw) } private fun TxHistoryItem.extractSubtitle(): TextReference = when (val interactionAddress = interactionAddressType) { - is TxHistoryItem.InteractionAddressType.Contract -> resourceReference( + is InteractionAddressType.Contract -> resourceReference( id = R.string.transaction_history_contract_address, formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) - is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference( + is InteractionAddressType.Multiple -> resourceReference( id = if (isOutgoing) { R.string.transaction_history_transaction_to_address } else { @@ -77,7 +89,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( }, formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)), ) - is TxHistoryItem.InteractionAddressType.User -> resourceReference( + is InteractionAddressType.User -> resourceReference( id = if (isOutgoing) { R.string.transaction_history_transaction_to_address } else { @@ -85,19 +97,28 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( }, formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) + is InteractionAddressType.Staking -> resourceReference( + id = R.string.common_staking, + ) } private fun TxHistoryItem.extractDirection() = if (isOutgoing) Direction.OUTGOING else Direction.INCOMING - private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) { - TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed - TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed - TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed + private fun TransactionStatus.tiUiStatus() = when (this) { + TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed + TransactionStatus.Failed -> TransactionState.Content.Status.Failed + TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed } private fun TxHistoryItem.getAmount(): String { - val prefix = when (status) { - TxHistoryItem.TransactionStatus.Failed -> "" + if (type == TransactionType.TronStakingTransactionType.Vote || + type == TransactionType.TronStakingTransactionType.Withdraw + ) { + return "" + } + val prefix = when { + status == TransactionStatus.Failed -> "" + this.amount.isZero() -> "" else -> if (isOutgoing) MINUS else PLUS } return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index 45a7931776..f37407d469 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.common.extensions.isZero import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -7,6 +8,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryItem.* import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.StringsSigns.MINUS @@ -41,34 +43,44 @@ internal class TxHistoryItemStateConverter( ) } - private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) { + private fun TxHistoryItem.extractIcon(): Int = if (status == TransactionStatus.Failed) { R.drawable.ic_close_24 } else { when (type) { - is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24 - is TxHistoryItem.TransactionType.Operation, - is TxHistoryItem.TransactionType.Swap, - is TxHistoryItem.TransactionType.Transfer, - is TxHistoryItem.TransactionType.UnknownOperation, + is TransactionType.Approve -> R.drawable.ic_doc_24 + is TransactionType.TronStakingTransactionType.Stake, + is TransactionType.TronStakingTransactionType.Vote, + -> R.drawable.ic_transaction_history_staking + is TransactionType.TronStakingTransactionType.Withdraw, + is TransactionType.TronStakingTransactionType.Unstake, + -> R.drawable.ic_transaction_history_unstaking + is TransactionType.Operation, + is TransactionType.Swap, + is TransactionType.Transfer, + is TransactionType.UnknownOperation, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } } private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) { - is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval) - is TxHistoryItem.TransactionType.Operation -> stringReference(type.name) - is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap) - is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) + is TransactionType.Approve -> resourceReference(R.string.common_approval) + is TransactionType.Operation -> stringReference(type.name) + is TransactionType.Swap -> resourceReference(R.string.common_swap) + is TransactionType.Transfer -> resourceReference(R.string.common_transfer) + is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) + is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) + is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) + is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } private fun TxHistoryItem.extractSubtitle(): TextReference = when (val interactionAddress = interactionAddressType) { - is TxHistoryItem.InteractionAddressType.Contract -> resourceReference( + is InteractionAddressType.Contract -> resourceReference( id = R.string.transaction_history_contract_address, formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) - is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference( + is InteractionAddressType.Multiple -> resourceReference( id = if (isOutgoing) { R.string.transaction_history_transaction_to_address } else { @@ -76,7 +88,7 @@ internal class TxHistoryItemStateConverter( }, formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)), ) - is TxHistoryItem.InteractionAddressType.User -> resourceReference( + is InteractionAddressType.User -> resourceReference( id = if (isOutgoing) { R.string.transaction_history_transaction_to_address } else { @@ -84,20 +96,29 @@ internal class TxHistoryItemStateConverter( }, formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) + is InteractionAddressType.Staking -> resourceReference( + id = R.string.common_staking, + ) } private fun TxHistoryItem.extractDirection() = if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING - private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) { - TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed - TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed - TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed + private fun TransactionStatus.tiUiStatus() = when (this) { + TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed + TransactionStatus.Failed -> TransactionState.Content.Status.Failed + TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed } private fun TxHistoryItem.getAmount(): String { - val prefix = when (status) { - TxHistoryItem.TransactionStatus.Failed -> "" + if (type == TransactionType.TronStakingTransactionType.Vote || + type == TransactionType.TronStakingTransactionType.Withdraw + ) { + return "" + } + val prefix = when { + status == TransactionStatus.Failed -> "" + this.amount.isZero() -> "" else -> if (isOutgoing) MINUS else PLUS } return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) From b32cc4a92875995cbae6898b72e92ce8c6789781 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 17:59:04 +0300 Subject: [PATCH 195/197] Updated on 2026-08-14 --- .../response/TokenMarketListResponse.kt | 55 ++++++++----------- .../core/ui/utils/BigDecimalFormatter.kt | 43 +++++++-------- .../converters/TokenMarketListConverter.kt | 3 +- .../com/tangem/domain/markets/TokenMarket.kt | 1 + .../impl/model/formatter/Formatters.kt | 6 +- .../converters/MarketsTokenItemConverter.kt | 12 +--- 6 files changed, 49 insertions(+), 71 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index a0f287971f..3dbf406f16 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -1,45 +1,36 @@ package com.tangem.datasource.api.markets.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class TokenMarketListResponse( - @Json(name = "imageHost") - val imageHost: String?, - @Json(name = "tokens") - val tokens: List, - @Json(name = "total") - val total: Int, - @Json(name = "limit") - val limit: Int, - @Json(name = "offset") - val offset: Int, - @Json(name = "timestamp") - val timestamp: Long? = null, + @Json(name = "imageHost") val imageHost: String?, + @Json(name = "tokens") val tokens: List, + @Json(name = "total") val total: Int, + @Json(name = "limit") val limit: Int, + @Json(name = "offset") val offset: Int, + @Json(name = "timestamp") val timestamp: Long? = null, ) { + + @JsonClass(generateAdapter = true) data class Token( - @Json(name = "id") - val id: String, - @Json(name = "name") - val name: String, - @Json(name = "symbol") - val symbol: String, - @Json(name = "current_price") - val currentPrice: BigDecimal, - @Json(name = "price_change_percentage") - val priceChangePercentage: PriceChangePercentage, - @Json(name = "market_rating") - val marketRating: Int?, - @Json(name = "market_cap") - val marketCap: BigDecimal?, + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "current_price") val currentPrice: BigDecimal, + @Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage, + @Json(name = "market_rating") val marketRating: Int?, + @Json(name = "market_cap") val marketCap: BigDecimal?, + @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, ) { + + @JsonClass(generateAdapter = true) data class PriceChangePercentage( - @Json(name = "24h") - val h24: BigDecimal, - @Json(name = "1w") - val week1: BigDecimal, - @Json(name = "30d") - val day30: BigDecimal, + @Json(name = "24h") val h24: BigDecimal, + @Json(name = "1w") val week1: BigDecimal, + @Json(name = "30d") val day30: BigDecimal, ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 16386f4fa3..e6944920a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -27,13 +27,7 @@ object BigDecimalFormatter { private const val FIAT_MARKET_DEFAULT_DIGITS = 2 private const val FIAT_MARKET_EXTENDED_DIGITS = 6 - - private val bigDecimal01 = BigDecimal("0.1") - private val bigDecimal001 = BigDecimal("0.01") - private val bigDecimal0001 = BigDecimal("0.001") - private val bigDecimal00001 = BigDecimal("0.0001") - private val bigDecimal000001 = BigDecimal("0.00001") - private val bigDecimal0000001 = BigDecimal("0.000001") + private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 fun formatCryptoAmount( cryptoAmount: BigDecimal?, @@ -237,19 +231,34 @@ object BigDecimalFormatter { if (fiatAmount == null) return EMPTY_BALANCE_SIGN val formatterCurrency = getCurrency(fiatCurrencyCode) - val decimals = getProperFiatPriceDecimals(fiatAmount) + val (formattedAmount, finalScale) = getFiatPriceUncappedWithScale(value = fiatAmount) val formatter = NumberFormat.getCurrencyInstance(locale).apply { currency = formatterCurrency - maximumFractionDigits = decimals + maximumFractionDigits = finalScale minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS roundingMode = RoundingMode.HALF_UP } - return formatter.format(fiatAmount) + return formatter.format(formattedAmount) .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } + fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair { + return if (value < BigDecimal.ONE) { + val leadingZeroes = value.scale() - value.precision() + val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES + + val amount = value + .setScale(scale, RoundingMode.HALF_UP) + .stripTrailingZeros() + + amount to amount.scale() + } else { + value to FIAT_MARKET_DEFAULT_DIGITS + } + } + fun formatFiatEditableAmount( fiatAmount: String?, fiatCurrencyCode: String, @@ -418,18 +427,4 @@ object BigDecimalFormatter { private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD - - @Suppress("MagicNumber") - fun getProperFiatPriceDecimals(price: BigDecimal): Int { - return when { - price >= BigDecimal.ONE -> 2 - price >= bigDecimal01 -> 3 - price >= bigDecimal001 -> 4 - price >= bigDecimal0001 -> 6 - price >= bigDecimal00001 -> 8 - price >= bigDecimal000001 -> 10 - price >= bigDecimal0000001 -> 12 - else -> price.stripTrailingZeros().scale() - } - } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 72aae03ec4..c4836449f3 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -23,6 +23,7 @@ internal object TokenMarketListConverter : Converter current -> PriceChangeType.UP diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index 0a482edd91..b59acbc16d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -1,8 +1,8 @@ package com.tangem.features.markets.tokenlist.impl.model.converters -import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter @@ -34,7 +34,7 @@ internal class MarketsTokenItemConverter( trendPercentText = value.getTrendPercent(), trendType = value.getTrendType(), chardData = value.getChartData(), - isUnder100kMarketCap = value.isUnder100kMarketCap(), + isUnder100kMarketCap = value.isUnderMarketCapLimit, ) } @@ -149,12 +149,4 @@ internal class MarketsTokenItemConverter( useAbsoluteValue = true, ) } - - private fun TokenMarket.isUnder100kMarketCap(): Boolean { - return marketCap?.let { it < decimal100k } ?: true - } - - private companion object { - val decimal100k: BigDecimal = BigDecimal.valueOf(100_000) - } } \ No newline at end of file From 2ba0f3d4a11327bbdb97b05562a0ec1450a45ac4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 18:24:55 +0300 Subject: [PATCH 196/197] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 6 ++ .../com/tangem/common/routing/AppRoute.kt | 10 ++- .../response/TokenMarketChartResponse.kt | 2 + .../response/TokenMarketInfoResponse.kt | 12 ++++ data/markets/build.gradle.kts | 2 + .../markets/DefaultMarketsTokenRepository.kt | 59 +++++++++++++++-- .../data/markets/MarketsBatchUpdateFetcher.kt | 35 +++++++--- .../analytics/MarketsDataAnalyticsEvent.kt | 42 ++++++++++++ .../data/markets/di/MarketsDataModule.kt | 3 + .../markets/GetTokenMarketInfoUseCase.kt | 7 +- .../markets/GetTokenPriceChartUseCase.kt | 3 + .../repositories/MarketsTokenRepository.kt | 21 +++++- .../details/MarketsTokenDetailsComponent.kt | 7 ++ .../token/block/TokenMarketBlockComponent.kt | 6 +- features/markets/impl/build.gradle.kts | 2 + .../DefaultMarketsTokenDetailsComponent.kt | 22 ++++++- .../analytics/MarketDetailsAnalyticsEvent.kt | 64 +++++++++++++++++++ .../impl/model/MarketsTokenDetailsModel.kt | 49 +++++++++++++- .../model/converters/DescriptionConverter.kt | 1 + .../model/converters/InsightsConverter.kt | 3 + .../impl/model/converters/LinksConverter.kt | 4 +- .../converters/PricePerformanceConverter.kt | 3 + .../converters/TokenMarketInfoConverter.kt | 14 +++- .../impl/model/state/QuotesStateUpdater.kt | 7 +- .../impl/ui/components/InfoBottomSheet.kt | 12 ++-- .../impl/ui/components/InsightsBlock.kt | 6 +- .../details/impl/ui/components/LinksBlock.kt | 14 ++-- .../ui/components/PricePerformanceBlock.kt | 6 +- .../impl/ui/state/InfoBottomSheetContent.kt | 1 + .../details/impl/ui/state/InsightsUM.kt | 2 + .../markets/details/impl/ui/state/LinksUM.kt | 3 +- .../impl/ui/state/PricePerformanceUM.kt | 2 + .../impl/DefaultMarketsEntryComponent.kt | 4 ++ .../api/MarketsPortfolioComponent.kt | 10 ++- .../impl/analytics/PortfolioAnalyticsEvent.kt | 50 +++++++++++++++ .../impl/model/MarketsPortfolioModel.kt | 45 ++++++++++++- .../impl/model/TokenActionsHandler.kt | 20 ++++-- .../block/impl/model/TokenMarketBlockModel.kt | 23 +++++-- .../analytics/MarketsListAnalyticsEvent.kt | 34 ++++++++++ .../tokenlist/impl/model/MarketsListModel.kt | 25 +++++++- .../presentation/TokenDetailsFragment.kt | 10 +-- .../WalletCurrencyActionsClickIntents.kt | 4 ++ 42 files changed, 581 insertions(+), 74 deletions(-) create mode 100644 data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt 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 f33cf4b1af..6d8ea8f746 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 @@ -194,6 +194,12 @@ internal class ChildFactory @Inject constructor( token = route.token, appCurrency = route.appCurrency, showPortfolio = route.showPortfolio, + analyticsParams = route.analyticsParams?.let { + MarketsTokenDetailsComponent.AnalyticsParams( + blockchain = it.blockchain, + source = it.source, + ) + }, ), componentFactory = marketsTokenDetailsComponentFactory, ) 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 2362e15686..0d8cee2df5 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 @@ -272,5 +272,13 @@ sealed class AppRoute(val path: String) : Route { val token: TokenMarketParams, val appCurrency: AppCurrency, val showPortfolio: Boolean, - ) : AppRoute(path = "/markets_token_details/${token.id}/$showPortfolio") + val analyticsParams: AnalyticsParams? = null, + ) : AppRoute(path = "/markets_token_details/${token.id}/$showPortfolio") { + + @Serializable + data class AnalyticsParams( + val blockchain: String?, + val source: String, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt index 777bbc1228..fbe921d20b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.markets.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class TokenMarketChartResponse( @Json(name = "prices") val prices: Map, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 13e4a315e1..68c54776fa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.markets.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class TokenMarketInfoResponse( @Json(name = "id") val id: String, @@ -30,6 +32,7 @@ data class TokenMarketInfoResponse( val pricePerformance: PricePerformance?, ) { + @JsonClass(generateAdapter = true) data class PriceChangePercentage( @Json(name = "24h") val day: BigDecimal?, @@ -47,6 +50,7 @@ data class TokenMarketInfoResponse( val allTime: BigDecimal?, ) + @JsonClass(generateAdapter = true) data class Network( @Json(name = "network_id") val networkId: String, @@ -58,6 +62,7 @@ data class TokenMarketInfoResponse( val decimalCount: Int?, ) + @JsonClass(generateAdapter = true) data class Insights( @Json(name = "holders_change") val holdersChange: Change?, @@ -70,6 +75,7 @@ data class TokenMarketInfoResponse( @Json(name = "networks") val sourceNetworks: List?, ) { + @JsonClass(generateAdapter = true) data class SourceNetwork( @Json(name = "network_id") val id: String, @@ -78,6 +84,7 @@ data class TokenMarketInfoResponse( ) } + @JsonClass(generateAdapter = true) data class Change( @Json(name = "24h") val day: BigDecimal?, @@ -87,6 +94,7 @@ data class TokenMarketInfoResponse( val month: BigDecimal?, ) + @JsonClass(generateAdapter = true) data class Metrics( @Json(name = "market_rating") val marketRating: Int?, @@ -102,6 +110,7 @@ data class TokenMarketInfoResponse( val fullyDilutedValuation: BigDecimal?, ) + @JsonClass(generateAdapter = true) data class Links( @Json(name = "official_links") val officialLinks: List? = null, @@ -113,6 +122,7 @@ data class TokenMarketInfoResponse( val blockchainSite: List? = null, ) + @JsonClass(generateAdapter = true) data class Link( @Json(name = "title") val title: String, @@ -122,6 +132,7 @@ data class TokenMarketInfoResponse( val link: String, ) + @JsonClass(generateAdapter = true) data class PricePerformance( @Json(name = "24h") val day: Range?, @@ -131,6 +142,7 @@ data class TokenMarketInfoResponse( val allTime: Range?, ) + @JsonClass(generateAdapter = true) data class Range( @Json(name = "low_price") val low: BigDecimal?, diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 7e23136300..5378b4b964 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) implementation(projects.core.pagination) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) implementation(projects.domain.legacy) implementation(projects.domain.markets) diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index cd5b9ca9fc..39637200e5 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -4,13 +4,16 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.applyL2Compatibility import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetwork import com.tangem.data.common.utils.retryOnError +import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent import com.tangem.data.markets.converters.TokenChartConverter import com.tangem.data.markets.converters.TokenMarketInfoConverter import com.tangem.data.markets.converters.TokenMarketListConverter import com.tangem.data.markets.converters.toRequestParam +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -31,6 +34,7 @@ internal class DefaultMarketsTokenRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, private val dispatcherProvider: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, ) : MarketsTokenRepository { private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher( @@ -62,10 +66,14 @@ internal class DefaultMarketsTokenRepository( // we shouldn't infinitely retry on the first batch request val res = if (isFirstBatchFetching) { - requestCall() + catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) { + requestCall() + } } else { retryOnError(priority = true) { - requestCall() + catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) { + requestCall() + } } } @@ -92,6 +100,9 @@ internal class DefaultMarketsTokenRepository( val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher( tangemTechApi = tangemTechApi, marketsApi = marketsApi, + onApiError = { + analyticsEventHandler.send(MarketsDataAnalyticsEvent.List.Error.toEvent()) + }, ) val atomicInteger = AtomicInteger(0) @@ -109,6 +120,7 @@ internal class DefaultMarketsTokenRepository( fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String, + tokenSymbol: String, ): TokenChart { val mappedTokenId = getTokenIdIfL2Network(tokenId) val response = marketsApi.getCoinChart( @@ -117,13 +129,23 @@ internal class DefaultMarketsTokenRepository( interval = interval.toRequestParam(), ) - return TokenChartConverter.convert(interval, response.getOrThrow()) + val result = catchApiErrorAndSendEvent( + errorEvent = MarketsDataAnalyticsEvent.Details.Error( + request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart, + tokenSymbol = tokenSymbol, + ), + ) { + response.getOrThrow() + } + + return TokenChartConverter.convert(interval, result) } override suspend fun getChartPreview( fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String, + tokenSymbol: String, ): TokenChart { val mappedTokenId = getTokenIdIfL2Network(tokenId) val response = marketsApi.getCoinsListCharts( @@ -132,7 +154,11 @@ internal class DefaultMarketsTokenRepository( interval = interval.toRequestParam(), ) - val chart = response.getOrThrow()[mappedTokenId] ?: error("No chart preview data for token $mappedTokenId") + val chart = catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) { + response.getOrThrow()[mappedTokenId] ?: error( + "No chart preview data for the token $mappedTokenId", + ) + } return TokenChartConverter.convert(interval, chart) } @@ -140,6 +166,7 @@ internal class DefaultMarketsTokenRepository( override suspend fun getTokenInfo( fiatCurrencyCode: String, tokenId: String, + tokenSymbol: String, languageCode: String, ): TokenMarketInfo { val response = marketsApi.getCoinMarketData( @@ -148,12 +175,22 @@ internal class DefaultMarketsTokenRepository( language = languageCode, ) - val resultResponse = response.getOrThrow().applyL2Compatibility(tokenId) + val result = catchApiErrorAndSendEvent( + errorEvent = MarketsDataAnalyticsEvent.Details.Error( + request = MarketsDataAnalyticsEvent.Details.Error.Request.Info, + tokenSymbol = tokenSymbol, + ), + ) { + response.getOrThrow() + } + + val resultResponse = result.applyL2Compatibility(tokenId) return TokenMarketInfoConverter.convert(resultResponse) } override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes { // TODO change method when backend is ready + // add error analytics event val response = marketsApi.getCoinMarketData( currency = fiatCurrencyCode, coinId = tokenId, @@ -194,4 +231,16 @@ internal class DefaultMarketsTokenRepository( ) } } + + private inline fun catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T { + return try { + block() + } catch (e: ApiResponseError.HttpException) { + analyticsEventHandler.send(errorEvent.toEvent()) + throw e + } catch (e: ApiResponseError.TimeoutException) { + analyticsEventHandler.send(errorEvent.toEvent()) + throw e + } + } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index 3a377ab901..95fb2cca25 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -4,6 +4,7 @@ import com.tangem.data.common.utils.retryOnError import com.tangem.data.markets.converters.TokenMarketChartsConverter import com.tangem.data.markets.converters.TokenQuotesShortConverter import com.tangem.data.markets.converters.toRequestParam +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse @@ -20,6 +21,7 @@ import kotlinx.coroutines.launch internal class MarketsBatchUpdateFetcher( private val marketsApi: TangemTechMarketsApi, private val tangemTechApi: TangemTechApi, + private val onApiError: () -> Unit, ) : BatchUpdateFetcher, TokenMarketUpdateRequest> { override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( @@ -35,11 +37,13 @@ internal class MarketsBatchUpdateFetcher( val updateTasks = idsToUpdate.map { batchIds -> async { retryOnError { - marketsApi.getCoinsListCharts( - coinIds = batchIds.second.joinToString(separator = ","), - interval = updateRequest.interval.toRequestParam(), - currency = updateRequest.currency, - ).getOrThrow() + catchApiError(onApiError) { + marketsApi.getCoinsListCharts( + coinIds = batchIds.second.joinToString(separator = ","), + interval = updateRequest.interval.toRequestParam(), + currency = updateRequest.currency, + ).getOrThrow() + } } } } @@ -62,11 +66,13 @@ internal class MarketsBatchUpdateFetcher( } is TokenMarketUpdateRequest.UpdateQuotes -> { val quotesRes = retryOnError { - tangemTechApi.getQuotes( - currencyId = updateRequest.currencyId, - coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","), - fields = quoteFields.joinToString(separator = ","), - ).getOrThrow() + catchApiError(onApiError) { + tangemTechApi.getQuotes( + currencyId = updateRequest.currencyId, + coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","), + fields = quoteFields.joinToString(separator = ","), + ).getOrThrow() + } } update { @@ -106,6 +112,15 @@ internal class MarketsBatchUpdateFetcher( ) } + private inline fun catchApiError(onError: () -> Unit, block: () -> T): T { + return try { + block() + } catch (e: ApiResponseError) { + onError() + throw e + } + } + companion object { private val quoteFields = listOf( "price", diff --git a/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt b/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt new file mode 100644 index 0000000000..b58f04cb33 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt @@ -0,0 +1,42 @@ +package com.tangem.data.markets.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed interface MarketsDataAnalyticsEvent { + + sealed class List( + event: String, + params: Map = mapOf(), + ) : AnalyticsEvent(category = "Markets", event = event, params = params), MarketsDataAnalyticsEvent { + + data object Error : List(event = "Data Error") + } + + sealed class Details( + event: String, + params: Map = mapOf(), + ) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params), MarketsDataAnalyticsEvent { + + data class Error( + val request: Request, + val tokenSymbol: String, + ) : Details( + event = "Data Error", + params = mapOf( + "Source" to request.source, + "Token" to tokenSymbol, + ), + ) { + + enum class Request(val source: String) { + Chart("Chart"), + Info("Blocks"), + } + } + } + + fun toEvent(): AnalyticsEvent = when (this) { + is List -> this + is Details -> this + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt index 03cb704bd6..4ef2f1e094 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -1,5 +1,6 @@ package com.tangem.data.markets.di +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.markets.DefaultMarketsTokenRepository import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -24,12 +25,14 @@ internal object MarketsDataModule { @DevTangemApi tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, + analyticsEventHandler: AnalyticsEventHandler, ): MarketsTokenRepository { return DefaultMarketsTokenRepository( marketsApi = marketsApi, tangemTechApi = tangemTechApi, dispatcherProvider = dispatchers, userWalletsStore = userWalletsStore, + analyticsEventHandler = analyticsEventHandler, ) } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt index c7935cb4e0..2038181eef 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt @@ -9,11 +9,16 @@ class GetTokenMarketInfoUseCase( private val marketsTokenRepository: MarketsTokenRepository, ) { - suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either { + suspend operator fun invoke( + appCurrency: AppCurrency, + tokenId: String, + tokenSymbol: String, + ): Either { return Either.catch { marketsTokenRepository.getTokenInfo( fiatCurrencyCode = appCurrency.code, tokenId = tokenId, + tokenSymbol = tokenSymbol, languageCode = SupportedLanguages.getCurrentSupportedLanguageCode(), ) }.mapLeft {} diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt index feeeadab56..c6d677f34b 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt @@ -12,6 +12,7 @@ class GetTokenPriceChartUseCase( appCurrency: AppCurrency, interval: PriceChangeInterval, tokenId: String, + tokenSymbol: String, preview: Boolean, ): Either { return Either.catch { @@ -20,12 +21,14 @@ class GetTokenPriceChartUseCase( fiatCurrencyCode = appCurrency.code, interval = interval, tokenId = tokenId, + tokenSymbol = tokenSymbol, ) } else { marketsTokenRepository.getChart( fiatCurrencyCode = appCurrency.code, interval = interval, tokenId = tokenId, + tokenSymbol = tokenSymbol, ) } }.mapLeft {} diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 1e40c55a13..c9ff5dae64 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -12,11 +12,26 @@ interface MarketsTokenRepository { nextBatchSize: Int, ): TokenListBatchFlow - suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart + suspend fun getChart( + fiatCurrencyCode: String, + interval: PriceChangeInterval, + tokenId: String, + tokenSymbol: String, + ): TokenChart - suspend fun getChartPreview(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart + suspend fun getChartPreview( + fiatCurrencyCode: String, + interval: PriceChangeInterval, + tokenId: String, + tokenSymbol: String, + ): TokenChart - suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo + suspend fun getTokenInfo( + fiatCurrencyCode: String, + tokenId: String, + tokenSymbol: String, + languageCode: String, + ): TokenMarketInfo suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt index 3a4b5e4b57..81ecc42dcc 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt @@ -20,6 +20,13 @@ interface MarketsTokenDetailsComponent : ComposableContentComponent { val token: TokenMarketParams, val appCurrency: AppCurrency, val showPortfolio: Boolean, + val analyticsParams: AnalyticsParams?, + ) + + @Serializable + data class AnalyticsParams( + val blockchain: String?, + val source: String, ) @Composable diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt index 9cc202bdb4..270528b0c7 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt @@ -11,11 +11,7 @@ interface TokenMarketBlockComponent : ComposableContentComponent { @Serializable data class Params( - val cryptoCurrencyID: CryptoCurrency.ID, - val tokenId: String, - val tokenName: String, - val tokenSymbol: String, - val tokenImageUrl: String?, + val cryptoCurrency: CryptoCurrency, ) interface Factory { diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index a6e62fdf7c..bd7451e801 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -63,6 +63,8 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) implementation(projects.core.featuretoggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) /* Common */ implementation(projects.common.ui) 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 d78eb7c19f..681d36de1b 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 @@ -7,6 +7,7 @@ import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel @@ -14,6 +15,7 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params +import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent 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 @@ -29,6 +31,7 @@ import kotlinx.coroutines.launch internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: Params, + analyticsEventHandler: AnalyticsEventHandler, portfolioComponentFactory: MarketsPortfolioComponent.Factory, ) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { @@ -38,12 +41,17 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( id = getTokenIdIfL2Network(params.token.id), ), ) + private val analyticsParams = params.analyticsParams + private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams) private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.showPortfolio) { portfolioComponentFactory.create( context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params(updatedParams.token), + params = MarketsPortfolioComponent.Params( + updatedParams.token, + analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) }, + ), ) } else { null @@ -59,6 +67,18 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( } } } + + // === Analytics === + if (analyticsParams != null) { + analyticsEventHandler.send( + MarketDetailsAnalyticsEvent.EventBuilder( + token = params.token, + ).screenOpened( + blockchain = analyticsParams.blockchain, + source = analyticsParams.source, + ), + ) + } } @Composable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt new file mode 100644 index 0000000000..545104f625 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt @@ -0,0 +1,64 @@ +package com.tangem.features.markets.details.impl.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarketParams + +internal class MarketDetailsAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { + + data class EventBuilder( + val token: TokenMarketParams, + ) { + fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent( + event = "Token Chart Screen Opened", + params = buildMap { + put("Token", token.symbol) + blockchain?.let { put("blockchain", it) } + put("Source", source) + }, + ) + + fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent( + event = "Button - Period", + params = mapOf( + "Token" to token.symbol, + "Period" to interval.toAnalyticsString(), + "Source" to intervalType.source, + ), + ) + + fun readMoreClicked() = MarketDetailsAnalyticsEvent( + event = "Button - Read More", + params = mapOf( + "Token" to token.symbol, + ), + ) + + fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent( + event = "Button - Links", + params = mapOf( + "Token" to token.symbol, + "Link" to linkTitle, + ), + ) + } + + enum class IntervalType(val source: String) { + Chart("Chart"), + PricePerformance("Price"), + Insights("Insights"), + } +} + +private fun PriceChangeInterval.toAnalyticsString() = when (this) { + PriceChangeInterval.H24 -> "24h" + PriceChangeInterval.WEEK -> "7d" + PriceChangeInterval.MONTH -> "1m" + PriceChangeInterval.MONTH3 -> "3m" + PriceChangeInterval.MONTH6 -> "6m" + PriceChangeInterval.YEAR -> "1y" + PriceChangeInterval.ALL_TIME -> "All" +} \ 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 c1f7cd2393..5f12f70b59 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.sorted +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,6 +21,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.formatter.* @@ -56,10 +58,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase, private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private var quotesJob = JobHolder() private val params = paramsContainer.require() + private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token) private val currentAppCurrency = getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -75,14 +79,36 @@ internal class MarketsTokenDetailsModel @Inject constructor( onInfoClick = { showInfoBottomSheet(it) }, - onLinkClick = { - urlOpener.openUrl(it.url) + onLinkClick = { link -> + urlOpener.openUrl(link.url) + // === Analytics === + analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title)) }, + // === Analytics === + onPricePerformanceIntervalChanged = { + analyticsEventHandler.send( + analyticsEventBuilder.intervalChanged( + intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, + interval = it, + ), + ) + }, + onInsightsIntervalChanged = { + analyticsEventHandler.send( + analyticsEventBuilder.intervalChanged( + intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights, + interval = it, + ), + ) + }, + // ================== ) private val descriptionConverter = DescriptionConverter( onReadModeClicked = { showInfoBottomSheet(it) + // === Analytics === + analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked()) }, ) @@ -167,6 +193,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( currentQuotes = currentQuotes, lastUpdatedTimestamp = lastUpdatedTimestamp, currentTokenInfo = currentTokenInfo, + onPricePerformanceIntervalChanged = { + analyticsEventHandler.send( + analyticsEventBuilder.intervalChanged( + intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, + interval = it, + ), + ) + }, ) private val loadChartJobHolder = JobHolder() @@ -221,6 +255,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( appCurrency = currentAppCurrency.value, interval = interval, tokenId = params.token.id, + tokenSymbol = params.token.symbol, preview = false, ) @@ -290,6 +325,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( val tokenMarketInfo = getTokenMarketInfoUseCase( appCurrency = currentAppCurrency.value, tokenId = params.token.id, + tokenSymbol = params.token.symbol, ) tokenMarketInfo.fold( @@ -368,6 +404,15 @@ internal class MarketsTokenDetailsModel @Inject constructor( private fun onSelectedIntervalChange(interval: PriceChangeInterval) { if (state.value.selectedInterval == interval) return + // === Analytics === + analyticsEventHandler.send( + analyticsEventBuilder.intervalChanged( + intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart, + interval = interval, + ), + ) + // ================== + val quotes = currentQuotes.value val priceChangePercent = quotes.getFormattedPercentByInterval(interval) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt index 40d76daa99..7b16182d58 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt @@ -32,6 +32,7 @@ internal class DescriptionConverter( ), ), body = stringReference(value.fullDescription ?: ""), + showGeneratedAINotification = true, ), ) }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt index 376bf704a0..adb9f89f72 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -5,6 +5,7 @@ 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.appcurrency.model.AppCurrency +import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.InfoPointUM @@ -21,6 +22,7 @@ import java.math.BigDecimal internal class InsightsConverter( private val appCurrency: Provider, private val onInfoClick: (InfoBottomSheetContent) -> Unit, + private val onIntervalChanged: (PriceChangeInterval) -> Unit, ) : Converter { override fun convert(value: TokenMarketInfo.Insights): InsightsUM { @@ -55,6 +57,7 @@ internal class InsightsConverter( ), ) }, + onIntervalChanged = onIntervalChanged, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt index 1b571e168f..ca676ee2c5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt @@ -1,9 +1,9 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.LinksUM +import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link import com.tangem.features.markets.impl.R import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList @@ -25,7 +25,7 @@ internal class LinksConverter( private fun TokenMarketInfo.Link.convert(): LinksUM.Link { return LinksUM.Link( - title = stringReference(title), + title = title, iconRes = getIconById(id), url = link, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt index 29b04a9541..d1800c88c5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.utils.Provider @@ -13,6 +14,7 @@ import java.math.RoundingMode @Stable internal class PricePerformanceConverter( private val appCurrency: Provider, + private val onIntervalChanged: (PriceChangeInterval) -> Unit, ) { fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { @@ -20,6 +22,7 @@ internal class PricePerformanceConverter( h24 = value.day.convert(currentPrice), month = value.month.convert(currentPrice), all = value.allTime.convert(currentPrice), + onIntervalChanged = onIntervalChanged, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt index e90ad2b0d3..4f3145408b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.LinksUM @@ -14,15 +15,24 @@ internal class TokenMarketInfoConverter( appCurrency: Provider, onInfoClick: (InfoBottomSheetContent) -> Unit, onLinkClick: (LinksUM.Link) -> Unit, + onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, + onInsightsIntervalChanged: (PriceChangeInterval) -> Unit, ) : Converter { - private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) + private val insightsConverter = InsightsConverter( + appCurrency = appCurrency, + onInfoClick = onInfoClick, + onIntervalChanged = onInsightsIntervalChanged, + ) @Suppress("UnusedPrivateMember") // TODO second markets iteration private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) - private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency) + private val pricePerformanceConverter = PricePerformanceConverter( + appCurrency = appCurrency, + onIntervalChanged = onPricePerformanceIntervalChanged, + ) private val linksConverter = LinksConverter(onLinkClick = onLinkClick) override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt index dd664b03ca..4bf8b58e27 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenQuotes import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter @@ -22,8 +23,12 @@ internal class QuotesStateUpdater( private val currentQuotes: MutableStateFlow, private val lastUpdatedTimestamp: MutableStateFlow, private val currentTokenInfo: MutableStateFlow, + private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, ) { - private val pricePerformanceConverter = PricePerformanceConverter(currentAppCurrency) + private val pricePerformanceConverter = PricePerformanceConverter( + currentAppCurrency, + onIntervalChanged = onPricePerformanceIntervalChanged, + ) suspend fun updateQuotes(newQuotes: TokenQuotes) { val triggerPriceChangeType = getFormattedPriceChange( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt index 180c7eefa9..f0d31afa7b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt @@ -44,11 +44,13 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { ), ) - AdditionalInfoNotification( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) + if (it.showGeneratedAINotification) { + AdditionalInfoNotification( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + } SpacerH(bottomBarHeight) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt index df5dab7cce..e13502279d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt @@ -47,7 +47,10 @@ internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) { PriceChangeInterval.MONTH, ), initialSelectedItem = PriceChangeInterval.H24, - onClick = { currentInterval = it }, + onClick = { + currentInterval = it + state.onIntervalChanged(it) + }, ) { Box( Modifier @@ -177,6 +180,7 @@ private fun ContentPreview() { ), ), onInfoClick = {}, + onIntervalChanged = {}, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index e08e4a3052..de51bd5a8f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -97,7 +97,7 @@ private fun SubBlock( ) { links.fastForEach { Chip( - text = it.title, + text = stringReference(it.title), iconResId = it.iconRes, onClick = { onLinkClick(it) }, ) @@ -163,36 +163,36 @@ private fun ContentPreview() { state = LinksUM( officialLinks = persistentListOf( LinksUM.Link( - title = stringReference("Website"), + title = "Website", iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), LinksUM.Link( - title = stringReference("Website"), + title = "Website", iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), LinksUM.Link( - title = stringReference("Website"), + title = "Website", iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), ), social = persistentListOf( LinksUM.Link( - title = stringReference("Twitter"), + title = "Twitter", iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), LinksUM.Link( - title = stringReference("Facebook"), + title = "Facebook", iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), ), repository = persistentListOf( LinksUM.Link( - title = stringReference("Github"), + title = "Github", iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index e1b5444e2e..c111de7630 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -54,7 +54,10 @@ internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier PriceChangeInterval.ALL_TIME, ), initialSelectedItem = PriceChangeInterval.H24, - onClick = { currentInterval = it }, + onClick = { + currentInterval = it + state.onIntervalChanged(it) + }, ) { Box( Modifier @@ -227,6 +230,7 @@ private fun ContentPreview() { high = "\$580,5M", indicatorFraction = 0.2f, ), + onIntervalChanged = {}, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt index 858fc017c5..c68995460f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt @@ -6,4 +6,5 @@ import com.tangem.core.ui.extensions.TextReference internal data class InfoBottomSheetContent( val title: TextReference, val body: TextReference, + val showGeneratedAINotification: Boolean = false, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt index 9156f9bf1b..e2d0b3270b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.details.impl.ui.state +import com.tangem.domain.markets.PriceChangeInterval import kotlinx.collections.immutable.ImmutableList internal data class InsightsUM( @@ -7,4 +8,5 @@ internal data class InsightsUM( val weekInfo: ImmutableList, val monthInfo: ImmutableList, val onInfoClick: () -> Unit, + val onIntervalChanged: (PriceChangeInterval) -> Unit, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt index d1a6d13860..5b4ea87e18 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.markets.details.impl.ui.state import androidx.annotation.DrawableRes -import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList internal data class LinksUM( @@ -13,7 +12,7 @@ internal data class LinksUM( ) { data class Link( @DrawableRes val iconRes: Int, - val title: TextReference, + val title: String, val url: String, ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt index e56e10312e..9448472a0d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt @@ -1,11 +1,13 @@ package com.tangem.features.markets.details.impl.ui.state import androidx.annotation.FloatRange +import com.tangem.domain.markets.PriceChangeInterval internal data class PricePerformanceUM( val h24: Value, val month: Value, val all: Value, + val onIntervalChanged: (PriceChangeInterval) -> Unit, ) { data class Value( val low: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index ad932e8965..79865f0661 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -78,6 +78,10 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( token = token.toSerializableParam(), appCurrency = appCurrency, showPortfolio = true, + analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams( + blockchain = null, + source = "Market", + ), ), ), ) 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 c736781c03..62babbdc62 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 @@ -11,7 +11,15 @@ import kotlinx.serialization.Serializable interface MarketsPortfolioComponent : ComposableContentComponent { @Serializable - data class Params(val token: TokenMarketParams) + data class Params( + val token: TokenMarketParams, + val analyticsParams: AnalyticsParams?, + ) + + @Serializable + data class AnalyticsParams( + val source: String, + ) fun setTokenNetworks(networks: List) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt new file mode 100644 index 0000000000..5f7ff68ab5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -0,0 +1,50 @@ +package com.tangem.features.markets.portfolio.impl.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM + +internal class PortfolioAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { + + data class EventBuilder( + val token: TokenMarketParams, + val source: String?, + ) { + + fun addToPortfolioClicked() = PortfolioAnalyticsEvent( + event = "Button - Add To Portfolio", + params = mapOf( + "Token" to token.symbol, + ), + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + + fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( + event = "Token Network Selected", + params = mapOf( + "Count" to blockchainNames.size.toString(), + "Token" to token.symbol, + "blockchain" to blockchainNames.joinToString(separator = ", "), + ), + ) + + fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = + PortfolioAnalyticsEvent( + event = when (actionUM) { + TokenActionsBSContentUM.Action.Buy -> "Button - Buy" + TokenActionsBSContentUM.Action.Receive -> "Button - Receive" + TokenActionsBSContentUM.Action.Exchange -> "Button - Swap" + else -> "error" + }, + params = buildMap { + put("Token", token.symbol) + source?.let { put("Source", source) } + put("blockchain", blockchainName) + }, + ) + } +} \ 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 index fa57001e94..9c6c4bcba1 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 @@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -24,9 +25,11 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -49,12 +52,17 @@ internal class MarketsPortfolioModel @Inject constructor( private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val saveMarketTokensUseCase: SaveMarketTokensUseCase, private val addToPortfolioManager: AddToPortfolioManager, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { val state: StateFlow get() = _state private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) private val params = paramsContainer.require() + private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( + token = params.token, + source = params.analyticsParams?.source, + ) /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) @@ -72,14 +80,37 @@ internal class MarketsPortfolioModel @Inject constructor( ) private val factory = MyPortfolioUMFactory( - onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) }, + onAddClick = { + onAddToPortfolioBSVisibilityChange(isShow = true) + // === Analytics === + analyticsEventHandler.send( + analyticsEventBuilder.addToPortfolioClicked(), + ) + }, addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( token = params.token, onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, onNetworkSwitchClick = ::onNetworkSwitchClick, - onWalletSelect = ::onWalletSelect, - onContinueClick = ::onContinueClick, + onWalletSelect = { + onWalletSelect(it) + // === Analytics === + analyticsEventHandler.send( + analyticsEventBuilder.addToPortfolioWalletChanged(), + ) + }, + onContinueClick = { selectedWalletId, addedNetworks -> + onContinueClick(selectedWalletId, addedNetworks) + + // === Analytics === + analyticsEventHandler.send( + analyticsEventBuilder.addToPortfolioContinue( + blockchainNames = addedNetworks.mapNotNull { + BlockchainUtils.getNetworkInfo(it.networkId)?.name + }, + ), + ) + }, ), currentState = Provider { _state.value }, tokenActionsHandler = tokenActionsIntentsFactory.create( @@ -87,6 +118,14 @@ internal class MarketsPortfolioModel @Inject constructor( updateTokenReceiveBSConfig = { updateBlock -> updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } }, + onHandleQuickAction = { handledAction -> + analyticsEventHandler.send( + analyticsEventBuilder.quickActionClick( + actionUM = handledAction.action, + blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name, + ), + ) + }, ), updateTokens = { updateBlock -> updateTokensState { state -> diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index b43dc3b16f..fc2464a7f9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -34,10 +34,9 @@ internal class TokenActionsHandler @AssistedInject constructor( private val clipboardManager: ClipboardManager, private val uiMessageSender: UiMessageSender, private val reduxStateHolder: ReduxStateHolder, - @Assisted - private val currentAppCurrency: Provider, - @Assisted("updateTokenReceiveBSConfig") - private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + @Assisted private val currentAppCurrency: Provider, + @Assisted private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, ) { @@ -48,6 +47,12 @@ internal class TokenActionsHandler @AssistedInject constructor( ) fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + onHandleQuickAction( + HandledQuickAction( + action = action, + cryptoCurrencyData = cryptoCurrencyData, + ), + ) if (handleDemoMode(action, cryptoCurrencyData.userWallet)) return when (action) { @@ -177,8 +182,13 @@ internal class TokenActionsHandler @AssistedInject constructor( interface Factory { fun create( currentAppCurrency: Provider, - @Assisted("updateTokenReceiveBSConfig") updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit, + onHandleQuickAction: (HandledQuickAction) -> Unit, ): TokenActionsHandler } + + data class HandledQuickAction( + val action: TokenActionsBSContentUM.Action, + val cryptoCurrencyData: PortfolioData.CryptoCurrencyData, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 49adf98ff9..d0ca34298a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -40,6 +40,7 @@ internal class TokenMarketBlockModel @Inject constructor( ) : Model() { private val params = paramsContainer.require() + private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false) private val currentAppCurrency = getSelectedAppCurrencyUseCase() @@ -56,7 +57,7 @@ internal class TokenMarketBlockModel @Inject constructor( val state = MutableStateFlow( TokenMarketBlockUM( - currencySymbol = params.tokenSymbol, + currencySymbol = params.cryptoCurrency.symbol, currentPrice = null, h24Percent = null, priceChangeType = PriceChangeType.NEUTRAL, @@ -72,7 +73,7 @@ internal class TokenMarketBlockModel @Inject constructor( private fun startFetching() { modelScope.launch { getTokenQuotesUseCase( - currencyID = params.cryptoCurrencyID, + currencyID = params.cryptoCurrency.id, interval = PriceChangeInterval.H24, refresh = true, ).collect { @@ -100,9 +101,12 @@ internal class TokenMarketBlockModel @Inject constructor( } }.saveIn(quotesUpdateJobHolder) + val tokenId = params.cryptoCurrency.id.rawCurrencyId ?: return + modelScope.launch(dispatchers.main) { val result = getTokenPriceChartUseCase( - tokenId = params.tokenId, + tokenId = tokenId, + tokenSymbol = params.cryptoCurrency.symbol, interval = PriceChangeInterval.H24, appCurrency = currentAppCurrency.value, // TODO get currency from quotes use case [REDACTED_TASK_KEY] preview = true, @@ -128,12 +132,13 @@ internal class TokenMarketBlockModel @Inject constructor( private fun navigateToMarketDetails() { val quotes = quotesState ?: return + val tokenId = params.cryptoCurrency.id.rawCurrencyId ?: return val tokenParam = TokenMarketParams( - id = params.tokenId, - name = params.tokenSymbol, - imageUrl = params.tokenImageUrl, - symbol = params.tokenSymbol, + id = tokenId, + name = params.cryptoCurrency.name, + imageUrl = params.cryptoCurrency.iconUrl, + symbol = params.cryptoCurrency.symbol, tokenQuotes = TokenMarketParams.Quotes( currentPrice = quotes.currentPrice, h24Percent = quotes.h24Percent, @@ -147,6 +152,10 @@ internal class TokenMarketBlockModel @Inject constructor( token = tokenParam, appCurrency = currentAppCurrency.value, showPortfolio = false, + analyticsParams = AppRoute.MarketsTokenDetails.AnalyticsParams( + blockchain = params.cryptoCurrency.network.name, + source = "Token", + ), ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt new file mode 100644 index 0000000000..99f0faa3c3 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.markets.tokenlist.impl.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM + +internal sealed class MarketsListAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Markets", event = event, params = params) { + + data object BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened") + + data class SortBy( + val sortByTypeUM: SortByTypeUM, + val interval: MarketsListUM.TrendInterval, + ) : MarketsListAnalyticsEvent( + event = "Sort By", + params = mapOf( + "Type" to when (sortByTypeUM) { + SortByTypeUM.Rating -> "Rating" + SortByTypeUM.Trending -> "Trending" + SortByTypeUM.ExperiencedBuyers -> "Buyers" + SortByTypeUM.TopGainers -> "Gainers" + SortByTypeUM.TopLosers -> "Losers" + }, + "Period" to when (interval) { + MarketsListUM.TrendInterval.H24 -> "24h" + MarketsListUM.TrendInterval.D7 -> "7d" + MarketsListUM.TrendInterval.M1 -> "1m" + }, + ), + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index e140b438b6..f3dd2d3c1a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -9,8 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.entry.BottomSheetState -import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager +import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager +import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM @@ -32,6 +34,7 @@ internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private var updateQuotesJob = JobHolder() @@ -221,10 +224,30 @@ internal class MarketsListModel @Inject constructor( } } + // analytics + initAnalytics() + // initial loading mainMarketsListManager.reload() } + private fun initAnalytics() { + containerBottomSheetState + .onEach { + if (it == BottomSheetState.EXPANDED) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) + } + }.launchIn(modelScope) + + state + .filter { it.isInSearchMode.not() } + .map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) } + .distinctUntilChanged() + .onEach { + analyticsEventHandler.send(it) + }.launchIn(modelScope) + } + private fun onTokenUIClicked(token: MarketsListItemUM) { modelScope.launch { activeListManager.getTokenById(token.id)?.let { found -> diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index c942539d2f..cb4222f44b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -85,15 +85,9 @@ internal class TokenDetailsFragment : ComposeFragment() { } private fun CryptoCurrency.toParam(): TokenMarketBlockComponent.Params? { - val tokenId = id.rawCurrencyId ?: return null // token price is not available + id.rawCurrencyId ?: return null // token price is not available - return TokenMarketBlockComponent.Params( - cryptoCurrencyID = id, - tokenId = tokenId, - tokenName = name, - tokenSymbol = symbol, - tokenImageUrl = iconUrl, - ) + return TokenMarketBlockComponent.Params(cryptoCurrency = this) } @Composable 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 d22db8edce..3b57403e42 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 @@ -403,6 +403,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( token = tokenMarketParams, appCurrency = getSelectedAppCurrencyUseCase.unwrap(), showPortfolio = false, + analyticsParams = AppRoute.MarketsTokenDetails.AnalyticsParams( + blockchain = cryptoCurrencyStatus.currency.network.name, + source = "Main", + ), ), ) } From bb32493ab0cc85a46537eb2ced39b4bb33a05cfe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Sep 2024 19:14:30 +0300 Subject: [PATCH 197/197] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 12 ------ .../local/token/DefaultStakingBalanceStore.kt | 21 +++++++--- .../local/token/StakingBalanceStore.kt | 10 +++-- .../data/staking/DefaultStakingRepository.kt | 22 ++++++---- .../model/stakekit/YieldBalanceList.kt | 11 +++-- .../staking/GetStakingYieldBalanceUseCase.kt | 31 -------------- .../tokens/GetCurrencyStatusUpdatesUseCase.kt | 4 +- .../domain/tokens/GetNodlTokenListUseCase.kt | 2 +- .../domain/tokens/error/TokenListError.kt | 2 +- .../mapper/CurrencyStatusErrorMappers.kt | 1 + .../error/mapper/TokenListErrorMappers.kt | 1 + .../CurrenciesStatusesLceOperations.kt | 16 ++++++- .../CurrenciesStatusesOperations.kt | 42 ++++++++++++++++--- 13 files changed, 100 insertions(+), 75 deletions(-) delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 2c64513e3b..c6d4c83b91 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -74,18 +74,6 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideGetStakingYieldBalanceUseCase( - stakingRepository: StakingRepository, - stakingErrorResolver: StakingErrorResolver, - ): GetStakingYieldBalanceUseCase { - return GetStakingYieldBalanceUseCase( - stakingRepository = stakingRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - @Provides @Singleton fun provideInitializeStakingProcessUseCase( 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 008cdd1c0f..0e2ae15ec3 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 @@ -29,22 +29,31 @@ internal class DefaultStakingBalanceStore( } } - override fun get(userWalletId: UserWalletId, integrationId: String): Flow { + override fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow { return dataStore.get(userWalletId.stringValue) .mapNotNull { balances -> - balances.firstOrNull { it.integrationId == integrationId } + balances.firstOrNull { it.integrationId == integrationId && it.addresses.address == address } } } - override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO? { + override suspend fun getSyncOrNull( + userWalletId: UserWalletId, + address: String, + integrationId: String, + ): YieldBalanceWrapperDTO? { return dataStore.getSyncOrNull(userWalletId.stringValue) - ?.firstOrNull { it.integrationId == integrationId } + ?.firstOrNull { it.integrationId == integrationId && it.addresses.address == address } } - override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) { + override suspend fun store( + userWalletId: UserWalletId, + integrationId: String, + address: String, + item: YieldBalanceWrapperDTO, + ) { mutex.withLock { val balances = dataStore.getSyncOrNull(userWalletId.stringValue) - ?.addOrReplace(item) { it.integrationId == integrationId } + ?.addOrReplace(item) { it.integrationId == integrationId && it.addresses.address == address } ?: setOf(item) dataStore.store(userWalletId.stringValue, balances) 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 421f03dae1..601b11ccb0 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 @@ -12,9 +12,13 @@ interface StakingBalanceStore { suspend fun store(userWalletId: UserWalletId, items: Set) - fun get(userWalletId: UserWalletId, integrationId: String): Flow + fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow - suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO? + suspend fun getSyncOrNull( + userWalletId: UserWalletId, + address: String, + integrationId: String, + ): YieldBalanceWrapperDTO? - suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) + suspend fun store(userWalletId: UserWalletId, integrationId: String, address: 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 6d22478790..efe1895104 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 @@ -37,7 +37,6 @@ import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList -import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionType @@ -310,6 +309,7 @@ internal class DefaultStakingRepository( stakingBalanceStore.store( userWalletId, requestBody.integrationId, + address, YieldBalanceWrapperDTO( balances = result, integrationId = requestBody.integrationId, @@ -328,9 +328,10 @@ internal class DefaultStakingRepository( send(YieldBalance.Empty) } else { launch(dispatchers.io) { + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") - stakingBalanceStore.get(userWalletId, integrationId) + stakingBalanceStore.get(userWalletId, address, integrationId) .collectLatest { send(yieldBalanceConverter.convert(it)) } @@ -354,9 +355,12 @@ internal class DefaultStakingRepository( } else { fetchSingleYieldBalance(userWalletId, cryptoCurrency) + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") - val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) + + val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId) ?: return@withContext YieldBalance.Error yieldBalanceConverter.convert(result) @@ -379,17 +383,19 @@ internal class DefaultStakingRepository( block = { val availableCurrencies = cryptoCurrencies .mapNotNull { currency -> - val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network) + val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) val integrationId = integrationIdMap[getIntegrationKey(currency.id)] - if (integrationId != null && address != null) { - address to integrationId + if (integrationId != null) { + addresses to integrationId } else { null } } - .distinctBy { it.second } - .map { getBalanceRequestData(it.first, it.second) } + .flatMap { (addresses, integrationId) -> + addresses.map { address -> address to integrationId } + } + .map { getBalanceRequestData(it.first.value, it.second) } .ifEmpty { return@invokeOnExpire } val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() 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 042aa9acbf..02f20f6e1c 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,10 +5,13 @@ sealed class YieldBalanceList { data class Data( val balances: List, ) : YieldBalanceList() { - fun getBalance(rawCurrencyId: String?): YieldBalance { - return balances.firstOrNull { yield -> - (yield as? YieldBalance.Data)?.balance?.items - ?.any { rawCurrencyId == it.rawCurrencyId } == true + + fun getBalance(address: String?, rawCurrencyId: String?): YieldBalance { + return balances.firstOrNull { yieldBalance -> + val data = yieldBalance as? YieldBalance.Data + data?.balance?.items?.any { + address == data.address && rawCurrencyId == it.rawCurrencyId + } == true } ?: YieldBalance.Error } } 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 deleted file mode 100644 index 16d17d97ad..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.domain.core.utils.EitherFlow -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.CryptoCurrency -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map - -class GetStakingYieldBalanceUseCase( - private val stakingRepository: StakingRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - - operator fun invoke( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): EitherFlow { - return stakingRepository.getSingleYieldBalanceFlow( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - ).map> { it.right() } - .catch { emit(stakingErrorResolver.resolve(it).left()) } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index dceb8551cf..f74a62b2d4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -44,7 +44,7 @@ class GetCurrencyStatusUpdatesUseCase( ): Flow> { return flow { emitAll( - getCurrency( + getCurrencyStatus( userWalletId = userWalletId, currencyId = currencyId, isSingleWalletWithTokens = isSingleWalletWithTokens, @@ -53,7 +53,7 @@ class GetCurrencyStatusUpdatesUseCase( }.flowOn(dispatchers.io) } - private suspend fun getCurrency( + private suspend fun getCurrencyStatus( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, isSingleWalletWithTokens: Boolean, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt index ae25bf7114..abbe7fa327 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt @@ -56,7 +56,7 @@ class GetNodlTokenListUseCase( stakingRepository = stakingRepository, ) - return operations.getNodlCurrencyStatusesFlow() + return operations.getCardCurrenciesStatusesFlow() .map { maybeCurrenciesStatuses -> maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt index 0a321a7874..5332319220 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt @@ -4,7 +4,7 @@ import com.tangem.domain.tokens.model.TokenList sealed class TokenListError { - object EmptyTokens : TokenListError() + data object EmptyTokens : TokenListError() data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : TokenListError() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt index 000588f24b..7fb8c805ca 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt @@ -10,6 +10,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencySt is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, + is CurrenciesStatusesOperations.Error.EmptyAddresses, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, -> CurrencyStatusError.UnableToCreateCurrency } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 4f918d6a79..5e4071d5b1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -10,6 +10,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, + is CurrenciesStatusesOperations.Error.EmptyAddresses, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, is CurrenciesStatusesOperations.Error.EmptyYieldBalances, -> TokenListError.EmptyTokens 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 8d97dc1a83..ff2271edbb 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 @@ -73,6 +73,7 @@ internal class CurrenciesStatusesLceOperations( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), getYieldBalances(userWalletId, nonEmptyCurrencies), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> val statuses = createCurrenciesStatuses( currencies = nonEmptyCurrencies, @@ -144,11 +145,15 @@ internal class CurrenciesStatusesLceOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } + val address = extractAddress(networkStatus) val isStakingSupported = stakingRepository.isStakingSupported( stakingRepository.getIntegrationKey(currency.id), ) val yieldBalance = if (isStakingSupported) { - (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + (yieldBalances as? YieldBalanceList.Data)?.getBalance( + address = address, + rawCurrencyId = currency.id.rawCurrencyId, + ) } else { null } @@ -221,4 +226,13 @@ internal class CurrenciesStatusesLceOperations( return networks to currenciesIds } + + private fun extractAddress(networkStatus: NetworkStatus?): String? { + return when (val value = networkStatus?.value) { + is NetworkStatus.NoAccount -> value.address.defaultAddress.value + is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value + is NetworkStatus.Verified -> value.address.defaultAddress.value + else -> null + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index a5cf2318cd..bea881092e 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 @@ -36,7 +36,12 @@ internal class CurrenciesStatusesOperations( networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies) - return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances) + return createCurrenciesStatuses( + nonEmptyCurrencies, + quotes, + networkStatuses, + yieldBalances, + ) }, catch = { raise(Error.DataError(it)) }, ) @@ -115,8 +120,7 @@ internal class CurrenciesStatusesOperations( return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances) } - /** Get flow of currency statuses for NODL card */ - fun getNodlCurrencyStatusesFlow(): Flow>> { + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( block = { getCurrenciesFromCard(userWalletId) }, @@ -147,12 +151,13 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> + getYieldBalances(nonEmptyCurrencies), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> createCurrenciesStatuses( currencies = nonEmptyCurrencies, maybeQuotes = maybeQuotes, maybeNetworkStatuses = maybeNetworksStatuses, - maybeYieldBalances = null, + maybeYieldBalances = maybeYieldBalances, ) } @@ -265,11 +270,16 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } + val address = extractAddress(networkStatus) + val isStakingSupported = stakingRepository.isStakingSupported( stakingRepository.getIntegrationKey(currency.id), ) val yieldBalance = if (isStakingSupported) { - (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + (yieldBalances as? YieldBalanceList.Data)?.getBalance( + address = address, + rawCurrencyId = currency.id.rawCurrencyId, + ) } else { null } @@ -393,6 +403,15 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyNetworksStatuses.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( cryptoCurrencies: List, ): Either { @@ -449,6 +468,15 @@ internal class CurrenciesStatusesOperations( return networks to currenciesIds } + private fun extractAddress(networkStatus: NetworkStatus?): String? { + return when (val value = networkStatus?.value) { + is NetworkStatus.NoAccount -> value.address.defaultAddress.value + is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value + is NetworkStatus.Verified -> value.address.defaultAddress.value + else -> null + } + } + sealed class Error { data object EmptyCurrencies : Error() @@ -457,6 +485,8 @@ internal class CurrenciesStatusesOperations( data object EmptyNetworksStatuses : Error() + data object EmptyAddresses : Error() + data object UnableToCreateCurrencyStatus : Error() data class DataError(val cause: Throwable) : Error()