Updated on 2026-08-14
This commit is contained in:
commit
29f55aae33
9 changed files with 101 additions and 18 deletions
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
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
|
||||
|
|
@ -41,13 +42,22 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
|||
|
||||
override val fallback: Option<AccountList> = none()
|
||||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun produce(): Flow<AccountList> {
|
||||
return walletAccountListFlowFactory.create(params.userWalletId)
|
||||
val walletId = params.userWalletId
|
||||
logger.i("produce() called for $walletId")
|
||||
return walletAccountListFlowFactory.create(walletId)
|
||||
.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 {
|
||||
|
|
@ -66,4 +76,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
|||
interface Factory : SingleAccountListProducer.Factory {
|
||||
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "SingleAccountListProducer"
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,10 +135,9 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
userWallet: UserWallet,
|
||||
networks: Set<Network>,
|
||||
): Map<Network, List<CryptoCurrency>> {
|
||||
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<CryptoCurrency>() } + existingNetworkWithCurrencies
|
||||
return networks.associateWith { network ->
|
||||
currenciesByNetworkId[network.id].orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getMultiWalletCurrenciesByRawId(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -45,6 +46,13 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
|
|||
networkCurrencies: Set<CryptoCurrency>,
|
||||
xpub: String? = null,
|
||||
): Either<Throwable, Unit> {
|
||||
// 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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<UserWallet>): 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<UserWallet>): List<UserWallet> {
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,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
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue