diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 498b0bcd0d..158fbd8808 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 498b0bcd0d871ed60c43b5d44f646548a4f11d37 +Subproject commit 158fbd8808d2db92ef82d3f9ed92c81340c707c5 diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt index 28fd5fd6be..76b5728dc8 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt @@ -11,9 +11,11 @@ import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -21,6 +23,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** * Default implementation of [SingleAccountListProducer]. @@ -43,23 +46,45 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( override val fallback: Option = none() + private val logger = TangemLogger.withTag(TAG) + @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow { + val walletId = params.userWalletId + logger.i("produce() called for $walletId") val accountListFlow: Flow = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) { - combineWithPaymentAccount() + combineWithPaymentAccount(walletId) } else { - walletAccountListFlowFactory.create(userWalletId = params.userWalletId) + walletAccountListFlowFactory.create(userWalletId = walletId) } - return accountListFlow.flowOn(dispatchers.default) + return accountListFlow + .onEach { accountList -> + logger.i( + "produce()[$walletId] emit: accounts=${accountList.accounts.size}, " + + "currencies=${accountList.flattenMapCurrencies().size}", + ) + } + .flowOn(dispatchers.default) } - private fun combineWithPaymentAccount(): Flow { - return walletAccountListFlowFactory.create(params.userWalletId) + private fun combineWithPaymentAccount(walletId: UserWalletId): Flow { + return walletAccountListFlowFactory.create(walletId) + .onEach { accountList -> + logger.i( + "produce()[$walletId]: accountList received accounts=${accountList.accounts.size}, " + + "currencies=${accountList.flattenMapCurrencies().size}", + ) + } .map { accountList -> - val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) - if (userWallet.isPaymentAccountSupported()) { - accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable -> + val userWallet = userWalletsListRepository.getSyncStrict(id = walletId) + val isPaymentSupported = userWallet.isPaymentAccountSupported() + logger.i( + "produce()[$walletId]: userWallet resolved (type=${userWallet::class.simpleName}), " + + "isPaymentAccountSupported=$isPaymentSupported", + ) + if (isPaymentSupported) { + accountList.plus(Account.Payment(walletId)).getOrElse { throwable -> error("Can not combine account list and payment account status: $throwable") } } else { @@ -77,4 +102,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( interface Factory : SingleAccountListProducer.Factory { override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer } + + private companion object { + const val TAG = "SingleAccountListProducer" + } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 49d1b90eec..33749350a7 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -12,6 +12,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -47,7 +48,15 @@ internal class WalletAccountListFlowFactory @Inject constructor( return accountsResponseStoreFactory.create(userWallet.walletId).data .filterNotNull() - .filter { it.accounts.isNotEmpty() } + .filter { response -> + val hasAccounts = response.accounts.isNotEmpty() + + if (!hasAccounts) { + TangemLogger.e("Account list is empty for ${userWallet.walletId}") + } + + hasAccounts + } .distinctUntilChanged() .map { converter.convert(it) } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index f56f43a761..addc692394 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -135,10 +135,9 @@ internal class DefaultCardCryptoCurrencyFactory( userWallet: UserWallet, networks: Set, ): Map> { - val response = walletAccountsFetcher.getSaved(userWallet.walletId) - ?: return emptyMap() + val response = walletAccountsFetcher.getSaved(userWallet.walletId) ?: return emptyMap() - val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO -> + val currenciesByNetworkId = response.accounts.flatMapTo(hashSetOf()) { accountDTO -> val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() ?: return@flatMapTo emptySet() @@ -152,9 +151,11 @@ internal class DefaultCardCryptoCurrencyFactory( accountIndex = accountIndex, ) } - .groupBy(CryptoCurrency::network) + .groupBy { it.network.id } - return networks.associateWith { emptyList() } + existingNetworkWithCurrencies + return networks.associateWith { network -> + currenciesByNetworkId[network.id].orEmpty() + } } private suspend fun getMultiWalletCurrenciesByRawId( diff --git a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt index 38625e0f4a..8e40ea13d2 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt @@ -1,6 +1,7 @@ package com.tangem.data.networks.fetcher import arrow.core.Either +import arrow.core.right import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.setSourceAsOnlyCache import com.tangem.data.networks.store.storeStatus @@ -44,6 +45,13 @@ internal class CommonNetworkStatusFetcher @Inject constructor( network: Network, networkCurrencies: Set, ): Either { + // Guard: empty networkCurrencies would result in NetworkStatus.Verified(amounts=emptyMap()), + // which overwrites any valid cached status and leaves all currencies in this network as Loading. + if (networkCurrencies.isEmpty()) { + TangemLogger.w("Skipping fetch for $userWalletId [${network.rawId}]: networkCurrencies is empty") + return Unit.right() + } + return Either.catchOn(dispatchers.default) { val result = withContext(dispatchers.io) { walletManagersFacade.update( diff --git a/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt index e6f9bf012d..e9dcb7f749 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt @@ -73,6 +73,32 @@ internal class CommonNetworkStatusFetcherTest { } } + @Test + fun `fetch skips wallet manager update when networkCurrencies is empty`() = runTest { + // Arrange + val userWalletId = UserWalletId("011") + val network = cryptoCurrencyFactory.ethereum.network + + // Act + val actual = fetcher.fetch( + userWalletId = userWalletId, + network = network, + networkCurrencies = emptySet(), + ) + + // Assert: returns success without touching the wallet manager facade — guard prevents storing + // a NetworkStatus.Verified with empty `amounts` over an existing valid cached status. + Truth.assertThat(actual).isEqualTo(Either.Right(Unit)) + + coVerify(inverse = true) { + walletManagersFacade.update( + userWalletId = any(), + network = any(), + extraTokens = any(), + ) + } + } + @ParameterizedTest @ProvideTestModels fun `fetch successfully for any result of walletManagersFacade`(model: SuccessTestModel) = runTest { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 501510ac6a..c632281590 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -68,7 +68,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( isAnyWalletUnlocked(state, wallets) -> { Action.UnlockWallet( selectedWallet = selectedWallet, - unlockedWallets = wallets.filterNot(UserWallet::isLocked), + unlockedWallets = getJustUnlockedWallets(state = state, wallets = wallets), ) } isAnyWalletNameChanged(state, wallets) -> { @@ -232,11 +232,28 @@ internal class WalletsUpdateActionResolver @Inject constructor( private fun isAnyWalletUnlocked(state: WalletScreenState, wallets: List): Boolean { return state.wallets.any { walletState -> val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } ?: return@any false - !wallet.isLocked && - (walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked) + wallet.isJustUnlockedFrom(walletState) } } + /** + * Wallets that transitioned from a locked UI state to unlocked data state in this update cycle. + * Does not include wallets that were already unlocked before — otherwise downstream handlers + * (e.g. [com.tangem.feature.wallet.child.wallet.model.WalletModel.unlockWallet]) would re-fetch + * already-loaded wallets and emit redundant transformer state changes. + */ + private fun getJustUnlockedWallets(state: WalletScreenState, wallets: List): List { + return state.wallets.mapNotNull { walletState -> + val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } + wallet?.takeIf { it.isJustUnlockedFrom(walletState) } + } + } + + private fun UserWallet.isJustUnlockedFrom(walletState: WalletState): Boolean { + return !isLocked && + (walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked) + } + private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { if (selectedWallet !is UserWallet.Cold) return false val prevSelectedWallet = state.getPrevSelectedWallet() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index d56549b7fd..ad7df36052 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -8,12 +8,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletStateTransformer import com.tangem.utils.extensions.indexOfFirstOrNull +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -41,7 +42,14 @@ internal class WalletStateController @Inject constructor( } fun update(transformer: WalletScreenStateTransformer) { - TangemLogger.d("Applying: ${transformer::class.simpleName}") + val walletStateTransformer = transformer as? WalletStateTransformer + val maybeWalletId = if (walletStateTransformer != null) { + " for ${walletStateTransformer.userWalletId}" + } else { + "" + } + + TangemLogger.d("Applying: ${transformer::class.simpleName}$maybeWalletId") mutableUiState.update(function = transformer::transform) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 6e44868ca7..a8e39cbf17 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -67,7 +67,7 @@ internal class UnlockWalletTransformer( is WalletState.MultiCurrency.Content, is WalletState.SingleCurrency.Content, -> { - TangemLogger.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("createLoadingState: Impossible to unlock wallet with not locked state") prevState } } @@ -79,7 +79,7 @@ internal class UnlockWalletTransformer( userWallet = unlockedWallet, ) is WalletUM.Content -> { - TangemLogger.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("createLoadingState2: Impossible to unlock wallet with not locked state") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index 2790e44084..437b7f9493 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList internal abstract class WalletStateTransformer( - protected val userWalletId: UserWalletId, + internal val userWalletId: UserWalletId, ) : WalletScreenStateTransformer { abstract fun transform(prevState: WalletState): WalletState