Updated on 2026-08-14
This commit is contained in:
parent
9cac096ce2
commit
65d9b4818b
20 changed files with 769 additions and 200 deletions
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ internal interface WalletClickIntents : TxHistoryClickIntents {
|
|||
|
||||
fun onScanCardClick()
|
||||
|
||||
fun onScanCardNotificationClick()
|
||||
|
||||
fun onScanToUnlockWalletClick()
|
||||
|
||||
fun onDetailsClick()
|
||||
|
||||
fun onBackupCardClick()
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue