From 97f41bd800090fd05f14cb19a573065c73e609bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Apr 2026 14:34:13 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TokenSyncDomainModule.kt | 8 +- .../tangem/tap/routing/utils/ChildFactory.kt | 1 - .../com/tangem/common/routing/AppRoute.kt | 1 - data/tokensync/build.gradle.kts | 14 +- .../data/tokensync/di/TokenSyncDataModule.kt | 49 +++ .../repository/DefaultTokenSyncRepository.kt | 346 ++++++++++++++++++ .../usecase/ManageCryptoCurrenciesUseCase.kt | 63 +++- .../tokens/model/tokensync/DiscoveredToken.kt | 14 - .../model/tokensync/TokenSyncProgress.kt | 22 -- .../tokens/repository/TokenSyncRepository.kt | 23 -- .../tokensync/model/TokenSyncProgress.kt | 2 - .../repository/TokenSyncRepository.kt | 2 + ...ensUseCase.kt => StartTokenSyncUseCase.kt} | 5 +- features/hot-wallet/impl/build.gradle.kts | 1 + .../HotAccessCodeRequestModel.kt | 10 + .../model/AddExistingWalletImportModel.kt | 9 + .../forgetwallet/ForgetWalletModel.kt | 9 + .../component/ManageTokensSource.kt | 1 - .../wallet-settings/impl/build.gradle.kts | 1 + .../model/WalletSettingsModel.kt | 11 + features/wallet/impl/build.gradle.kts | 1 + .../wallet/child/wallet/model/WalletModel.kt | 11 + .../intents/WalletWarningsClickIntents.kt | 18 + .../common/WalletPreviewDataLegacy.kt | 4 +- .../preview/WalletScreenPreviewDataLegacy.kt | 4 +- .../domain/GetMultiWalletWarningsFactory.kt | 34 ++ .../domain/WalletAdditionalInfoFactory.kt | 52 ++- .../implementors/MultiWalletContentLoader.kt | 10 +- .../wallet/state/model/TokenSyncProgressUM.kt | 13 + .../state/model/WalletAdditionalInfo.kt | 11 +- .../wallet/state/model/WalletCardState.kt | 5 +- .../wallet/state/model/WalletState.kt | 3 + .../SetTokenListErrorTransformer.kt | 2 +- .../transformers/SetTokenListTransformer.kt | 2 +- .../SetTokenSyncProgressTransformer.kt | 47 +-- .../UpdateWalletCardsCountTransformer.kt | 11 +- .../wallet/subscribers/TokenSyncSubscriber.kt | 45 +++ .../wallet/ui/components/common/WalletCard.kt | 66 ++-- 38 files changed, 768 insertions(+), 163 deletions(-) create mode 100644 data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt create mode 100644 data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt delete mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt delete mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt rename domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/{SyncTokensUseCase.kt => StartTokenSyncUseCase.kt} (94%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt index 890ef9b566..49da21bf6d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt @@ -4,7 +4,7 @@ import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.tokensync.repository.TokenSyncRepository import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase -import com.tangem.domain.tokensync.usecase.SyncTokensUseCase +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.utils.coroutines.AppCoroutineScope import dagger.Module import dagger.Provides @@ -36,12 +36,12 @@ internal object TokenSyncDomainModule { @Provides @Singleton - fun provideSyncTokensUseCase( + fun provideStartTokenSyncUseCase( tokenSyncRepository: TokenSyncRepository, manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, appCoroutineScope: AppCoroutineScope, - ): SyncTokensUseCase { - return SyncTokensUseCase( + ): StartTokenSyncUseCase { + return StartTokenSyncUseCase( tokenSyncRepository = tokenSyncRepository, manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, appCoroutineScope = appCoroutineScope, 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 b09329cb26..a200fea92f 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 @@ -138,7 +138,6 @@ internal class ChildFactory @Inject constructor( AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT - AppRoute.ManageTokens.Source.TOKEN_SYNC_BANNER -> ManageTokensSource.TOKEN_SYNC_BANNER } val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None 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 8f0ab46964..ea5ec5fdcc 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 @@ -146,7 +146,6 @@ sealed class AppRoute(val path: String) : Route { STORIES, SETTINGS, ACCOUNT, - TOKEN_SYNC_BANNER, } } diff --git a/data/tokensync/build.gradle.kts b/data/tokensync/build.gradle.kts index 5c9c29675d..973df2ab9a 100644 --- a/data/tokensync/build.gradle.kts +++ b/data/tokensync/build.gradle.kts @@ -11,14 +11,24 @@ android { } dependencies { + api(projects.domain.tokensync) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.models) + implementation(projects.domain.walletManager) + implementation(projects.domain.wallets) + implementation(projects.data.common) + implementation(projects.libs.blockchainSdk) implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.domain.models) + + implementation(tangemDeps.blockchain) + + implementation(deps.androidx.datastore) implementation(deps.hilt.android) kapt(deps.hilt.kapt) - implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.moshi) } \ No newline at end of file diff --git a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt new file mode 100644 index 0000000000..525a7612fa --- /dev/null +++ b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt @@ -0,0 +1,49 @@ +package com.tangem.data.tokensync.di + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.tokensync.repository.DefaultTokenSyncRepository +import com.tangem.data.tokensync.store.TokenSyncStoreFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.tokensync.repository.TokenSyncRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +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 TokenSyncDataModule { + + @Provides + @Singleton + fun provideTokenSyncRepository( + walletManagersFacade: WalletManagersFacade, + tangemTechApi: TangemTechApi, + userWalletsListRepository: UserWalletsListRepository, + networkFactory: NetworkFactory, + appPreferencesStore: AppPreferencesStore, + tokenSyncStoreFactory: TokenSyncStoreFactory, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + dispatchers: CoroutineDispatcherProvider, + excludedBlockchains: ExcludedBlockchains, + ): TokenSyncRepository { + return DefaultTokenSyncRepository( + walletManagersFacade = walletManagersFacade, + tangemTechApi = tangemTechApi, + userWalletsListRepository = userWalletsListRepository, + networkFactory = networkFactory, + appPreferencesStore = appPreferencesStore, + tokenSyncStoreFactory = tokenSyncStoreFactory, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + dispatchers = dispatchers, + excludedBlockchains = excludedBlockchains, + ) + } +} \ No newline at end of file diff --git a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt new file mode 100644 index 0000000000..ed5dccaddf --- /dev/null +++ b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt @@ -0,0 +1,346 @@ +package com.tangem.data.tokensync.repository + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.tokenbalance.models.TokenBalance +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.tokensync.store.TokenSyncStore +import com.tangem.data.tokensync.store.TokenSyncStoreFactory +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +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.getObjectMapSync +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokensync.model.TokenSyncProgress +import com.tangem.domain.tokensync.repository.TokenSyncRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext +import java.math.BigDecimal +import java.util.concurrent.ConcurrentHashMap + +@Suppress("LongParameterList") +internal class DefaultTokenSyncRepository( + private val walletManagersFacade: WalletManagersFacade, + private val tangemTechApi: TangemTechApi, + private val userWalletsListRepository: UserWalletsListRepository, + private val networkFactory: NetworkFactory, + private val appPreferencesStore: AppPreferencesStore, + private val tokenSyncStoreFactory: TokenSyncStoreFactory, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + private val dispatchers: CoroutineDispatcherProvider, + private val excludedBlockchains: ExcludedBlockchains, +) : TokenSyncRepository { + + private val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS) + private val progressStates = ConcurrentHashMap>() + + override fun observeSyncProgress(userWalletId: UserWalletId): Flow { + return getProgressFlow(userWalletId) + } + + override fun acknowledgeCompletion(userWalletId: UserWalletId) { + val key = userWalletId.stringValue + val stateFlow = progressStates[key] ?: return + stateFlow.value = TokenSyncProgress.Idle + progressStates.remove(key, stateFlow) + } + + override suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List { + val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) + val storedTokens = tokenSyncStore.get() + if (storedTokens.isEmpty()) return emptyList() + + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + return responseCryptoCurrenciesFactory.createCurrencies( + tokens = storedTokens, + userWallet = userWallet, + accountIndex = DerivationIndex.Main, + ) + } + + override suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) { + val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) + tokenSyncStore.clear() + } + + override suspend fun clearPendingFlag(userWalletId: UserWalletId) { + setPendingFlag(userWalletId, value = false) + } + + override suspend fun getPendingSyncWalletIds(): List { + val pendingMap = appPreferencesStore + .getObjectMapSync(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY) + return pendingMap + .filter { it.value } + .map { UserWalletId(it.key) } + } + + override suspend fun runSync(userWalletId: UserWalletId) { + val networks = getSupportedNetworks(userWalletId) + + if (networks.isEmpty()) return + + setPendingFlag(userWalletId, value = true) + val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) + tokenSyncStore.clear() + + val batches = networks.chunked(MAX_CONCURRENT_REQUESTS) + var completedNetworks = 0 + getProgressFlow(userWalletId).value = TokenSyncProgress.InProgress( + completedNetworks = 0, + totalNetworks = networks.size, + ) + + for (batch in batches) { + val batchResults = processBatch(userWalletId, batch) + completedNetworks = handleBatchResults( + userWalletId = userWalletId, + results = batchResults, + tokenSyncStore = tokenSyncStore, + completedNetworks = completedNetworks, + totalNetworks = networks.size, + ) + } + } + + override suspend fun completeSync(userWalletId: UserWalletId) { + setPendingFlag(userWalletId, value = false) + getProgressFlow(userWalletId).value = TokenSyncProgress.Completed + } + + private suspend fun processBatch(userWalletId: UserWalletId, batch: List): List { + return coroutineScope { + batch.map { network -> + async(dispatchers.io) { + semaphore.withPermit { + processNetwork(userWalletId, network) + } + } + }.awaitAll() + } + } + + private suspend fun handleBatchResults( + userWalletId: UserWalletId, + results: List, + tokenSyncStore: TokenSyncStore, + completedNetworks: Int, + totalNetworks: Int, + ): Int { + var completed = completedNetworks + val progressFlow = getProgressFlow(userWalletId) + + for (result in results) { + completed++ + handleNetworkResult(result, tokenSyncStore) + progressFlow.value = TokenSyncProgress.InProgress( + completedNetworks = completed, + totalNetworks = totalNetworks, + ) + } + + return completed + } + + private suspend fun handleNetworkResult(result: NetworkResult, tokenSyncStore: TokenSyncStore) { + when (result) { + is NetworkResult.Success -> { + if (result.responseTokens.isNotEmpty()) { + try { + tokenSyncStore.append(result.responseTokens) + } catch (e: Exception) { + TangemLogger.e("Failed to store discovered tokens for network: ${result.networkId}", e) + } + } + } + is NetworkResult.Error -> { + TangemLogger.e("Token sync failed for network: ${result.networkId}", result.cause) + } + } + } + + private suspend fun processNetwork(userWalletId: UserWalletId, network: Network): NetworkResult { + return try { + val tokenBalances = fetchAndFilterTokenBalances(userWalletId, network) + + if (tokenBalances.isEmpty()) { + return NetworkResult.Success( + networkId = network.backendId, + responseTokens = emptyList(), + ) + } + + val enrichedTokens = enrichTokensWithCatalog(tokenBalances, network) + + val responseTokens = enrichedTokens + .filter { it.contractAddress != null } + .map { it.toResponseToken() } + + NetworkResult.Success( + networkId = network.backendId, + responseTokens = responseTokens, + ) + } catch (e: Exception) { + NetworkResult.Error(networkId = network.backendId, cause = e) + } + } + + private suspend fun fetchAndFilterTokenBalances(userWalletId: UserWalletId, network: Network): List { + return withContext(dispatchers.io) { + walletManagersFacade.getTokenBalances(userWalletId, network) + .filter { it.amount > BigDecimal.ZERO } + } + } + + private suspend fun enrichTokensWithCatalog( + tokenBalances: List, + network: Network, + ): List = withContext(dispatchers.io) { + val tokensToEnrich = tokenBalances.filter { !it.isNativeToken } + + val catalogMap = fetchCatalogInfo( + networkId = network.backendId, + contractAddresses = tokensToEnrich.mapNotNull(TokenBalance::contractAddress), + ) + + tokenBalances.mapNotNull { balance -> + if (balance.isNativeToken) return@mapNotNull null + + val contractAddressLower = balance.contractAddress?.lowercase() + val coin = contractAddressLower?.let { catalogMap[it] } ?: return@mapNotNull null + val decimals = coin.networks + .find { it.contractAddress?.lowercase() == contractAddressLower } + ?.decimalCount + ?.toInt() + ?: 0 + + DiscoveredToken( + contractAddress = balance.contractAddress, + symbol = coin.symbol, + name = coin.name, + decimals = decimals, + amount = balance.amount, + isNativeToken = false, + currencyId = coin.id, + networkId = network.backendId, + ) + } + } + + private suspend fun fetchCatalogInfo( + networkId: String, + contractAddresses: List, + ): Map { + if (contractAddresses.isEmpty()) return emptyMap() + + return try { + val response = tangemTechApi.getCoins( + networkId = networkId, + contractAddresses = contractAddresses.joinToString(","), + active = true, + ).getOrThrow() + + buildMap { + for (coin in response.coins) { + for (network in coin.networks) { + val address = network.contractAddress?.lowercase() ?: continue + put(address, coin) + } + } + } + } catch (e: Exception) { + TangemLogger.w( + "Failed to fetch catalog info for networkId=$networkId, addresses=${contractAddresses.size}", + e, + ) + emptyMap() + } + } + + private fun getSupportedNetworks(userWalletId: UserWalletId): List { + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + + return Blockchain.entries + .filter { !it.isTestnet() } + .filter { it !in excludedBlockchains } + .mapNotNull { blockchain -> + networkFactory.create( + blockchain = blockchain, + extraDerivationPath = null, + userWallet = userWallet, + ) + } + } + + private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow { + return progressStates.getOrPut(userWalletId.stringValue) { + MutableStateFlow(TokenSyncProgress.Idle) + } + } + + private suspend fun setPendingFlag(userWalletId: UserWalletId, value: Boolean) { + appPreferencesStore.editData { prefs -> + prefs.setObjectMap( + key = PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY, + value = prefs.getObjectMap(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY) + .plus(userWalletId.stringValue to value), + ) + } + } + + private fun DiscoveredToken.toResponseToken(): UserTokensResponse.Token { + return UserTokensResponse.Token( + id = currencyId, + networkId = networkId, + name = name, + symbol = symbol, + decimals = decimals, + contractAddress = contractAddress, + ) + } + + private data class DiscoveredToken( + val contractAddress: String?, + val symbol: String, + val name: String, + val decimals: Int, + val amount: BigDecimal, + val isNativeToken: Boolean, + val currencyId: String?, + val networkId: String, + ) + + private sealed class NetworkResult { + data class Success( + val networkId: String, + val responseTokens: List, + ) : NetworkResult() + + data class Error( + val networkId: String, + val cause: Throwable, + ) : NetworkResult() + } + + companion object { + private const val MAX_CONCURRENT_REQUESTS = 3 + } +} \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index e9656e1601..3561434daf 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -78,6 +78,33 @@ class ManageCryptoCurrenciesUseCase( add: List = emptyList(), remove: List = emptyList(), skipDerivationErrors: Boolean = true, + ): Either = invokeInternal( + accountId = accountId, + add = add, + remove = remove, + skipDerivationErrors = skipDerivationErrors, + awaitTokensSyncFinished = false, + ) + + suspend fun invokeAndAwait( + accountId: AccountId, + add: List = emptyList(), + remove: List = emptyList(), + skipDerivationErrors: Boolean = true, + ): Either = invokeInternal( + accountId = accountId, + add = add, + remove = remove, + skipDerivationErrors = skipDerivationErrors, + awaitTokensSyncFinished = true, + ) + + private suspend fun invokeInternal( + accountId: AccountId, + add: List, + remove: List, + skipDerivationErrors: Boolean, + awaitTokensSyncFinished: Boolean, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { TangemLogger.d("No currencies to add or remove, skipping") @@ -111,9 +138,11 @@ class ManageCryptoCurrenciesUseCase( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), ) - parallelUpdatingScope.launch { - syncTokens(userWalletId, modifiedCurrencyList) - + syncTokensAndLaunchUpdates( + userWalletId = userWalletId, + modifiedCurrencyList = modifiedCurrencyList, + awaitSync = awaitTokensSyncFinished, + ) { cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) @@ -129,6 +158,7 @@ class ManageCryptoCurrenciesUseCase( accountId: AccountId, networkId: String, contractAddress: String, + awaitTokensSyncFinished: Boolean = false, ): Either = eitherOn(dispatchers.default) { val userWalletId = accountId.userWalletId @@ -152,9 +182,11 @@ class ManageCryptoCurrenciesUseCase( saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total)) - parallelUpdatingScope.launch { - syncTokens(userWalletId, modifiedCurrencyList) - + syncTokensAndLaunchUpdates( + userWalletId = userWalletId, + modifiedCurrencyList = modifiedCurrencyList, + awaitSync = awaitTokensSyncFinished, + ) { cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) } @@ -285,6 +317,25 @@ class ManageCryptoCurrenciesUseCase( .onFailure { TangemLogger.e("Failed to sync tokens for wallet $userWalletId", it) } } + private suspend fun syncTokensAndLaunchUpdates( + userWalletId: UserWalletId, + modifiedCurrencyList: ModifiedCurrencyList, + awaitSync: Boolean, + updates: suspend () -> Unit, + ) { + if (awaitSync) { + syncTokens(userWalletId, modifiedCurrencyList) + parallelUpdatingScope.launch { + updates() + } + } else { + parallelUpdatingScope.launch { + syncTokens(userWalletId, modifiedCurrencyList) + updates() + } + } + } + /** * Creates wallet managers for the given [currencies] if they do not already exist. * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt deleted file mode 100644 index ddf1a0069b..0000000000 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.tokens.model.tokensync - -import java.math.BigDecimal - -data class DiscoveredToken( - val contractAddress: String?, - val symbol: String, - val name: String, - val decimals: Int, - val amount: BigDecimal, - val isNativeToken: Boolean, - val currencyId: String?, - val networkId: String, -) \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt deleted file mode 100644 index 9e94d71268..0000000000 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.domain.tokens.model.tokensync - -sealed class TokenSyncProgress { - - data object Idle : TokenSyncProgress() - - data class InProgress( - val completedNetworks: Int, - val totalNetworks: Int, - ) : TokenSyncProgress() { - val progressPercent: Int - get() = if (totalNetworks > 0) { - completedNetworks * 100 / totalNetworks - } else { - 0 - } - } - - data object Completed : TokenSyncProgress() - - data class Error(val cause: Throwable) : TokenSyncProgress() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt deleted file mode 100644 index 5919dd5414..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.domain.tokens.repository - -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.tokensync.TokenSyncProgress -import kotlinx.coroutines.flow.Flow - -interface TokenSyncRepository { - - suspend fun runSync(userWalletId: UserWalletId) - - suspend fun getPendingSyncWalletIds(): List - - fun observeSyncProgress(userWalletId: UserWalletId): Flow - - fun acknowledgeCompletion(userWalletId: UserWalletId) - - suspend fun clearPendingFlag(userWalletId: UserWalletId) - - suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List - - suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) -} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt index f094aa655a..ee78b42a70 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt @@ -17,6 +17,4 @@ sealed class TokenSyncProgress { } data object Completed : TokenSyncProgress() - - data class Error(val cause: Throwable) : TokenSyncProgress() } \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt index c6f2d19976..b56da08053 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt @@ -9,6 +9,8 @@ interface TokenSyncRepository { suspend fun runSync(userWalletId: UserWalletId) + suspend fun completeSync(userWalletId: UserWalletId) + suspend fun getPendingSyncWalletIds(): List fun observeSyncProgress(userWalletId: UserWalletId): Flow diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt similarity index 94% rename from domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt rename to domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt index ddf41c5794..d0a1efdfa5 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap -class SyncTokensUseCase( +class StartTokenSyncUseCase( private val tokenSyncRepository: TokenSyncRepository, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val appCoroutineScope: AppCoroutineScope, @@ -25,6 +25,7 @@ class SyncTokensUseCase( try { tokenSyncRepository.runSync(userWalletId) applyDiscoveredTokens(userWalletId) + tokenSyncRepository.completeSync(userWalletId) } catch (e: Exception) { TangemLogger.e("Token sync failed for wallet: $userWalletId", e) } finally { @@ -61,7 +62,7 @@ class SyncTokensUseCase( if (currencies.isEmpty()) return true val accountId = AccountId.forMainCryptoPortfolio(userWalletId) - return manageCryptoCurrenciesUseCase( + return manageCryptoCurrenciesUseCase.invokeAndAwait( accountId = accountId, add = currencies, ).fold( diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 37c0ca6773..0daf9f2d1e 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.tokensync) /** Common */ implementation(projects.common.ui) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index f23c52ad2f..c71cf787d8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -11,10 +11,12 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM import com.tangem.features.hotwallet.impl.R @@ -30,12 +32,15 @@ import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped +@Suppress("LongParameterList") internal class HotAccessCodeRequestModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, private val userWalletsListRepository: UserWalletsListRepository, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val result = MutableStateFlow(null) @@ -214,6 +219,11 @@ internal class HotAccessCodeRequestModel @Inject constructor( val currentRequest = currentRequest.value ?: return val userWallet = userWalletsListRepository.userWalletsSync() .firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return + + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.cancel(userWallet.walletId) + } + userWalletsListRepository.delete(listOf(userWallet.walletId)) dismiss() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index eb4f297ea2..ed40d1e958 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -17,8 +17,10 @@ import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.common.wallets.error.SaveWalletError +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.MnemonicRepository import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM @@ -40,6 +42,8 @@ internal class AddExistingWalletImportModel @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, private val appsFlyerStore: AppsFlyerStore, @@ -109,6 +113,11 @@ internal class AddExistingWalletImportModel @Inject constructor( } .onRight { setImportProgress(false) + + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase(userWallet.walletId) + } + analyticsEventHandler.send( event = OnboardingAnalyticsEvent.Onboarding.Finished( source = AnalyticsParam.ScreensSources.ImportWallet.value, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt index 872ba5c072..fa801e998e 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt @@ -12,8 +12,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.features.hotwallet.ForgetWalletComponent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM import com.tangem.features.hotwallet.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -23,6 +25,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class ForgetWalletModel @Inject constructor( paramsContainer: ParamsContainer, @@ -30,6 +33,8 @@ internal class ForgetWalletModel @Inject constructor( private val router: Router, private val deleteWalletUseCase: DeleteWalletUseCase, private val uiMessageSender: UiMessageSender, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -79,6 +84,10 @@ internal class ForgetWalletModel @Inject constructor( private fun forgetWallet() { modelScope.launch { + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.cancel(params.userWalletId) + } + val hasUserWallets = deleteWalletUseCase(params.userWalletId) .getOrElse { error -> TangemLogger.e("Unable to delete wallet: $error") diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt index 38034501c5..fcce6daea7 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -9,7 +9,6 @@ enum class ManageTokensSource(val analyticsName: String) { ONBOARDING(analyticsName = "Onboarding"), SETTINGS(analyticsName = "Wallet Settings"), ACCOUNT(analyticsName = "Account"), - TOKEN_SYNC_BANNER(analyticsName = "Token Sync Banner"), SEND_VIA_SWAP(analyticsName = "SendViaSwap"), } diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 634724e099..ec188e4f5e 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.notifications) + implementation(projects.domain.tokensync) /* AndroidX */ implementation(deps.androidx.fragment.ktx) 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 52efecf7c9..4391d1ca87 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 @@ -37,6 +37,7 @@ import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.settings.repositories.PermissionRepository +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.analytics.Settings import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction @@ -48,6 +49,7 @@ import com.tangem.feature.walletsettings.utils.AccountItemsDelegate import com.tangem.feature.walletsettings.utils.AccountListSortingSaver import com.tangem.feature.walletsettings.utils.ItemsBuilder import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -88,6 +90,8 @@ internal class WalletSettingsModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val singleAccountListSupplier: SingleAccountListSupplier, private val accountListSortingSaver: AccountListSortingSaver, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -249,6 +253,13 @@ internal class WalletSettingsModel @Inject constructor( } private fun forgetWallet() = modelScope.launch { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrNull() + + if (userWallet is UserWallet.Hot && hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.cancel(params.userWalletId) + } + val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { error -> TangemLogger.e("Unable to delete wallet: $error") diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e3f92c0e20..d612025527 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -125,6 +125,7 @@ dependencies { implementation(projects.domain.yieldSupply.models) implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) + implementation(projects.domain.tokensync) /** Feature Apis */ implementation(projects.features.details.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 16d2963864..d304d3fe84 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -38,6 +38,8 @@ import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.domain.wallets.usecase.* import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -123,6 +125,8 @@ internal class WalletModel @Inject constructor( private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val uiMessageSender: UiMessageSender, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val startTokenSyncUseCase: StartTokenSyncUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -155,6 +159,7 @@ internal class WalletModel @Inject constructor( subscribeTangemPayOnWalletState() subscribeToMainScreenQrScanning() enableNotificationsIfNeeded() + applyPendingTokenSyncs() clickIntents.initialize(innerWalletRouter, modelScope) @@ -819,6 +824,12 @@ internal class WalletModel @Inject constructor( } } + private fun applyPendingTokenSyncs() { + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.applyPendingSyncs() + } + } + private fun enableNotificationsIfNeeded() { modelScope.launch { val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index ee38dd19e0..665908c863 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -19,6 +19,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -39,6 +40,7 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked import com.tangem.domain.tokens.model.details.NavigationAction +import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic @@ -94,6 +96,10 @@ internal interface WalletWarningsClickIntents { fun onUpgradeHotWalletClick(userWalletId: UserWalletId) fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) + + fun onDismissTokenSyncNotification(userWalletId: UserWalletId) + + fun onTokenSyncManageClick(userWalletId: UserWalletId) } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -126,6 +132,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val uiMessageSender: UiMessageSender, private val reviewManager: ReviewManager, private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, + private val acknowledgeTokenSyncCompletionUseCase: AcknowledgeTokenSyncCompletionUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -501,6 +508,17 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onDismissTokenSyncNotification(userWalletId: UserWalletId) { + acknowledgeTokenSyncCompletionUseCase(userWalletId) + } + + override fun onTokenSyncManageClick(userWalletId: UserWalletId) { + acknowledgeTokenSyncCompletionUseCase(userWalletId) + router.openManageTokensScreen( + AccountId.forMainCryptoPortfolio(userWalletId), + ) + } + private companion object { const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" + "&utm_medium=banner" + diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt index 59253bc6c3..bd122d0d8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt @@ -20,7 +20,9 @@ internal object WalletPreviewDataLegacy { balance = "8923,05312312312312312312331231231233432423423424234 $", additionalInfo = WalletAdditionalInfo( hideable = false, - content = TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"), + content = WalletAdditionalInfo.Content.Text( + TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"), + ), ), imageResId = R.drawable.ill_wallet2_cards3_120_106, dropDownItems = persistentListOf(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index 91a230aa1f..aa268b3051 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -160,7 +160,7 @@ internal object WalletScreenPreviewDataLegacy { title = "Note", additionalInfo = WalletAdditionalInfo( hideable = false, - content = TextReference.Str("Locked"), + content = WalletAdditionalInfo.Content.Text(TextReference.Str("Locked")), ), imageResId = R.drawable.ill_note_btc_120_106, dropDownItems = persistentListOf(), @@ -172,7 +172,7 @@ internal object WalletScreenPreviewDataLegacy { title = "Wallet 1", additionalInfo = WalletAdditionalInfo( hideable = false, - content = TextReference.Str("Seed phrase"), + content = WalletAdditionalInfo.Content.Text(TextReference.Str("Seed phrase")), ), imageResId = R.drawable.ill_wallet2_cards3_120_106, cardCount = 3, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index fd7ada0172..f0d03bf32a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -27,6 +27,9 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.tokensync.model.TokenSyncProgress +import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -43,6 +46,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -61,6 +65,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase, private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, + private val observeTokenSyncUseCase: ObserveTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") @@ -69,6 +75,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val params = SingleAccountStatusListProducer.Params(userWallet.walletId) val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) + val tokenSyncProgressFlow = if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) { + observeTokenSyncUseCase(userWallet.walletId).distinctUntilChanged() + } else { + flowOf(TokenSyncProgress.Idle) + } + return combine( accountStatusListFlow, isReadyToShowRateAppUseCase().distinctUntilChanged(), @@ -84,6 +96,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) .distinctUntilChanged(), + tokenSyncProgressFlow, ) { array -> array } .map { array -> val accountStatusList = array[0] as AccountStatusList @@ -95,6 +108,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowYieldPromo = array[6] as Boolean val shouldShowUpgradeBanner = array[7] as Boolean val closureTimestamp = array[8] as? Long + val tokenSyncProgress = array[9] as TokenSyncProgress val flattenCurrencies = accountStatusList.flattenCurrencies() val paymentAccountStatus = accountStatusList.accountStatuses @@ -139,6 +153,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( clickIntents = clickIntents, ) + addTokenSyncCompletedNotification( + userWallet = userWallet, + tokenSyncProgress = tokenSyncProgress, + clickIntents = clickIntents, + ) + addPushReminderNotification( clickIntents = clickIntents, shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification && @@ -384,6 +404,20 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( // } // } + private fun MutableList.addTokenSyncCompletedNotification( + userWallet: UserWallet, + tokenSyncProgress: TokenSyncProgress, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotification.TokenSyncCompleted( + onCloseClick = { clickIntents.onDismissTokenSyncNotification(userWallet.walletId) }, + onManageTokensClick = { clickIntents.onTokenSyncManageClick(userWallet.walletId) }, + ), + condition = tokenSyncProgress is TokenSyncProgress.Completed, + ) + } + private fun MutableList.addRateTheAppNotification( isReadyToShowRating: Boolean, clickIntents: WalletClickIntents, 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 b911a9a509..838ea96770 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 @@ -10,6 +10,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import java.math.BigDecimal @@ -28,7 +29,11 @@ internal object WalletAdditionalInfoFactory { * @param wallet current wallet * @param currencyAmount amount of currency */ - fun resolve(wallet: UserWallet, currencyAmount: BigDecimal? = null): WalletAdditionalInfo { + fun resolve( + wallet: UserWallet, + currencyAmount: BigDecimal? = null, + syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle, + ): WalletAdditionalInfo { return when (wallet) { is UserWallet.Cold -> { if (wallet.isMultiCurrency) { @@ -37,19 +42,26 @@ internal object WalletAdditionalInfoFactory { wallet.resolveSingleCurrencyInfo(currencyAmount) } } - is UserWallet.Hot -> wallet.resolveAdditionalInfo() + is UserWallet.Hot -> wallet.resolveAdditionalInfo(syncProgress) } } - private fun UserWallet.Hot.resolveAdditionalInfo(): WalletAdditionalInfo { + private fun UserWallet.Hot.resolveAdditionalInfo(syncProgress: TokenSyncProgressUM): WalletAdditionalInfo { + val content = if (syncProgress is TokenSyncProgressUM.InProgress) { + WalletAdditionalInfo.Content.SyncProgress(syncProgress.progressPercent) + } else { + WalletAdditionalInfo.Content.Text( + TextReference.Res(R.string.hw_mobile_wallet) + + when { + isLocked -> DIVIDER + TextReference.Res(R.string.common_locked) + backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup) + else -> TextReference.Str("") + }, + ) + } return WalletAdditionalInfo( hideable = false, - content = TextReference.Res(R.string.hw_mobile_wallet) + - when { - isLocked -> DIVIDER + TextReference.Res(R.string.common_locked) - backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup) - else -> TextReference.Str("") - }, + content = content, isHotBackedUp = backedUp, ) } @@ -58,9 +70,11 @@ internal object WalletAdditionalInfoFactory { return if (isLocked) { WalletAdditionalInfo( hideable = false, - content = getBackupInfoWithDivider( - backupCardsCount = getCardsCount(), - ) + TextReference.Res(R.string.common_locked), + content = WalletAdditionalInfo.Content.Text( + getBackupInfoWithDivider( + backupCardsCount = getCardsCount(), + ) + TextReference.Res(R.string.common_locked), + ), ) } else { val cardTypeResolver = scanResponse.cardTypesResolver @@ -76,8 +90,10 @@ internal object WalletAdditionalInfoFactory { return if (isImported) { WalletAdditionalInfo( hideable = false, - content = getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res( - id = R.string.common_seed_phrase, + content = WalletAdditionalInfo.Content.Text( + getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res( + id = R.string.common_seed_phrase, + ), ), ) } else { @@ -94,7 +110,7 @@ internal object WalletAdditionalInfoFactory { } private fun getBackupInfo(backupCardsCount: Int?): WalletAdditionalInfo { - val content = if (backupCardsCount != null) { + val ref = if (backupCardsCount != null) { getBackupInfoTextReference(count = backupCardsCount) } else { TextReference.EMPTY @@ -102,7 +118,7 @@ internal object WalletAdditionalInfoFactory { return WalletAdditionalInfo( hideable = false, - content = content, + content = WalletAdditionalInfo.Content.Text(ref), ) } @@ -118,7 +134,7 @@ internal object WalletAdditionalInfoFactory { return if (isLocked) { WalletAdditionalInfo( hideable = false, - content = TextReference.Res(R.string.common_locked), + content = WalletAdditionalInfo.Content.Text(TextReference.Res(R.string.common_locked)), ) } else { val blockchain = scanResponse.cardTypesResolver.getBlockchain() @@ -126,7 +142,7 @@ internal object WalletAdditionalInfoFactory { WalletAdditionalInfo( hideable = true, - content = TextReference.Str(value = amount.orEmpty()), + content = WalletAdditionalInfo.Content.Text(TextReference.Str(value = amount.orEmpty())), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 69217500ac..4313751a77 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -17,10 +18,12 @@ internal class MultiWalletContentLoader @AssistedInject constructor( private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory, private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, + private val tokenSyncSubscriberFactory: TokenSyncSubscriber.Factory, private val designFeatureToggles: DesignFeatureToggles, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List = listOf( + override fun create(): List = listOfNotNull( accountListSubscriberFactory.create(userWallet), walletNFTListSubscriberFactory.create(userWallet), checkWalletWithFundsSubscriberFactory.create(userWallet), @@ -31,6 +34,11 @@ internal class MultiWalletContentLoader @AssistedInject constructor( }, multiWalletActionButtonsSubscriberFactory.create(userWallet), tangemPayMainSubscriberFactory.create(userWallet), + if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) { + tokenSyncSubscriberFactory.create(userWallet) + } else { + null + }, ) @AssistedFactory diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt new file mode 100644 index 0000000000..2bcd1e9487 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed class TokenSyncProgressUM { + + data object Idle : TokenSyncProgressUM() + + data class InProgress(val progressPercent: Int) : TokenSyncProgressUM() + + data object Completed : TokenSyncProgressUM() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt index c18257cf76..cc7b235d4e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt @@ -6,7 +6,12 @@ import com.tangem.core.ui.extensions.TextReference @Immutable data class WalletAdditionalInfo( val hideable: Boolean, - val content: TextReference, + val content: Content, val isHotBackedUp: Boolean = false, - val shouldShowProgress: Boolean = false, -) \ No newline at end of file +) { + @Immutable + sealed interface Content { + data class Text(val text: TextReference) : Content + data class SyncProgress(val progressPercent: Int) : Content + } +} \ No newline at end of file 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 52efcdeb0e..1d657b0170 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 @@ -85,7 +85,10 @@ internal sealed interface WalletCardState { private companion object { val defaultAdditionalInfo: WalletAdditionalInfo - get() = WalletAdditionalInfo(hideable = true, content = EMPTY_BALANCE_TEXT) + get() = WalletAdditionalInfo( + hideable = true, + content = WalletAdditionalInfo.Content.Text(EMPTY_BALANCE_TEXT), + ) } } 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 9459402f44..3903da422a 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 @@ -27,6 +27,7 @@ internal sealed interface WalletState : WalletStateHolder { abstract val tangemPayState: TangemPayState abstract val tangemPayMainUM: TangemPayMainUM abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + abstract val tokenSyncProgressUM: TokenSyncProgressUM data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -40,6 +41,7 @@ internal sealed interface WalletState : WalletStateHolder { override val tangemPayState: TangemPayState, override val tangemPayMainUM: TangemPayMainUM, override val isTangemPayRefactorEnabled: Boolean, + override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle, ) : MultiCurrency() data class Locked( @@ -61,6 +63,7 @@ internal sealed interface WalletState : WalletStateHolder { override val tangemPayState: TangemPayState = TangemPayState.Empty override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty override val isTangemPayRefactorEnabled: Boolean = false + override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle } } 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 6bfbef7ab9..ef5698e378 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 @@ -61,7 +61,7 @@ internal class SetTokenListErrorTransformer( walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState(), tokensListUM = WalletTokensListUM.Empty( onEmptyClick = { - clickIntents.onManageTokensClick(walletUM.walletsBalanceUM.id) + clickIntents.onTokenSyncManageClick(walletUM.walletsBalanceUM.id) }, ), buttons = walletUM.disableButtons(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 27046856a0..ebfe4ac052 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -122,7 +122,7 @@ internal class SetTokenListTransformer( if (params !is TokenConverterParams.Account) { return WalletTokensListUM.Empty( onEmptyClick = { - clickIntents.onManageTokensClick(userWallet.walletId) + clickIntents.onTokenSyncManageClick(userWallet.walletId) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt index 283537ff7a..0dfbaf6dd9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt @@ -1,51 +1,34 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM 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.WalletUM internal class SetTokenSyncProgressTransformer( - userWalletId: UserWalletId, - private val progressPercent: Int, -) : WalletStateTransformer(userWalletId) { + private val userWallet: UserWallet, + private val progress: TokenSyncProgressUM, +) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { return when (prevState) { - is WalletState.MultiCurrency.Content -> { - val updatedCardState = updateCardState(prevState.walletCardState) - prevState.copy(walletCardState = updatedCardState) - } - else -> { - prevState - } + is WalletState.MultiCurrency.Content -> prevState.copy( + walletCardState = updateCardState(prevState.walletCardState), + tokenSyncProgressUM = progress, + ) + else -> prevState } } - override fun transform(walletUM: WalletUM): WalletUM { - return walletUM - } + override fun transform(walletUM: WalletUM): WalletUM = walletUM private fun updateCardState(cardState: WalletCardState): WalletCardState { - val additionalInfo = WalletAdditionalInfo( - hideable = false, - content = resourceReference( - id = R.string.initial_wallet_sync_restore_progress, - formatArgs = wrappedList(progressPercent), - ), - shouldShowProgress = true, - ) + val additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet, syncProgress = progress) return when (cardState) { - is WalletCardState.Loading -> { - cardState.copy(additionalInfo = additionalInfo) - } - is WalletCardState.Content -> { - cardState.copy(additionalInfo = additionalInfo) - } + is WalletCardState.Loading -> cardState.copy(additionalInfo = additionalInfo) + is WalletCardState.Content -> cardState.copy(additionalInfo = additionalInfo) else -> cardState } } 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 2b4dd9e457..86cc55373e 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 @@ -4,6 +4,7 @@ import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.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.state.model.TokenSyncProgressUM 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.WalletUM @@ -17,7 +18,9 @@ internal class UpdateWalletCardsCountTransformer( override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.MultiCurrency.Content -> { - prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + prevState.copy( + walletCardState = prevState.walletCardState.toUpdatedState(prevState.tokenSyncProgressUM), + ) } is WalletState.SingleCurrency.Content -> { prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) @@ -35,10 +38,12 @@ internal class UpdateWalletCardsCountTransformer( return walletUM // todo redesign main } - private fun WalletCardState.toUpdatedState(): WalletCardState { + private fun WalletCardState.toUpdatedState( + syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle, + ): WalletCardState { return when (this) { is WalletCardState.Content -> copy( - additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet, syncProgress = syncProgress), imageResId = walletImageResolver.resolve(userWallet = userWallet), cardCount = (userWallet as? UserWallet.Cold)?.getCardsCount(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt new file mode 100644 index 0000000000..1095805674 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokensync.model.TokenSyncProgress +import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenSyncProgressTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach + +internal class TokenSyncSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, + private val observeTokenSyncUseCase: ObserveTokenSyncUseCase, +) : WalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow<*> { + return observeTokenSyncUseCase(userWallet.walletId) + .onEach { current -> handleProgress(userWallet, current) } + } + + private fun handleProgress(userWallet: UserWallet, current: TokenSyncProgress) { + val progressUM = when (current) { + is TokenSyncProgress.InProgress -> TokenSyncProgressUM.InProgress(current.progressPercent) + is TokenSyncProgress.Completed -> TokenSyncProgressUM.Completed + is TokenSyncProgress.Idle -> TokenSyncProgressUM.Idle + } + stateController.update( + SetTokenSyncProgressTransformer( + userWallet = userWallet, + progress = progressUM, + ), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): TokenSyncSubscriber + } +} \ 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 26547001a4..9f7899e8e9 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 @@ -41,6 +41,9 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.feature.wallet.impl.R import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -156,16 +159,10 @@ private fun CardContainer(state: WalletCardState, isBalanceHidden: Boolean, item .padding(vertical = TangemTheme.dimens.spacing8), ) - val additionalText by remember(state.additionalInfo, isBalanceHidden) { - mutableStateOf( - state.additionalInfo?.content?.orMaskWithStars( - maskWithStars = state.additionalInfo?.hideable == true && isBalanceHidden, - ), - ) - } AdditionalInfo( - text = additionalText, - showProgress = state.additionalInfo?.shouldShowProgress == true, + content = state.additionalInfo?.content, + hideable = state.additionalInfo?.hideable == true, + isBalanceHidden = isBalanceHidden, modifier = Modifier.conditional( state.imageResId == null, ) { fillMaxWidth() }, @@ -300,28 +297,53 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { } @Composable -private fun AdditionalInfo(text: TextReference?, showProgress: Boolean, modifier: Modifier = Modifier) { +private fun AdditionalInfo( + content: WalletAdditionalInfo.Content?, + hideable: Boolean, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { AnimatedContent( - targetState = text, + targetState = content, + contentKey = { con -> + when (con) { + is WalletAdditionalInfo.Content.Text -> con + is WalletAdditionalInfo.Content.SyncProgress -> WalletAdditionalInfo.Content.SyncProgress::class + null -> null + } + }, label = "Update the additional text", modifier = modifier, transitionSpec = { fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith fadeOut(animationSpec = tween(durationMillis = 90)) }, - ) { animatedText -> - if (animatedText != null) { + ) { animatedContent -> + if (animatedContent != null) { Row( horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - AdditionalInfoText(text = animatedText) - if (showProgress) { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16), - color = TangemTheme.colors.icon.accent, - strokeWidth = TangemTheme.dimens.size2, - ) + when (animatedContent) { + is WalletAdditionalInfo.Content.Text -> { + AdditionalInfoText( + text = animatedContent.text.orMaskWithStars( + maskWithStars = hideable && isBalanceHidden, + ), + ) + } + is WalletAdditionalInfo.Content.SyncProgress -> { + AdditionalInfoText( + text = resourceReference( + id = R.string.initial_wallet_sync_restore_progress, + formatArgs = wrappedList(animatedContent.progressPercent), + ), + ) + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size16), + color = TangemTheme.colors.icon.accent, + strokeWidth = TangemTheme.dimens.size2, + ) + } } } } else { @@ -396,7 +418,7 @@ private class WalletCardStateProvider : CollectionPreviewParameterProvider