Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-07 20:47:47 +08:00
parent 9cac096ce2
commit 65d9b4818b
20 changed files with 769 additions and 200 deletions

View file

@ -13,6 +13,7 @@ import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
@ -68,25 +69,29 @@ internal class DefaultNetworksRepository(
networks: Set<Network.ID>,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId),
skipCache = refresh,
block = { fetchNetworksStatuses(userWalletId, networks) },
)
}
private suspend fun fetchNetworksStatuses(userWalletId: UserWalletId, networks: Set<Network.ID>) {
coroutineScope {
networks
.map { networkId ->
async {
fetchNetworkStatus(userWalletId, networkId)
fetchNetworkStatusIfCacheExpired(userWalletId, networkId, refresh)
}
}
.awaitAll()
}
}
private suspend fun fetchNetworkStatusIfCacheExpired(
userWalletId: UserWalletId,
networkId: Network.ID,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId, networkId),
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, networkId) },
)
}
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
val currencies = getCurrencies(userWalletId)
.asSequence()
@ -97,6 +102,17 @@ internal class DefaultNetworksRepository(
networkId = networkId,
extraTokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
// Invalidate cache key if wallet manager update failed
when (result) {
is UpdateWalletManagerResult.Verified,
is UpdateWalletManagerResult.NoAccount,
-> Unit
is UpdateWalletManagerResult.Unreachable,
is UpdateWalletManagerResult.MissedDerivation,
-> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkId))
}
val networkStatus = networkStatusFactory.createNetworkStatus(
networkId = networkId,
result = result,
@ -126,5 +142,7 @@ internal class DefaultNetworksRepository(
}
}
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId"
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, nerworkId: Network.ID): String {
return "network_status_${userWalletId}_${nerworkId.value}"
}
}

View file

@ -8,7 +8,8 @@ import com.tangem.domain.wallets.models.GetSelectedWalletError
import com.tangem.domain.wallets.models.UserWallet
/**
* Use case for getting selected wallet
* Use case for getting selected wallet.
* Important! If all wallets is locked, use case returns a error.
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
*

View file

@ -0,0 +1,50 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import com.tangem.common.Provider
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletDeleteStateConverter.DeleteWalletModel
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/**
* Converter that responds on wallet deleting action. Returns [WalletState] without deleted wallet.
*
* @property currentStateProvider current state provider
*/
internal class WalletDeleteStateConverter(
private val currentStateProvider: Provider<WalletState>,
) : Converter<DeleteWalletModel, WalletState> {
override fun convert(value: DeleteWalletModel): WalletState {
return when (val state = currentStateProvider()) {
is WalletState.ContentState -> {
value.cacheState.copySealed(
walletsListConfig = state.walletsListConfig.copy(
selectedWalletIndex = value.action.selectedWalletIndex,
wallets = state.walletsListConfig.wallets.deleteWallet(id = value.action.deletedWalletId),
),
pullToRefreshConfig = value.cacheState.pullToRefreshConfig.copy(isRefreshing = false),
)
}
is WalletState.Initial -> state
}
}
private fun List<WalletCardState>.deleteWallet(id: UserWalletId): ImmutableList<WalletCardState> {
return this
.mapIndexedNotNull { index, currentWallet ->
if (currentWallet.id == id) return@mapIndexedNotNull null
getOrNull(index) ?: return@mapIndexedNotNull null
}
.toImmutableList()
}
data class DeleteWalletModel(
val cacheState: WalletState.ContentState,
val action: WalletsUpdateActionResolver.Action.DeleteWallet,
)
}

View file

@ -1,110 +1,78 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import com.tangem.common.Provider
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
internal class WalletLockedConverter(
private val currentStateProvider: Provider<WalletState>,
private val currentCardTypeResolverProvider: Provider<CardTypesResolver>,
private val currentWalletProvider: Provider<UserWallet>,
private val clickIntents: WalletClickIntents,
) : Converter<Unit, WalletState> {
override fun convert(value: Unit): WalletState {
return when (val state = currentStateProvider()) {
is WalletState.ContentState -> {
val cardTypeResolver = currentCardTypeResolverProvider()
if (cardTypeResolver.isMultiwalletAllowed()) {
state.toMultiCurrencyLockedState(cardTypeResolver)
} else {
state.toSingleCurrencyLockedState(cardTypeResolver)
}
}
is WalletState.Initial -> state
is WalletMultiCurrencyState.Content -> state.toMultiCurrencyLockedState()
is WalletSingleCurrencyState.Content -> state.toSingleCurrencyLockedState()
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
is WalletState.Initial,
-> state
}
}
private fun WalletState.ContentState.toMultiCurrencyLockedState(
cardTypeResolver: CardTypesResolver,
): WalletMultiCurrencyState.Locked {
private fun WalletMultiCurrencyState.Content.toMultiCurrencyLockedState(): WalletState {
return WalletMultiCurrencyState.Locked(
onBackClick = onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(cardTypeResolver),
pullToRefreshConfig = pullToRefreshConfig,
topBarConfig = topBarConfig.updateCallback(),
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick,
onUnlockClick = clickIntents::onUnlockWalletClick,
onScanClick = clickIntents::onScanCardClick,
onScanClick = clickIntents::onScanToUnlockWalletClick,
)
}
private fun WalletState.ContentState.toSingleCurrencyLockedState(
cardTypeResolver: CardTypesResolver,
): WalletSingleCurrencyState.Locked {
private fun WalletSingleCurrencyState.Content.toSingleCurrencyLockedState(): WalletState {
return WalletSingleCurrencyState.Locked(
onBackClick = onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(cardTypeResolver),
pullToRefreshConfig = pullToRefreshConfig,
buttons = createButtons(),
topBarConfig = topBarConfig.updateCallback(),
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
buttons = buttons.disableButtons(),
onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick,
onUnlockClick = clickIntents::onUnlockWalletClick,
onScanClick = clickIntents::onScanCardClick,
onScanClick = clickIntents::onScanToUnlockWalletClick,
onExploreClick = clickIntents::onExploreClick,
)
}
private fun WalletState.ContentState.createTopBarConfig(): WalletTopBarConfig {
return topBarConfig.copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick)
private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig {
return copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick)
}
private fun WalletState.ContentState.createWalletsListConfig(
cardTypeResolver: CardTypesResolver,
): WalletsListConfig {
return walletsListConfig.copy(
wallets = walletsListConfig.wallets
.map { walletCardState ->
WalletCardState.LockedContent(
id = walletCardState.id,
title = walletCardState.title,
additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) {
WalletAdditionalInfoFactory.resolve(
cardTypesResolver = cardTypeResolver,
wallet = currentWalletProvider(),
)
} else {
null
},
imageResId = walletCardState.imageResId,
onRenameClick = walletCardState.onRenameClick,
onDeleteClick = walletCardState.onDeleteClick,
)
private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig {
return copy(isRefreshing = false)
}
private fun PersistentList<WalletManageButton>.disableButtons(): PersistentList<WalletManageButton> {
return this
.map { button ->
when (button) {
is WalletManageButton.Buy -> button.copy(enabled = false)
is WalletManageButton.Sell -> button.copy(enabled = false)
is WalletManageButton.Send -> button.copy(enabled = false)
is WalletManageButton.Swap -> button.copy(enabled = false)
is WalletManageButton.Receive -> button
}
.toImmutableList(),
)
}
private fun createButtons(): PersistentList<WalletManageButton> {
return persistentListOf(
WalletManageButton.Buy(enabled = false, onClick = {}),
WalletManageButton.Send(enabled = false, onClick = {}),
WalletManageButton.Receive(onClick = {}),
WalletManageButton.Sell(enabled = false, onClick = {}),
)
}
.toPersistentList()
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import com.tangem.common.Provider
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
internal class WalletRenameStateConverter(
private val currentStateProvider: Provider<WalletState>,
) : Converter<String, WalletState> {
override fun convert(value: String): WalletState {
return when (val state = currentStateProvider()) {
is WalletState.ContentState -> {
state.copySealed(
walletsListConfig = state.walletsListConfig.renameSelectedWallet(name = value),
)
}
is WalletState.Initial -> state
}
}
private fun WalletsListConfig.renameSelectedWallet(name: String): WalletsListConfig {
return copy(
wallets = wallets
.mapIndexed { index, walletCard ->
if (index == selectedWalletIndex) walletCard.copySealed(title = name) else walletCard
}
.toImmutableList(),
)
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
@ -26,22 +27,31 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val currentWalletProvider: Provider<UserWallet>,
private val currencyStatusErrorConverter: CurrencyStatusErrorConverter,
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, WalletSingleCurrencyState.Content> {
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, WalletState> {
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): WalletSingleCurrencyState.Content {
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): WalletState {
return value.fold(
ifLeft = currencyStatusErrorConverter::convert,
ifRight = ::convertContent,
)
}
private fun convertContent(status: CryptoCurrencyStatus): WalletSingleCurrencyState.Content {
val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
val currencyName = state.marketPriceBlockState.currencyName
return state.copy(
walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state),
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
)
private fun convertContent(status: CryptoCurrencyStatus): WalletState {
return when (val state = currentStateProvider()) {
is WalletSingleCurrencyState.Content -> {
val currencyName = state.marketPriceBlockState.currencyName
state.copy(
walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state),
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
)
}
is WalletMultiCurrencyState.Content,
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
is WalletState.Initial,
-> state
}
}
private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState {

View file

@ -1,8 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import androidx.annotation.DrawableRes
import com.tangem.common.Provider
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
@ -33,12 +35,12 @@ internal class WalletSkeletonStateConverter(
) : Converter<SkeletonModel, WalletState.ContentState> {
override fun convert(value: SkeletonModel): WalletState.ContentState {
val cardTypeResolver = value.wallets[value.selectedWalletIndex].scanResponse.cardTypesResolver
val selectedWallet = value.wallets[value.selectedWalletIndex]
return if (cardTypeResolver.isMultiwalletAllowed()) {
return if (selectedWallet.isMultiCurrency) {
createMultiCurrencyState(value = value)
} else {
createSingleCurrencyState(value = value, currencyName = cardTypeResolver.getBlockchain().currency)
createSingleCurrencyState(value = value, currencyName = selectedWallet.getPrimaryCurrencyName())
}
}
@ -74,6 +76,10 @@ internal class WalletSkeletonStateConverter(
)
}
private fun UserWallet.getPrimaryCurrencyName(): String {
return scanResponse.cardTypesResolver.getBlockchain().currency
}
private fun createTopBarConfig(): WalletTopBarConfig {
return WalletTopBarConfig(
onScanCardClick = clickIntents::onScanCardClick,
@ -84,46 +90,63 @@ internal class WalletSkeletonStateConverter(
private fun createWalletsListConfig(value: SkeletonModel): WalletsListConfig {
return WalletsListConfig(
selectedWalletIndex = value.selectedWalletIndex,
wallets = value.wallets.map(::createWalletState).toImmutableList(),
wallets = value.wallets.mapIndexed(::createWalletCardState).toImmutableList(),
onWalletChange = clickIntents::onWalletChange,
)
}
private fun createWalletState(wallet: UserWallet): WalletCardState {
val state = currentStateProvider()
// If it isn't first initialization (example, when user unlocks wallet)
return if (state is WalletState.ContentState) {
val initializedWallet = state.walletsListConfig.wallets.first { it.id == wallet.walletId }
// If wallet is initialized, return it, otherwise return loading state
if (initializedWallet !is WalletCardState.Loading) {
initializedWallet.copySealed(title = wallet.name)
} else {
createWalletLoadingState(wallet)
}
} else {
createWalletLoadingState(wallet)
}
/**
* Create wallet card state by [index] and [wallet].
* If current wallet card state is initialized, then method returns it.
* Otherwise, returns loading wallet card state.
*/
private fun createWalletCardState(index: Int, wallet: UserWallet): WalletCardState {
return currentStateProvider().getInitializedWalletCardState(index) ?: wallet.mapToWalletCardState()
}
private fun createWalletLoadingState(wallet: UserWallet): WalletCardState {
val cardTypeResolver = wallet.scanResponse.cardTypesResolver
private fun WalletState.getInitializedWalletCardState(index: Int): WalletCardState? {
return (this as? WalletState.ContentState)?.walletsListConfig?.wallets?.getOrNull(index)
}
return WalletCardState.Loading(
id = wallet.walletId,
title = wallet.name,
additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) {
WalletAdditionalInfoFactory.resolve(cardTypesResolver = cardTypeResolver, wallet = wallet)
} else {
null
},
imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver),
private fun UserWallet.mapToWalletCardState(): WalletCardState {
return if (isLocked) mapToLockedWalletCardState() else mapToLoadingWalletCardState()
}
private fun UserWallet.mapToLockedWalletCardState(): WalletCardState {
return WalletCardState.LockedContent(
id = walletId,
title = name,
additionalInfo = createAdditionalInfo(),
imageResId = createImageResId(),
onRenameClick = clickIntents::onRenameClick,
onDeleteClick = clickIntents::onDeleteClick,
)
}
private fun UserWallet.mapToLoadingWalletCardState(): WalletCardState {
return WalletCardState.Loading(
id = walletId,
title = name,
additionalInfo = createAdditionalInfo(),
imageResId = createImageResId(),
onRenameClick = clickIntents::onRenameClick,
onDeleteClick = clickIntents::onDeleteClick,
)
}
private fun UserWallet.createAdditionalInfo(): TextReference? {
return if (isMultiCurrency) {
WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this)
} else {
null
}
}
@DrawableRes
private fun UserWallet.createImageResId(): Int? {
return WalletImageResolver.resolve(cardTypesResolver = scanResponse.cardTypesResolver)
}
private fun createPullToRefreshConfig(): WalletPullToRefreshConfig {
return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe)
}

View file

@ -25,6 +25,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.Wal
import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
@ -45,8 +46,15 @@ internal class WalletStateFactory(
) {
private val tokenActionsProvider by lazy { TokenActionsProvider(clickIntents) }
private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) }
private val walletsUnlockStateConverter by lazy { WalletsUnlockStateConverter(currentStateProvider, clickIntents) }
private val walletRenameStateConverter by lazy { WalletRenameStateConverter(currentStateProvider) }
private val walletDeleteStateConverter by lazy { WalletDeleteStateConverter(currentStateProvider) }
private val tokenListErrorConverter by lazy {
TokenListErrorConverter(currentStateProvider)
}
@ -92,8 +100,6 @@ internal class WalletStateFactory(
private val lockedConverter by lazy {
WalletLockedConverter(
currentStateProvider = currentStateProvider,
currentCardTypeResolverProvider = currentCardTypeResolverProvider,
currentWalletProvider = currentWalletProvider,
clickIntents = clickIntents,
)
}
@ -123,6 +129,21 @@ internal class WalletStateFactory(
)
}
fun getStateWithUpdatedWalletName(name: String): WalletState = walletRenameStateConverter.convert(value = name)
fun getUnlockedState(action: WalletsUpdateActionResolver.Action.UnlockWallet): WalletState {
return walletsUnlockStateConverter.convert(value = action)
}
fun getStateWithoutDeletedWallet(
cacheState: WalletState.ContentState,
action: WalletsUpdateActionResolver.Action.DeleteWallet,
): WalletState {
return walletDeleteStateConverter.convert(
value = WalletDeleteStateConverter.DeleteWalletModel(cacheState = cacheState, action = action),
)
}
fun getStateByTokensList(maybeTokenList: Either<TokenListError, TokenList>): WalletState {
return loadedTokensListConverter.convert(maybeTokenList)
}

View file

@ -0,0 +1,142 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import com.tangem.common.Provider
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
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.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver.Action.UnlockWallet as UnlockWalletAction
/**
* Converter that responds on wallets unlocking action. Returns [WalletState] with unlocked wallets.
*
* @property currentStateProvider current ui state provider
* @property clickIntents screen click intents
*
[REDACTED_AUTHOR]
*/
internal class WalletsUnlockStateConverter(
private val currentStateProvider: Provider<WalletState>,
private val clickIntents: WalletClickIntents,
) : Converter<UnlockWalletAction, WalletState> {
override fun convert(value: UnlockWalletAction): WalletState {
return when (val state = currentStateProvider()) {
is WalletMultiCurrencyState.Locked -> state.toMultiCurrencyContentState(value)
is WalletSingleCurrencyState.Locked -> state.toSingleCurrencyContentState(value)
is WalletState.Initial,
is WalletMultiCurrencyState.Content,
is WalletSingleCurrencyState.Content,
-> state
}
}
private fun WalletMultiCurrencyState.Locked.toMultiCurrencyContentState(action: UnlockWalletAction): WalletState {
return WalletMultiCurrencyState.Content(
onBackClick = onBackClick,
topBarConfig = topBarConfig.updateCallback(),
walletsListConfig = walletsListConfig.unlockWallets(action),
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
tokensListState = WalletTokensListState.Loading(),
notifications = persistentListOf(),
bottomSheetConfig = null,
tokenActionsBottomSheet = null,
onManageTokensClick = clickIntents::onManageTokensClick,
)
}
private fun WalletSingleCurrencyState.Locked.toSingleCurrencyContentState(action: UnlockWalletAction): WalletState {
return WalletSingleCurrencyState.Content(
onBackClick = onBackClick,
topBarConfig = topBarConfig.updateCallback(),
walletsListConfig = walletsListConfig.unlockWallets(action),
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
notifications = persistentListOf(),
bottomSheetConfig = null,
buttons = buttons,
marketPriceBlockState = MarketPriceBlockState.Loading(
currencyName = action.selectedWallet.getPrimaryCurrencyName(),
),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
),
),
)
}
private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig {
return copy(onMoreClick = clickIntents::onDetailsClick)
}
private fun WalletsListConfig.unlockWallets(action: UnlockWalletAction): WalletsListConfig {
return this.copy(
selectedWalletIndex = action.selectedWalletIndex,
wallets = wallets.unlockWallets(action),
)
}
private fun List<WalletCardState>.unlockWallets(action: UnlockWalletAction): ImmutableList<WalletCardState> {
return this
.map { prevWallet ->
if (prevWallet is WalletCardState.LockedContent && action.isUnlockedWallet(prevWallet.id)) {
prevWallet.mapToLoadingWalletCardState(
userWallet = action.getUnlockWallet(prevWallet.id),
)
} else {
prevWallet
}
}
.toImmutableList()
}
private fun UnlockWalletAction.isUnlockedWallet(walletId: UserWalletId): Boolean {
return unlockedWallets.any { it.walletId == walletId }
}
private fun UnlockWalletAction.getUnlockWallet(walletId: UserWalletId): UserWallet {
return unlockedWallets.firstOrNull { it.walletId == walletId }
?: error("Unlocked wallet with id $walletId not found")
}
private fun WalletCardState.mapToLoadingWalletCardState(userWallet: UserWallet): WalletCardState {
return WalletCardState.Loading(
id = id,
title = title,
additionalInfo = userWallet.createAdditionalInfo(),
imageResId = WalletImageResolver.resolve(cardTypesResolver = userWallet.scanResponse.cardTypesResolver),
onRenameClick = onRenameClick,
onDeleteClick = onDeleteClick,
)
}
private fun UserWallet.createAdditionalInfo(): TextReference? {
return if (isMultiCurrency) {
WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this)
} else {
null
}
}
private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig {
return copy(isRefreshing = false)
}
private fun UserWallet.getPrimaryCurrencyName(): String {
return scanResponse.cardTypesResolver.getBlockchain().currency
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
@ -41,18 +42,34 @@ internal class WalletLoadedTxHistoryConverter(
}
private fun convertError(error: TxHistoryListError): WalletState {
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy(
txHistoryState = when (error) {
is TxHistoryListError.DataError -> {
TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick)
}
},
)
return when (val state = currentStateProvider()) {
is WalletSingleCurrencyState.Content -> {
state.copy(
txHistoryState = when (error) {
is TxHistoryListError.DataError -> {
TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick)
}
},
)
}
is WalletMultiCurrencyState.Content,
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
is WalletState.Initial,
-> state
}
}
private fun convert(items: Flow<PagingData<TxHistoryItem>>): WalletState {
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy(
txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items),
)
return when (val state = currentStateProvider()) {
is WalletSingleCurrencyState.Content -> {
state.copy(txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items))
}
is WalletMultiCurrencyState.Content,
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
is WalletState.Initial,
-> state
}
}
}

View file

@ -51,11 +51,11 @@ internal class WalletLoadingTxHistoryConverter(
}
}
private fun convert(value: Int): WalletSingleCurrencyState.Content {
val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
val txHistoryContent = requireNotNull(state.txHistoryState as? Content)
private fun convert(value: Int): WalletState {
val state = currentStateProvider()
val txHistoryContent = (state as? WalletSingleCurrencyState.Content)?.txHistoryState as? Content
txHistoryContent.contentItems.update {
txHistoryContent?.contentItems?.update {
PagingData.from(
data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) +
MutableList(

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -163,22 +164,14 @@ private fun CardContainer(
var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) }
DropdownMenu(
expanded = isMenuVisible,
ManageWalletContextMenu(
isMenuVisible = isMenuVisible,
pressOffset = pressOffset,
itemHeight = itemHeight,
onDismissRequest = { isMenuVisible = false },
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
offset = pressOffset.copy(y = pressOffset.y - itemHeight),
) {
MenuItem(
textResId = R.string.common_rename,
imageVector = Icons.Outlined.Edit,
onClick = {
isMenuVisible = false
isRenameWalletDialogVisible = true
},
)
MenuItem(textResId = R.string.common_delete, imageVector = Icons.Outlined.Delete, onClick = onDeleteClick)
}
onShowRenameWalletDialogClick = { isRenameWalletDialogVisible = true },
onDeleteClick = onDeleteClick,
)
if (isRenameWalletDialogVisible) {
RenameWalletDialogContent(
@ -192,6 +185,41 @@ private fun CardContainer(
}
}
@Suppress("LongParameterList")
@Composable
private fun ManageWalletContextMenu(
isMenuVisible: Boolean,
pressOffset: DpOffset,
itemHeight: Dp,
onDismissRequest: () -> Unit,
onShowRenameWalletDialogClick: () -> Unit,
onDeleteClick: () -> Unit,
) {
DropdownMenu(
expanded = isMenuVisible,
onDismissRequest = onDismissRequest,
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
offset = pressOffset.copy(y = pressOffset.y - itemHeight),
) {
MenuItem(
textResId = R.string.common_rename,
imageVector = Icons.Outlined.Edit,
onClick = {
onDismissRequest()
onShowRenameWalletDialogClick()
},
)
MenuItem(
textResId = R.string.common_delete,
imageVector = Icons.Outlined.Delete,
onClick = {
onDismissRequest()
onDeleteClick()
},
)
}
}
@Composable
private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) {
DropdownMenuItem(

View file

@ -19,7 +19,9 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollec
@Composable
internal fun WalletSideEffects(lazyListState: LazyListState, walletsListConfig: WalletsListConfig) {
LaunchedEffect(key1 = walletsListConfig.selectedWalletIndex) {
lazyListState.scrollToItem(walletsListConfig.selectedWalletIndex)
if (!lazyListState.isScrollInProgress) {
lazyListState.animateScrollToItem(walletsListConfig.selectedWalletIndex)
}
}
val dragInteraction = lazyListState.interactionSource.interactions.collectAsState(initial = null)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
@ -27,7 +28,8 @@ internal class ScrollOffsetCollector(
private val LazyListItemInfo.halfItemSize get() = size.div(other = 2)
override suspend fun emit(value: List<LazyListItemInfo>) {
if (!lazyListState.isScrollInProgress || dragInteraction.value == null || value.size <= 1) return
if (isNotUserInteraction() || value.size <= 1) return
val firstItem = value.firstOrNull() ?: return
val lastItem = value.lastOrNull() ?: return
@ -37,4 +39,12 @@ internal class ScrollOffsetCollector(
callback(lastItem.index - 1)
}
}
/**
* Sometimes the list is scrolled programmatically. Example: selecting a specific wallet when a user opens the
* screen for the first time or scans a new wallet. Therefore [ScrollOffsetCollector] should not respond to changes.
*/
private fun isNotUserInteraction(): Boolean {
return !lazyListState.isScrollInProgress || dragInteraction.value !is DragInteraction.Start
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
@ -20,7 +21,7 @@ internal class TokenListToWalletStateConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val isWalletContentHidden: Boolean,
clickIntents: WalletClickIntents,
) : Converter<TokenList, WalletMultiCurrencyState.Content> {
) : Converter<TokenList, WalletState> {
private val tokenListToContentConverter = TokenListToContentItemsConverter(
isWalletContentHidden = isWalletContentHidden,
@ -28,12 +29,20 @@ internal class TokenListToWalletStateConverter(
clickIntents = clickIntents,
)
override fun convert(value: TokenList): WalletMultiCurrencyState.Content {
val state = requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content)
return state.copy(
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance),
tokensListState = tokenListToContentConverter.convert(value = value),
)
override fun convert(value: TokenList): WalletState {
return when (val state = currentStateProvider()) {
is WalletMultiCurrencyState.Content -> {
state.copy(
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance),
tokensListState = tokenListToContentConverter.convert(value = value),
)
}
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Content,
is WalletSingleCurrencyState.Locked,
is WalletState.Initial,
-> state
}
}
private fun WalletMultiCurrencyState.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig {

View file

@ -12,6 +12,10 @@ internal interface WalletClickIntents : TxHistoryClickIntents {
fun onScanCardClick()
fun onScanCardNotificationClick()
fun onScanToUnlockWalletClick()
fun onDetailsClick()
fun onBackupCardClick()

View file

@ -59,7 +59,7 @@ internal class WalletNotificationsListFactory(
}
if (tokenList != null && tokenList.hasMissedDerivations()) {
add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardClick))
add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardNotificationClick))
}
if (isUserAlreadyRateAppCallback()) {

View file

@ -10,13 +10,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState
*/
internal object WalletStateCache {
private val states = mutableMapOf<UserWalletId, WalletState>()
private val states = mutableMapOf<UserWalletId, WalletState.ContentState>()
/** Get state by [userWalletId] */
fun getState(userWalletId: UserWalletId): WalletState? = states[userWalletId]
fun getState(userWalletId: UserWalletId): WalletState.ContentState? = states[userWalletId]
/** Add or update [state] by [userWalletId] */
fun update(userWalletId: UserWalletId, state: WalletState) {
fun update(userWalletId: UserWalletId, state: WalletState.ContentState) {
states[userWalletId] = state
}
}

View file

@ -47,6 +47,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -63,7 +64,7 @@ import kotlin.properties.Delegates
internal class WalletViewModel @Inject constructor(
private val getWalletsUseCase: GetWalletsUseCase,
private val saveWalletUseCase: SaveWalletUseCase,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val selectWalletUseCase: SelectWalletUseCase,
private val updateWalletUseCase: UpdateWalletUseCase,
private val deleteWalletUseCase: DeleteWalletUseCase,
@ -130,6 +131,11 @@ internal class WalletViewModel @Inject constructor(
private val notificationsJobHolder = JobHolder()
private val refreshContentJobHolder = JobHolder()
private val walletsUpdateActionResolver = WalletsUpdateActionResolver(
currentStateProvider = Provider { uiState },
getSelectedWalletUseCase = getSelectedWalletUseCase,
)
override fun onCreate(owner: LifecycleOwner) {
viewModelScope.launch(dispatchers.main) {
delay(timeMillis = 1_800)
@ -148,29 +154,48 @@ internal class WalletViewModel @Inject constructor(
}
private fun updateWallets(sourceList: List<UserWallet>) {
if (sourceList.isEmpty()) return
wallets = sourceList
val currentState = uiState
val previousSelectedWalletIndex = (currentState as? WalletState.ContentState)
?.walletsListConfig
?.selectedWalletIndex
if (sourceList.isEmpty()) return
val selectedWalletIndex = if (currentState is WalletLockedState) {
currentState.getSelectedWalletIndex()
} else {
val selectedWallet = getSelectedWalletUseCase().fold(
ifLeft = { error("Selected wallet is null") },
ifRight = { it },
)
sourceList.indexOfFirst { it.walletId == selectedWallet.walletId }
when (val action = walletsUpdateActionResolver.resolve(sourceList)) {
is WalletsUpdateActionResolver.Action.InitialWallets -> {
loadAndUpdateState(index = action.selectedWalletIndex)
}
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
uiState = stateFactory.getStateWithUpdatedWalletName(name = action.name)
}
is WalletsUpdateActionResolver.Action.UnlockWallet -> {
uiState = stateFactory.getUnlockedState(action)
getContentItemsUpdates(index = action.selectedWalletIndex)
}
is WalletsUpdateActionResolver.Action.DeleteWallet -> {
deleteWalletAndUpdateState(action = action)
}
is WalletsUpdateActionResolver.Action.AddWallet -> {
loadAndUpdateState(index = action.selectedWalletIndex)
}
is WalletsUpdateActionResolver.Action.Unknown -> Unit
}
}
if (previousSelectedWalletIndex != selectedWalletIndex) {
uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex)
private fun loadAndUpdateState(index: Int) {
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
getContentItemsUpdates(index = selectedWalletIndex)
getContentItemsUpdates(index = index)
}
private fun deleteWalletAndUpdateState(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
val cacheState = WalletStateCache.getState(userWalletId = action.selectedWalletId)
if (cacheState != null) {
uiState = stateFactory.getStateWithoutDeletedWallet(cacheState, action)
if (cacheState.isLoadingState()) {
getContentItemsUpdates(action.selectedWalletIndex)
}
} else {
loadAndUpdateState(index = action.selectedWalletIndex)
}
}
@ -181,23 +206,56 @@ internal class WalletViewModel @Inject constructor(
}
override fun onScanCardClick() {
viewModelScope.launch(dispatchers.io) {
scanCardProcessor.scan()
.doOnSuccess {
// If card's public key is null then user wallet will be null
val userWallet = UserWalletBuilder(scanResponse = it).build()
if (userWallet != null) {
saveWalletUseCase(userWallet = userWallet, canOverride = false)
}
}
}
}
override fun onScanCardNotificationClick() {
scanToUpdateSelectedWallet(
onSuccessSave = {
// Reload currencies with missed derivation
fetchTokenListUseCase(userWalletId = it.walletId)
},
)
}
override fun onScanToUnlockWalletClick() {
scanToUpdateSelectedWallet()
}
private fun scanToUpdateSelectedWallet(onSuccessSave: suspend (UserWallet) -> Unit = {}) {
val state = uiState as? WalletState.ContentState ?: return
val prevRequestPolicyStatus = getBiometricsStatusUseCase()
// Update access the code policy according access code saving status
setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase())
viewModelScope.launch(dispatchers.io) {
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
scanCardProcessor.scan(
cardId = getWallet(state.walletsListConfig.selectedWalletIndex).cardId,
allowsRequestAccessCodeFromRepository = true,
)
.doOnSuccess {
// If card's public key is null then user wallet will be null
val userWallet = UserWalletBuilder(scanResponse = it).build()
if (userWallet != null) {
saveWalletUseCase(userWallet)
saveWalletUseCase(userWallet = userWallet, canOverride = true)
.onLeft {
// Rollback policy if card saving was failed
setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus)
}
.onRight { onSuccessSave(userWallet) }
} else {
// Rollback policy if card saving was failed
setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus)
@ -245,10 +303,7 @@ internal class WalletViewModel @Inject constructor(
}
override fun onWalletChange(index: Int) {
val state = requireNotNull(uiState as? WalletState.ContentState) {
"Impossible to change wallet if state isn't WalletState.ContentState"
}
val state = uiState as? WalletState.ContentState ?: return
if (state.walletsListConfig.selectedWalletIndex == index) return
viewModelScope.launch(dispatchers.io) {
@ -256,15 +311,26 @@ internal class WalletViewModel @Inject constructor(
}
val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id)
if (cacheState != null) {
uiState = if (cacheState is WalletState.ContentState) {
cacheState.copySealed(
walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index),
pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false),
)
} else {
cacheState
}
if (cacheState != null && cacheState !is WalletLockedState) {
uiState = cacheState.copySealed(
walletsListConfig = state.walletsListConfig.copy(
selectedWalletIndex = index,
wallets = state.walletsListConfig.wallets
.mapIndexed { mapIndex, currentWallet ->
val cacheWallet = cacheState.walletsListConfig.wallets.getOrNull(mapIndex)
if (currentWallet is WalletCardState.Loading && cacheWallet != null &&
cacheWallet.isLoaded()
) {
cacheWallet
} else {
currentWallet
}
}
.toImmutableList(),
),
pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false),
)
if (cacheState.isLoadingState()) {
getContentItemsUpdates(index)
@ -275,6 +341,10 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun WalletCardState.isLoaded(): Boolean {
return this !is WalletCardState.Loading && this !is WalletCardState.LockedContent
}
override fun onRefreshSwipe() {
val selectedWalletIndex = (uiState as? WalletState.ContentState)
?.walletsListConfig
@ -436,10 +506,11 @@ internal class WalletViewModel @Inject constructor(
}
override fun onDeleteClick(userWalletId: UserWalletId) {
val state = uiState as? WalletState.ContentState ?: return
viewModelScope.launch(dispatchers.io) {
val either = deleteWalletUseCase(userWalletId)
val state = requireNotNull(uiState as? WalletState.ContentState)
if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) onBackClick()
}
}
@ -611,9 +682,10 @@ internal class WalletViewModel @Inject constructor(
private fun WalletState.isLoadingState(): Boolean {
// Check the base components
if (this is WalletState.ContentState) {
walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading ||
notifications.isEmpty()
if (this is WalletState.ContentState &&
walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading
) {
return true
}
// Check the special components

View file

@ -0,0 +1,161 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import com.tangem.common.Provider
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
/**
* Resolver that determines which update action will be performed
*
* @property currentStateProvider current state provider
* @property getSelectedWalletUseCase use case that returns selected wallet
*/
internal class WalletsUpdateActionResolver(
private val currentStateProvider: Provider<WalletState>,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
) {
fun resolve(wallets: List<UserWallet>): Action {
val selectedWallet = wallets.getSelectedWallet()
return when (val state = currentStateProvider()) {
is WalletState.Initial -> {
Action.InitialWallets(
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
)
}
is WalletState.ContentState -> {
getActionToUpdateContent(state = state, wallets = wallets, selectedWallet = selectedWallet)
}
}
}
private fun List<UserWallet>.getSelectedWallet(): UserWallet {
val hasUnlockedWallet = any { !it.isLocked }
return if (hasUnlockedWallet) {
val selectedWalletId = getSelectedWalletUseCase().fold(ifLeft = ::error, ifRight = UserWallet::walletId)
firstOrNull { it.walletId == selectedWalletId }
?: error("Wallets don't contain a wallet with id: $selectedWalletId")
} else {
lastOrNull() ?: error("Wallets is empty")
}
}
private fun getActionToUpdateContent(
state: WalletState.ContentState,
wallets: List<UserWallet>,
selectedWallet: UserWallet,
): Action {
return if (isWalletsCountChanged(state, wallets)) {
getActionToChangeWallets(state = state, wallets = wallets, selectedWallet = selectedWallet)
} else {
getActionToUpdateCurrentWallet(state = state, wallets = wallets, selectedWallet = selectedWallet)
}
}
private fun isWalletsCountChanged(state: WalletState.ContentState, wallets: List<UserWallet>): Boolean {
val prevWalletsSize = state.walletsListConfig.wallets.size
val walletsSize = wallets.size
return prevWalletsSize != walletsSize
}
private fun getActionToChangeWallets(
state: WalletState.ContentState,
wallets: List<UserWallet>,
selectedWallet: UserWallet,
): Action {
val prevWalletsSize = state.walletsListConfig.wallets.size
return when {
prevWalletsSize > wallets.size -> {
Action.DeleteWallet(
selectedWalletId = selectedWallet.walletId,
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
deletedWalletId = state.walletsListConfig.wallets.getDeletedWalletId(wallets),
)
}
prevWalletsSize < wallets.size -> {
Action.AddWallet(
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
)
}
else -> Action.Unknown
}
}
private fun List<WalletCardState>.getDeletedWalletId(wallets: List<UserWallet>): UserWalletId {
return this
.map(WalletCardState::id)
.firstOrNull { !wallets.map(UserWallet::walletId).contains(it) }
?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids")
}
private fun getActionToUpdateCurrentWallet(
state: WalletState.ContentState,
wallets: List<UserWallet>,
selectedWallet: UserWallet,
): Action {
val selectedWalletName = selectedWallet.name
if (state.getPrevSelectedWalletName() != selectedWalletName) {
return Action.UpdateWalletName(selectedWalletName)
}
if (state is WalletLockedState && !selectedWallet.isLocked) {
return Action.UnlockWallet(
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
selectedWallet = selectedWallet,
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
)
}
return Action.Unknown
}
private fun WalletState.ContentState.getPrevSelectedWalletName(): String {
val prevSelectedWalletIndex = walletsListConfig.selectedWalletIndex
val prevSelectedWallet = walletsListConfig.wallets.getOrNull(prevSelectedWalletIndex)
?: error("Previous selected wallet is not found")
return prevSelectedWallet.title
}
private fun List<UserWallet>.indexOfWallet(id: UserWalletId): Int {
val selectedIndex = indexOfFirst { it.walletId == id }
return if (selectedIndex == -1) {
error("Wallets don't contain a wallet with id: $id")
} else {
selectedIndex
}
}
sealed class Action {
data class InitialWallets(val selectedWalletIndex: Int) : Action()
data class UpdateWalletName(val name: String) : Action()
data class UnlockWallet(
val selectedWalletIndex: Int,
val selectedWallet: UserWallet,
val unlockedWallets: List<UserWallet>,
) : Action()
data class DeleteWallet(
val selectedWalletId: UserWalletId,
val selectedWalletIndex: Int,
val deletedWalletId: UserWalletId,
) : Action()
data class AddWallet(val selectedWalletIndex: Int) : Action()
object Unknown : Action()
}
}