diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index bbc46e5650..7608be3109 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -77,6 +77,6 @@ }, { "name": "WALLET_BALANCE_FETCHER_ENABLED", - "version": "undefined" + "version": "5.27.0" } ] 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 4676289125..fa2f9a10b6 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 @@ -12,6 +12,9 @@ import kotlinx.coroutines.launch */ class JobHolder { + val isActive: Boolean + get() = job?.isActive ?: false + private var job: Job? = null /** Update current [JobHolder.job] and return new [job] */ diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index b6886643dc..00debb0cd3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -85,7 +85,9 @@ class CachedCurrenciesStatusesOperations( val nonEmptyCurrencies = currenciesFlow.mapNotNull { it.getOrNull() }.firstOrNull()?.toNonEmptyListOrNull() - if (!isFetchingStarted(userWalletId) && nonEmptyCurrencies != null) { + if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId) && + nonEmptyCurrencies != null + ) { launch { setFetchStarted(userWalletId) @@ -145,7 +147,7 @@ class CachedCurrenciesStatusesOperations( ) } - if (!isFetchingStarted(userWalletId)) { + if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId)) { launch { setFetchStarted(userWalletId) 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 287c272322..27f0b11f81 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 @@ -21,7 +21,6 @@ import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency @@ -32,10 +31,7 @@ import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory -import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver -import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase +import com.tangem.feature.wallet.presentation.wallet.domain.* import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig @@ -85,7 +81,7 @@ internal class WalletModel @Inject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, private val deepLinksRegistry: DeepLinksRegistry, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, + private val walletContentFetcher: WalletContentFetcher, private val tokensFeatureToggles: TokensFeatureToggles, private val appRouter: AppRouter, private val routingFeatureToggle: RoutingFeatureToggle, @@ -182,7 +178,7 @@ internal class WalletModel @Inject constructor( ) } .onEach(::updateWallets) - .flowOn(dispatchers.main) + .flowOn(dispatchers.default) .launchIn(modelScope) .saveIn(walletsUpdateJobHolder) } @@ -370,41 +366,43 @@ internal class WalletModel @Inject constructor( ), ) + fetchWalletContent(userWallet = action.selectedWallet) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, coroutineScope = modelScope, ) - fetchIfSingleWallet(action.selectedWallet) + val otherWallets = action.wallets.minus(action.selectedWallet) + + otherWallets.onEach { userWallet -> + modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } + } if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { - withContext(dispatchers.io) { delay(timeMillis = 1_800) } + val direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { + Direction.RIGHT + } else { + Direction.LEFT + } - walletEventSender.send( - event = WalletEvent.DemonstrateWalletsScrollPreview( - direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { - Direction.RIGHT - } else { - Direction.LEFT - }, - ), - ) + demonstrateWalletsScrollPreview(direction = direction) } } - private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { + private suspend fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { walletScreenContentLoader.cancel(action.prevWalletId) tokenListStore.remove(action.prevWalletId) + fetchWalletContent(userWallet = action.selectedWallet) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, coroutineScope = modelScope, ) - fetchIfSingleWallet(userWallet = action.selectedWallet) - stateHolder.update( ReinitializeWalletTransformer( prevWalletId = action.prevWalletId, @@ -415,15 +413,15 @@ internal class WalletModel @Inject constructor( ) } - private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { + private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { + fetchWalletContent(userWallet = action.selectedWallet) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, coroutineScope = modelScope, ) - fetchIfSingleWallet(userWallet = action.selectedWallet) - stateHolder.update( AddWalletTransformer( userWallet = action.selectedWallet, @@ -494,6 +492,16 @@ internal class WalletModel @Inject constructor( ) } + private fun demonstrateWalletsScrollPreview(direction: Direction) { + modelScope.launch(dispatchers.mainImmediate) { + delay(timeMillis = 1_800) + + walletEventSender.send( + event = WalletEvent.DemonstrateWalletsScrollPreview(direction = direction), + ) + } + } + private fun scrollToWallet(prevIndex: Int, newIndex: Int, onConsume: () -> Unit = {}) { // Should not show scroll animation if WalletScreen isn't in the background. if (screenLifecycleProvider.isBackgroundState.value) { @@ -521,16 +529,25 @@ internal class WalletModel @Inject constructor( } } + private suspend fun fetchWalletContent(userWallet: UserWallet) { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + /* + * Updating the balance of the current wallet is an essential part of InitializationWallets, + * so the coroutine is launched in the current context + */ + supervisorScope { + launch { walletContentFetcher(userWalletId = userWallet.walletId) } + } + } else { + fetchIfSingleWallet(userWallet = userWallet) + } + } + private fun fetchIfSingleWallet(userWallet: UserWallet) { - modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) - } else { - if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId) - .onLeft { Timber.e(it.toString()) } - } + if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { + modelScope.launch { + fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId) + .onLeft { Timber.e(it.toString()) } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 759a4ac87b..fb53073780 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -12,13 +12,12 @@ import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletContentFetcher import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -30,7 +29,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -47,7 +45,7 @@ internal class WalletClickIntents @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, + private val walletContentFetcher: WalletContentFetcher, private val tokensFeatureToggles: TokensFeatureToggles, private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -92,6 +90,10 @@ internal class WalletClickIntents @Inject constructor( stateHolder.update { it.copy(selectedWalletIndex = index) } maybeUserWallet.onRight { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + launch { walletContentFetcher(userWalletId = it.walletId) } + } + walletScreenContentLoader.load( userWallet = it, clickIntents = this@WalletClickIntents, @@ -131,21 +133,27 @@ internal class WalletClickIntents @Inject constructor( ) modelScope.launch { - val maybeFetchResult = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher.invoke(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .mapLeft { - Timber.e(it) - TokenListError.DataError(it) - } + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) } else { val isSingleWalletWithToken = userWallet is UserWallet.Cold && userWallet.cardTypesResolver.isSingleWalletWithToken() - if (isSingleWalletWithToken) { + val maybeFetchResult = if (isSingleWalletWithToken) { fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) } else { fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) } + + maybeFetchResult.onLeft { + stateHolder.update( + SetTokenListErrorTransformer( + selectedWallet = userWallet, + error = it, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + ), + ) + } } buildList { @@ -159,16 +167,6 @@ internal class WalletClickIntents @Inject constructor( } .awaitAll() - maybeFetchResult.onLeft { - stateHolder.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = it, - appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - ), - ) - } - stateHolder.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) @@ -186,11 +184,11 @@ internal class WalletClickIntents @Inject constructor( modelScope.launch { if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) + walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) } else { fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) } + onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt new file mode 100644 index 0000000000..5ce0f3ecb1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt @@ -0,0 +1,75 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveInAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Wallet content fetcher + * + * @property walletBalanceFetcher fetcher for wallet balance + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class WalletContentFetcher @Inject constructor( + private val walletBalanceFetcher: WalletBalanceFetcher, + private val dispatchers: CoroutineDispatcherProvider, +) { + + private val fetchingJobMap = ConcurrentHashMap() + private val mutex = Mutex() + + suspend operator fun invoke(userWalletId: UserWalletId, forceUpdate: Boolean = false) = supervisorScope { + withContext(dispatchers.default) { + // Use mutex to ensure thread safety + val jobHolder = mutex.withLock { + val savedJobHolder = fetchingJobMap.getOrPut(key = userWalletId, defaultValue = ::JobHolder) + + /* + * If this is not a forced update and there is a saved job in the cache + * (doesn't matter if it is active or not), then skip the update process. + */ + if (!forceUpdate && savedJobHolder != null && !savedJobHolder.isEmpty()) { + Timber.d("Skip fetching for $userWalletId") + + return@withContext + } + + /* + * If this is a forced update and there is already a job in the cache that is updating the balance, + * then cancel the previous update. + */ + if (forceUpdate && savedJobHolder?.isActive == true) { + Timber.d("Cancel old fetching for $userWalletId") + + savedJobHolder.cancel() + } + + JobHolder().also { fetchingJobMap[userWalletId] = it } + } + + Timber.d("Start fetching for $userWalletId") + + val maybeResult = launch { + walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + .onLeft(Timber::e) + } + .saveInAndJoin(jobHolder) + + Timber.d("Finish fetching with result $maybeResult for $userWalletId") + } + } +} \ No newline at end of file