Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-07 12:16:22 +04:00
parent 76987714e2
commit 74c09ab3ba
6 changed files with 154 additions and 59 deletions

View file

@ -77,6 +77,6 @@
},
{
"name": "WALLET_BALANCE_FETCHER_ENABLED",
"version": "undefined"
"version": "5.27.0"
}
]

View file

@ -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] */

View file

@ -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)

View file

@ -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()) }
}
}
}

View file

@ -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,

View file

@ -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<UserWalletId, JobHolder>()
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")
}
}
}