Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-19 09:16:46 +03:00
commit 6437018fc8
1160 changed files with 35466 additions and 6540 deletions

View file

@ -7,6 +7,14 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCaseV2
import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCaseV2
import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCaseV2
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -27,6 +35,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken
import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance
import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter
import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -46,6 +55,12 @@ internal class OrganizeTokensModel @Inject constructor(
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val toggleTokenListGroupingUseCaseV2: ToggleTokenListGroupingUseCaseV2,
private val toggleTokenListSortingUseCaseV2: ToggleTokenListSortingUseCaseV2,
private val applyTokenListSortingUseCaseV2: ApplyTokenListSortingUseCaseV2,
) : Model(), OrganizeTokensIntents {
private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow()
@ -56,15 +71,24 @@ internal class OrganizeTokensModel @Inject constructor(
listStateProvider = Provider { uiState.value.itemsState },
)
private val dragAndDropAdapterV2 = DragAndDropAdapterV2(
tokenListUMProvider = Provider { uiState.value.tokenListUM },
)
private val stateHolder = OrganizeTokensStateHolder(
intents = this,
dragAndDropIntents = dragAndDropAdapter,
dragAndDropAdapterV2 = dragAndDropAdapterV2,
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
accountsFeatureToggles = accountsFeatureToggles,
)
private val userWalletId = paramsContainer.require<OrganizeTokensComponent.Params>().userWalletId
private var cachedTokenList: TokenList? = null
private var cachedAccountStatusList: AccountStatusList? = null
private var isAccountsModeEnabled: Boolean = false
val uiState: StateFlow<OrganizeTokensState> = stateHolder.stateFlow
@ -89,59 +113,109 @@ internal class OrganizeTokensModel @Inject constructor(
}
override fun onSortClick() {
val list = cachedTokenList ?: return
if (list.sortedBy == TokensSortType.BALANCE) return
if (accountsFeatureToggles.isFeatureEnabled) {
val list = cachedAccountStatusList ?: return
if (list.sortType == TokensSortType.BALANCE) return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance)
modelScope.launch {
toggleTokenListSortingUseCase(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSorting(it)
cachedTokenList = it
},
)
modelScope.launch {
toggleTokenListSortingUseCaseV2(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled)
cachedAccountStatusList = it
},
)
}
} else {
val list = cachedTokenList ?: return
if (list.sortedBy == TokensSortType.BALANCE) return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance)
modelScope.launch {
toggleTokenListSortingUseCase(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSorting(it)
cachedTokenList = it
},
)
}
}
}
override fun onGroupClick() {
val list = cachedTokenList ?: return
if (accountsFeatureToggles.isFeatureEnabled) {
val list = cachedAccountStatusList ?: return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group)
modelScope.launch {
toggleTokenListGroupingUseCase(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSorting(it)
cachedTokenList = it
},
)
modelScope.launch {
toggleTokenListGroupingUseCaseV2(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled)
cachedAccountStatusList = it
},
)
}
} else {
val list = cachedTokenList ?: return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group)
modelScope.launch {
toggleTokenListGroupingUseCase(list).fold(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateAfterTokenListSorting(it)
cachedTokenList = it
},
)
}
}
}
override fun onApplyClick() {
modelScope.launch {
stateHolder.updateStateToDisplayProgress()
val listState = uiState.value.itemsState
val resolver = CryptoCurrenciesIdsResolver()
val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork
val isSortedByBalance = uiState.value.header.isSortedByBalance
sendAnalyticsEvent(
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
val result = if (accountsFeatureToggles.isFeatureEnabled) {
val tokensListUM = uiState.value.tokenListUM
val result = applyTokenListSortingUseCase(
userWalletId = userWalletId,
sortedTokensIds = resolver.resolve(listState, cachedTokenList),
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
val isGroupedByNetwork = tokensListUM.isGrouped
sendAnalyticsEvent(
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
applyTokenListSortingUseCaseV2(
sortedTokensIdsByAccount = resolver.resolveV2(tokensListUM, cachedAccountStatusList),
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
} else {
val listState = uiState.value.itemsState
val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork
sendAnalyticsEvent(
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
applyTokenListSortingUseCase(
userWalletId = userWalletId,
sortedTokensIds = resolver.resolve(listState, cachedTokenList),
isGroupedByNetwork = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
}
result.fold(
ifLeft = stateHolder::updateStateWithError,
@ -161,10 +235,24 @@ internal class OrganizeTokensModel @Inject constructor(
private fun bootstrapTokenList() {
modelScope.launch {
val tokenList = getTokenList() ?: return@launch
if (accountsFeatureToggles.isFeatureEnabled) {
val accountList = singleAccountStatusListSupplier.getSyncOrNull(
SingleAccountStatusListProducer.Params(userWalletId),
) ?: return@launch
stateHolder.updateStateWithTokenList(tokenList)
cachedTokenList = tokenList
isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync()
stateHolder.updateStateWithAccountList(
accountStatusList = accountList,
isAccountsModeEnabled = isAccountsModeEnabled,
)
cachedAccountStatusList = accountList
} else {
val tokenList = getTokenList() ?: return@launch
stateHolder.updateStateWithTokenList(tokenList)
cachedTokenList = tokenList
}
}
}
@ -180,14 +268,25 @@ internal class OrganizeTokensModel @Inject constructor(
}
private fun bootstrapDragAndDropUpdates() {
dragAndDropAdapter.dragAndDropUpdates
.distinctUntilChanged()
.onEach { (type, updatedListState) ->
disableSortingByBalanceIfListChanged(type)
if (accountsFeatureToggles.isFeatureEnabled) {
dragAndDropAdapterV2.dragAndDropUpdates
.distinctUntilChanged()
.onEach { (type, updatedListState) ->
disableSortingByBalanceIfListChangedV2(type)
stateHolder.updateStateWithManualSorting(updatedListState)
}
.launchIn(modelScope)
stateHolder.updateStateWithManualSortingV2(updatedListState)
}
.launchIn(modelScope)
} else {
dragAndDropAdapter.dragAndDropUpdates
.distinctUntilChanged()
.onEach { (type, updatedListState) ->
disableSortingByBalanceIfListChanged(type)
stateHolder.updateStateWithManualSorting(updatedListState)
}
.launchIn(modelScope)
}
}
private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) {
@ -199,6 +298,15 @@ internal class OrganizeTokensModel @Inject constructor(
}
}
private fun disableSortingByBalanceIfListChangedV2(dragOperationType: DragAndDropAdapterV2.DragOperation.Type) {
if (dragOperationType !is DragAndDropAdapterV2.DragOperation.Type.End) return
if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) {
cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE)
stateHolder.disableSortingByBalance()
}
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->

View file

@ -9,6 +9,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
@ -18,6 +19,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.settings.*
@ -93,7 +95,9 @@ internal class WalletModel @Inject constructor(
private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
private val tangemPayOnboardingRepository: OnboardingRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountsFeatureToggles: AccountsFeatureToggles,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
) : Model() {
@ -113,7 +117,13 @@ internal class WalletModel @Inject constructor(
init {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened)
suggestToEnableBiometrics()
screenLifecycleProvider.isBackgroundState
.onEach { isBackground ->
if (isBackground.not()) {
suggestToEnableBiometrics()
}
}.launchIn(modelScope)
suggestToOpenMarkets()
maybeMigrateNames()
@ -158,15 +168,13 @@ internal class WalletModel @Inject constructor(
walletScreenContentLoader.cancelAll()
}
private fun suggestToEnableBiometrics() {
modelScope.launch(dispatchers.main) {
withContext(dispatchers.io) { delay(timeMillis = 1_800) }
private suspend fun suggestToEnableBiometrics() {
if (shouldShowAskBiometryBottomSheet()) {
delay(timeMillis = 1_800)
if (shouldShowAskBiometryBottomSheet()) {
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForBiometry,
)
}
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForBiometry,
)
}
}
@ -344,16 +352,24 @@ internal class WalletModel @Inject constructor(
* and every minute while user stays on the main screen
*/
screenLifecycleProvider.isBackgroundState.onEach { inBackground ->
// fast exit
if (!tangemPayFeatureToggles.isTangemPayEnabled) return@onEach
updateTangemPayJobHolder.cancel()
if (!inBackground && tangemPayFeatureToggles.isTangemPayEnabled) {
modelScope.launch {
modelScope.launch {
// fast exit
val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced()
if (!initialDataProduced) return@launch
if (!inBackground) {
refreshTangemPayInfo()
while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL)
refreshTangemPayInfo()
}
}.saveIn(updateTangemPayJobHolder)
}
}
}.saveIn(updateTangemPayJobHolder)
}.launchIn(modelScope)
}
@ -365,7 +381,12 @@ internal class WalletModel @Inject constructor(
value = info,
onClickIssue = ::issueOrder,
onClickKyc = innerWalletRouter::openTangemPayOnboarding,
openDetails = innerWalletRouter::openTangemPayDetails,
openDetails = { config ->
innerWalletRouter.openTangemPayDetails(
userWalletId = stateHolder.getSelectedWalletId(),
config = config,
)
},
),
)
}
@ -516,21 +537,39 @@ internal class WalletModel @Inject constructor(
}
private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
walletScreenContentLoader.load(
userWallet = action.selectedWallet,
clickIntents = clickIntents,
coroutineScope = modelScope,
)
if (accountsFeatureToggles.isFeatureEnabled) {
fetchWalletContent(userWallet = action.selectedWallet)
fetchWalletContent(userWallet = action.selectedWallet)
stateHolder.update(
AddWalletTransformer(
userWallet = action.selectedWallet,
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
),
)
stateHolder.update(
AddWalletTransformer(
walletScreenContentLoader.load(
userWallet = action.selectedWallet,
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
),
)
coroutineScope = modelScope,
)
} else {
walletScreenContentLoader.load(
userWallet = action.selectedWallet,
clickIntents = clickIntents,
coroutineScope = modelScope,
)
fetchWalletContent(userWallet = action.selectedWallet)
stateHolder.update(
AddWalletTransformer(
userWallet = action.selectedWallet,
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
),
)
}
scrollToWallet(prevIndex = action.prevWalletIndex, newIndex = action.selectedWalletIndex) {
stateHolder.update {

View file

@ -24,7 +24,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.core.lce.Lce
@ -157,7 +157,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
override fun onSendClick(
@ -360,7 +360,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
return@launch
}
saveCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency)
manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency)
} else {
removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency)
}
@ -507,7 +507,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
appRouter.push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
cryptoCurrency = cryptoCurrency,
yieldId = yield?.id ?: return@launch,
),
)

View file

@ -11,10 +11,15 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.DialogMessage.Companion.invoke
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.card.SetCardWasScannedUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.UnlockWalletError
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -105,7 +110,7 @@ internal interface WalletWarningsClickIntents {
fun onDenyPermissions()
fun onFinishWalletActivationClick(type: WalletActivationBannerType)
fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean)
}
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ -139,34 +144,9 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val messageSender: UiMessageSender,
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
private val uiMessageSender: UiMessageSender,
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
private val finalizeWalletSetupAlertBS
get() = bottomSheetMessage {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUMV2.Icon.Type.Warning
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_activation_need_title)
body = resourceReference(R.string.hw_activation_need_description)
}
secondaryButton {
text = resourceReference(R.string.common_later)
onClick {
closeBs()
}
}
primaryButton {
text = resourceReference(R.string.hw_activation_need_backup)
onClick {
val userWallet = getSelectedUserWallet() ?: return@onClick
appRouter.push(WalletActivation(userWallet.walletId))
closeBs()
}
}
}
override fun onAddBackupCardClick() {
analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped)
@ -221,11 +201,40 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
userWalletsListRepository.unlockAllWallets()
.onLeft {
val selectedUserWallet = getSelectedUserWallet() ?: return@onLeft
val selectedUserWalletId = selectedUserWallet.walletId
val method = when (selectedUserWallet) {
is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan()
is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode
}
userWalletsListRepository.unlock(stateHolder.getSelectedWalletId(), method)
userWalletsListRepository
.unlock(stateHolder.getSelectedWalletId(), method)
.onLeft {
when (it) {
UnlockWalletError.AlreadyUnlocked -> Unit
UnlockWalletError.ScannedCardWalletNotMatched -> {
uiMessageSender.send(
message = DialogMessage(
title = resourceReference(R.string.common_warning),
message = resourceReference(R.string.error_wrong_wallet_tapped),
),
)
}
UnlockWalletError.UnableToUnlock -> {
Timber.e("Unable to unlock wallet with id: $selectedUserWalletId")
uiMessageSender.send(
SnackbarMessage(TextReference.Res(R.string.generic_error)),
)
}
UnlockWalletError.UserCancelled -> Unit
UnlockWalletError.UserWalletNotFound -> {
// This should never happen in this flow
Timber.e("User wallet not found for unlock: $selectedUserWalletId")
uiMessageSender.send(
SnackbarMessage(TextReference.Res(R.string.generic_error)),
)
}
}
}
}
}
return
@ -466,14 +475,38 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onFinishWalletActivationClick(type: WalletActivationBannerType) {
when (type) {
override fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean) {
when (bannerType) {
WalletActivationBannerType.Attention -> {
val userWallet = getSelectedUserWallet() ?: return
appRouter.push(WalletActivation(userWallet.walletId))
appRouter.push(WalletActivation(userWallet.walletId, isBackupExists))
}
WalletActivationBannerType.Warning -> {
messageSender.send(finalizeWalletSetupAlertBS)
val message = bottomSheetMessage {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUMV2.Icon.Type.Warning
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_activation_need_title)
body = resourceReference(R.string.hw_activation_need_description)
}
secondaryButton {
text = resourceReference(R.string.common_later)
onClick {
closeBs()
}
}
primaryButton {
text = resourceReference(R.string.hw_activation_need_backup)
onClick {
val userWallet = getSelectedUserWallet() ?: return@onClick
appRouter.push(WalletActivation(userWallet.walletId, isBackupExists))
closeBs()
}
}
}
messageSender.send(message)
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.account
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import javax.inject.Inject
@ -12,4 +13,5 @@ internal class AccountDependencies @Inject constructor(
val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
val expandedAccountsHolder: ExpandedAccountsHolder,
val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
val singleAccountStatusSupplier: SingleAccountStatusSupplier,
)

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -13,25 +14,30 @@ import javax.inject.Inject
@ModelScoped
internal class ExpandedAccountsHolder @Inject constructor(
private val singleAccountListSupplier: SingleAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) {
private val expandedAccounts = MutableStateFlow<Map<UserWalletId, Set<AccountId>>>(mapOf())
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = channelFlow {
walletAccounts(userWallet)
.onEach { accountList ->
val isSingleAccount = accountList.totalAccounts == 1
combine(
flow = walletAccounts(userWallet),
flow2 = isAccountsModeEnabledUseCase.invoke(),
transform = { accountList, isAccountsMode ->
val isSingleAccount = accountList.accounts.size == 1
val defaultExpanded = when {
!isAccountsMode -> setOf()
isSingleAccount -> setOf(accountList.mainAccount.accountId)
else -> setOf()
}
expandedAccounts.update { map ->
var expandedSet = map[userWallet.walletId] ?: defaultExpanded
// force expand for single account
if (isSingleAccount) expandedSet = defaultExpanded
if (isSingleAccount || !isAccountsMode) expandedSet = defaultExpanded
map.plus(userWallet.walletId to expandedSet)
}
}.launchIn(this)
},
).launchIn(this)
expandedAccounts
.mapNotNull { map -> map[userWallet.walletId] }

View file

@ -8,15 +8,13 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
@ -88,7 +86,7 @@ internal object WalletPreviewData {
private const val networksSize = 10
private const val tokensSize = 3
private val draggableItems by lazy {
private val draggableItems: PersistentList<DraggableItem> by lazy {
List(networksSize) { it }
.flatMap { index ->
val lastNetworkIndex = networksSize - 1
@ -98,11 +96,13 @@ internal object WalletPreviewData {
val group = DraggableItem.GroupHeader(
id = networkNumber,
networkName = "$networkNumber",
roundingMode = when (index) {
0 -> DraggableItem.RoundingMode.Top()
lastNetworkIndex -> DraggableItem.RoundingMode.Bottom()
else -> DraggableItem.RoundingMode.None
},
accountId = "account_$networkNumber",
)
val tokens: MutableList<DraggableItem.Token> = mutableListOf()
@ -117,6 +117,7 @@ internal object WalletPreviewData {
),
),
groupId = group.id,
accountId = "account_$networkNumber",
roundingMode = when {
i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom()
else -> DraggableItem.RoundingMode.None
@ -125,7 +126,10 @@ internal object WalletPreviewData {
)
}
val divider = DraggableItem.Placeholder(id = "divider_$networkNumber")
val divider = DraggableItem.Placeholder(
id = "divider_$networkNumber",
accountId = "account_$networkNumber",
)
buildList {
add(group)
@ -154,6 +158,7 @@ internal object WalletPreviewData {
itemsState = OrganizeTokensListState.GroupedByNetwork(
items = draggableItems,
),
tokenListUM = OrganizeTokensListUM.EmptyList,
header = OrganizeTokensState.HeaderConfig(
onSortClick = {},
onGroupClick = {},

View file

@ -106,6 +106,28 @@ internal object WalletScreenPreviewData {
),
)
private val emptyPortfolioContentState = WalletTokensListState.ContentState.PortfolioContent(
items = persistentListOf(
TokensListItemUM.Portfolio(
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
isExpanded = false,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem
.copy(iconState = AccountItemPreviewData.accountLetterIcon),
),
TokensListItemUM.Portfolio(
tokens = persistentListOf(),
isExpanded = true,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem,
),
),
organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig(
isEnabled = true,
onClick = {},
),
)
private val noteLockedCard by lazy {
WalletCardState.LockedContent(
id = UserWalletId(stringValue = "1"),
@ -208,4 +230,12 @@ internal object WalletScreenPreviewData {
multiWalletState.copy(tokensListState = portfolioContentState),
),
)
internal val accountScreenWithEmptyTokensState =
walletScreenState.copy(
wallets = persistentListOf(
singleWalletLockedState,
multiWalletState.copy(tokensListState = emptyPortfolioContentState),
),
)
}

View file

@ -33,6 +33,7 @@ import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.tokenlist.ExpandedPortfolioHeader
import com.tangem.core.ui.components.tokenlist.internal.DraggableGroupTitleItem
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.TextReference
@ -47,6 +48,7 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
@ -73,6 +75,7 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier
.padding(paddingValues)
.fillMaxSize(),
listState = tokensListState,
tokensListUM = state.tokenListUM,
state = state.itemsState,
dndConfig = state.dndConfig,
isBalanceHidden = state.isBalanceHidden,
@ -96,11 +99,17 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier
private fun TokenList(
listState: LazyListState,
state: OrganizeTokensListState,
tokensListUM: OrganizeTokensListUM,
dndConfig: OrganizeTokensState.DragAndDropConfig,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val tokenList = if (tokensListUM !is OrganizeTokensListUM.EmptyList) {
tokensListUM.items
} else {
state.items
}
Box(modifier = modifier) {
val onDragEnd: (Int, Int) -> Unit = remember {
{ _, _ ->
@ -127,7 +136,7 @@ private fun TokenList(
// because sometimes items disappear after reordering in 1.7.4+ compose-runtime version
// check removing after update to compose-runtime 1.8.0+
val forceRecompose = remember { mutableIntStateOf(0) }
LaunchedEffect(state.items) {
LaunchedEffect(tokenList) {
forceRecompose.intValue++
}
@ -140,7 +149,7 @@ private fun TokenList(
contentPadding = listContentPadding,
) {
itemsIndexed(
items = state.items,
items = tokenList,
key = { _, item -> item.id },
) { index, item ->
@ -206,6 +215,13 @@ private fun LazyItemScope.DraggableItem(
)
// Should be presented in the list but remain invisible
is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth())
is DraggableItem.Portfolio -> ExpandedPortfolioHeader(
state = item.tokenItemState,
isCollapsable = false,
modifier = modifierWithBackground
.padding(top = 8.dp)
.fillMaxWidth(),
)
}
}

View file

@ -2,19 +2,24 @@ package com.tangem.feature.wallet.presentation.organizetokens
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverterV2
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.NetworkGroupToDraggableItemsConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -23,7 +28,9 @@ import kotlinx.coroutines.flow.update
internal class OrganizeTokensStateHolder(
private val intents: OrganizeTokensIntents,
private val dragAndDropIntents: DragAndDropIntents,
private val dragAndDropAdapterV2: DragAndDropAdapterV2,
private val appCurrencyProvider: Provider<AppCurrency>,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
private val stateFlowInternal: MutableStateFlow<OrganizeTokensState> = MutableStateFlow(getInitialState())
@ -54,6 +61,16 @@ internal class OrganizeTokensStateHolder(
updateState { tokenListConverter.convert(tokenList) }
}
fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) {
updateState {
TokenListToStateConverterV2(
accountStatusList = accountStatusList,
isAccountsMode = isAccountsModeEnabled,
appCurrency = appCurrencyProvider(),
).transform(this)
}
}
fun updateStateAfterTokenListSorting(tokenList: TokenList) {
updateState {
tokenListConverter.convert(tokenList).copy(
@ -62,6 +79,18 @@ internal class OrganizeTokensStateHolder(
}
}
fun updateStateAfterTokenListSortingV2(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) {
updateState {
TokenListToStateConverterV2(
accountStatusList = accountStatusList,
isAccountsMode = isAccountsModeEnabled,
appCurrency = appCurrencyProvider(),
).transform(this).copy(
scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent),
)
}
}
fun updateStateToDisplayProgress() {
updateState { inProgressStateConverter.convert(value = this) }
}
@ -70,6 +99,10 @@ internal class OrganizeTokensStateHolder(
updateState { inProgressStateConverter.convertBack(value = this) }
}
fun updateStateWithManualSortingV2(tokenListUM: OrganizeTokensListUM) {
updateState { copy(tokenListUM = tokenListUM) }
}
fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) {
updateState { copy(itemsState = itemsState) }
}
@ -94,6 +127,7 @@ internal class OrganizeTokensStateHolder(
return OrganizeTokensState(
onBackClick = intents::onBackClick,
itemsState = OrganizeTokensListState.Empty,
tokenListUM = OrganizeTokensListUM.EmptyList,
header = OrganizeTokensState.HeaderConfig(
onSortClick = intents::onSortClick,
onGroupClick = intents::onGroupClick,
@ -102,12 +136,21 @@ internal class OrganizeTokensStateHolder(
onApplyClick = intents::onApplyClick,
onCancelClick = intents::onCancelClick,
),
dndConfig = OrganizeTokensState.DragAndDropConfig(
onItemDragged = dragAndDropIntents::onItemDragged,
onItemDragStart = dragAndDropIntents::onItemDraggingStart,
onItemDragEnd = dragAndDropIntents::onItemDraggingEnd,
canDragItemOver = dragAndDropIntents::canDragItemOver,
),
dndConfig = if (accountsFeatureToggles.isFeatureEnabled) {
OrganizeTokensState.DragAndDropConfig(
onItemDragged = dragAndDropAdapterV2::onItemDragged,
onItemDragStart = dragAndDropAdapterV2::onItemDraggingStart,
onItemDragEnd = dragAndDropAdapterV2::onItemDraggingEnd,
canDragItemOver = dragAndDropAdapterV2::canDragItemOver,
)
} else {
OrganizeTokensState.DragAndDropConfig(
onItemDragged = dragAndDropIntents::onItemDragged,
onItemDragStart = dragAndDropIntents::onItemDraggingStart,
onItemDragEnd = dragAndDropIntents::onItemDraggingEnd,
canDragItemOver = dragAndDropIntents::canDragItemOver,
)
},
scrollListToTop = consumedEvent(),
isBalanceHidden = true,
)

View file

@ -6,7 +6,6 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem.RoundingMode
/**
* Helper class for the DND list items
@ -26,12 +25,14 @@ internal sealed class DraggableItem {
*
* @property id ID of the network group
* @property networkName network group name
* @property accountId account id
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
data class GroupHeader(
override val id: Int,
val networkName: String,
val accountId: String = "",
override val roundingMode: RoundingMode = RoundingMode.None,
override val showShadow: Boolean = false,
) : DraggableItem() {
@ -50,6 +51,7 @@ internal sealed class DraggableItem {
*
* @property tokenItemState state of the token item
* @property groupId ID of the network group which contains this token
* @property accountId account id
* @property id ID of the token
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
@ -57,6 +59,7 @@ internal sealed class DraggableItem {
data class Token(
val tokenItemState: TokenItemState.Draggable,
val groupId: Int,
val accountId: String = "",
override val showShadow: Boolean = false,
override val roundingMode: RoundingMode = RoundingMode.None,
) : DraggableItem() {
@ -67,14 +70,32 @@ internal sealed class DraggableItem {
* Helper item used to detect possible positions where a draggable item can be placed.
*
* @property id ID of the placeholder
* @property accountId account id
* */
data class Placeholder(
override val id: String,
val accountId: String = "",
) : DraggableItem() {
override val showShadow: Boolean = false
override val roundingMode: RoundingMode = RoundingMode.None
}
/**
* Item for portfolio.
*
* @property tokenItemState state of the portfolio item
* @property id ID of the portfolio
* @property roundingMode item [RoundingMode]
* @property showShadow if true then item should be elevated
* */
data class Portfolio(
override val roundingMode: RoundingMode = RoundingMode.None,
val tokenItemState: TokenItemState,
) : DraggableItem() {
override val id: String = tokenItemState.id
override val showShadow: Boolean = false
}
/**
* Rounding mode of the [DraggableItem]
*
@ -122,6 +143,7 @@ internal sealed class DraggableItem {
* */
fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) {
is Placeholder -> this
is Portfolio -> this.copy(roundingMode = mode)
is GroupHeader -> this.copy(roundingMode = mode)
is Token -> this.copy(roundingMode = mode)
}
@ -134,7 +156,9 @@ internal sealed class DraggableItem {
* @return updated [DraggableItem]
* */
fun updateShadowVisibility(show: Boolean): DraggableItem = when (this) {
is Placeholder -> this
is Portfolio,
is Placeholder,
-> this
is GroupHeader -> this.copy(showShadow = show)
is Token -> this.copy(showShadow = show)
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@Deprecated("Use OrganizeTokensListUM instead, will be removed in future releases")
@Immutable
internal sealed class OrganizeTokensListState {
abstract val items: PersistentList<DraggableItem>
@ -19,4 +20,26 @@ internal sealed class OrganizeTokensListState {
data object Empty : OrganizeTokensListState() {
override val items: PersistentList<DraggableItem> = persistentListOf()
}
}
@Immutable
internal sealed interface OrganizeTokensListUM {
val items: PersistentList<DraggableItem>
val isGrouped: Boolean
data class AccountList(
override val items: PersistentList<DraggableItem>,
override val isGrouped: Boolean,
) : OrganizeTokensListUM
data class TokensList(
override val items: PersistentList<DraggableItem>,
override val isGrouped: Boolean,
) : OrganizeTokensListUM
data object EmptyList : OrganizeTokensListUM {
override val items: PersistentList<DraggableItem> = persistentListOf()
override val isGrouped: Boolean = false
}
}

View file

@ -8,6 +8,7 @@ import org.burnoutcrew.reorderable.ItemPosition
internal data class OrganizeTokensState(
val onBackClick: () -> Unit,
val itemsState: OrganizeTokensListState,
val tokenListUM: OrganizeTokensListUM,
val header: HeaderConfig,
val actions: ActionsConfig,
val dndConfig: DragAndDropConfig,

View file

@ -1,9 +1,13 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrencies
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM
internal class CryptoCurrenciesIdsResolver {
@ -29,4 +33,26 @@ internal class CryptoCurrenciesIdsResolver {
currencyStatus?.currency?.id
}
}
fun resolveV2(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies {
val draggableTokens = when (tokensListUM) {
OrganizeTokensListUM.EmptyList -> return emptyMap()
is OrganizeTokensListUM.AccountList,
is OrganizeTokensListUM.TokensList,
-> tokensListUM.items.filterIsInstance<DraggableItem.Token>()
}
return accountStatusList?.accountStatuses
?.filter { it.getCryptoTokenList() != TokenList.Empty }
?.associate { accountStatus ->
val currencies = accountStatus.flattenCurrencies()
accountStatus.account as Account.CryptoPortfolio to draggableTokens
.asSequence()
.filter { it.accountId == accountStatus.account.accountId.value }
.mapNotNull { sortedToken ->
currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency
}
.toList()
} ?: emptyMap()
}
}

View file

@ -2,6 +2,9 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
internal fun getGroupPlaceholder(index: Int): DraggableItem.Placeholder {
return DraggableItem.Placeholder(id = "placeholder_${index.inc()}")
internal fun getGroupPlaceholder(index: Int, accountId: String = ""): DraggableItem.Placeholder {
return DraggableItem.Placeholder(
id = "placeholder_${accountId}_${index.inc()}",
accountId = accountId,
)
}

View file

@ -12,7 +12,9 @@ internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
1 -> DraggableItem.RoundingMode.Top()
lastItemIndex -> DraggableItem.RoundingMode.Bottom()
else -> when (item) {
is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None
is DraggableItem.Portfolio,
is DraggableItem.Placeholder,
-> DraggableItem.RoundingMode.None
is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true)
is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) {
DraggableItem.RoundingMode.Bottom(showGap = true)
@ -28,6 +30,48 @@ internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
}
}
internal fun List<DraggableItem>.uniteItemsV2(isAccountsMode: Boolean): List<DraggableItem> {
val items = this
val lastItemIndex = items.lastIndex
return items
.asSequence()
.mapIndexed { index, item ->
val mode = when (index) {
0 -> if (item is DraggableItem.Placeholder) {
DraggableItem.RoundingMode.None
} else {
DraggableItem.RoundingMode.Top()
}
lastItemIndex -> DraggableItem.RoundingMode.Bottom()
1 -> if (items.first() is DraggableItem.Placeholder) {
DraggableItem.RoundingMode.Top()
} else {
DraggableItem.RoundingMode.None
}
else -> when (item) {
is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None
is DraggableItem.GroupHeader -> if (isAccountsMode) {
DraggableItem.RoundingMode.None
} else {
DraggableItem.RoundingMode.Top(showGap = true)
}
is DraggableItem.Token -> applyRoundingModeToToken(
isAccountsMode = isAccountsMode,
items = items,
index = index,
lastItemIndex = lastItemIndex,
)
is DraggableItem.Portfolio -> DraggableItem.RoundingMode.Top(showGap = true)
}
}
item
.updateRoundingMode(mode)
.updateShadowVisibility(show = false)
}.toList()
}
internal fun List<DraggableItem>.divideMovingItem(movingItem: DraggableItem): List<DraggableItem> {
val mutableList = this.toMutableList()
val listIterator = mutableList.listIterator()
@ -63,12 +107,55 @@ private fun List<DraggableItem>.prepareItems(): List<DraggableItem> {
return mutableListOf<DraggableItem>().apply {
add(DraggableItem.Placeholder(firstPlaceholderId))
val itemsWithoutFirstPlaceholder = if (items.firstOrNull()?.id == firstPlaceholderId) {
items.drop(n = 1)
} else {
items
}
val itemsWithoutFirstPlaceholder = items.filterNot { it.id == firstPlaceholderId }
addAll(itemsWithoutFirstPlaceholder)
}
}
/**
* Applying rounding to tokens
*
* If is in accounts mode without grouping
* * PORTFOLIO
* * TOKEN
* * TOKEN <- add rounding
* * PORTFOLIO index + 1 is PORTFOLIO
*
* If is in accounts mode with grouping
* * PORTFOLIO
* * PLACEHOLDER
* * GROUPING
* * TOKEN
* * TOKEN <- add rounding
* * PLACEHOLDER index + 1 is PLACEHOLDER
* * PORTFOLIO index + 2 is PORTFOLIO
* * PLACEHOLDER
*
* If is not accounts mode without grouping
* * TOKEN
* * TOKEN <- add rounding
*
* If is not accounts mode with grouping
* * PLACEHOLDER
* * GROUPING
* * TOKEN
* * TOKEN <- add rounding
* * PLACEHOLDER index + 1 is PLACEHOLDER
*/
private fun applyRoundingModeToToken(
isAccountsMode: Boolean,
items: List<DraggableItem>,
index: Int,
lastItemIndex: Int,
) = when {
isAccountsMode && index + 1 < lastItemIndex &&
(items[index + 1] is DraggableItem.Portfolio ||
items[index + 1] is DraggableItem.Placeholder && items[index + 2] is DraggableItem.Portfolio) -> {
DraggableItem.RoundingMode.Bottom(showGap = true)
}
(!isAccountsMode || index + 1 == lastItemIndex) && items[index + 1] is DraggableItem.Placeholder -> {
DraggableItem.RoundingMode.Bottom(showGap = true)
}
else -> DraggableItem.RoundingMode.None
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
@ -15,4 +16,16 @@ internal inline fun OrganizeTokensListState.updateItems(
is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems)
is OrganizeTokensListState.Empty -> this
}
}
internal inline fun OrganizeTokensListUM.updateItems(
update: (PersistentList<DraggableItem>) -> List<DraggableItem>,
): OrganizeTokensListUM {
val updatedItems = update(items).toPersistentList()
return when (this) {
is OrganizeTokensListUM.AccountList -> copy(items = updatedItems)
is OrganizeTokensListUM.TokensList -> copy(items = updatedItems)
OrganizeTokensListUM.EmptyList -> this
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.OrganizedTokenListConverter
import com.tangem.utils.converter.Converter
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class TokenListToStateConverterV2(
private val accountStatusList: AccountStatusList,
private val isAccountsMode: Boolean,
private val appCurrency: AppCurrency,
) : Transformer<OrganizeTokensState> {
private val accountListItemConverter by lazy {
AccountTokenItemConverter(appCurrency = appCurrency, isAccountsMode)
}
override fun transform(prevState: OrganizeTokensState): OrganizeTokensState {
val tokenListUM = accountListItemConverter.convert(accountStatusList)
return prevState.copy(
tokenListUM = tokenListUM,
header = prevState.header.copy(
isEnabled = tokenListUM !is OrganizeTokensListUM.EmptyList,
isSortedByBalance = accountStatusList.sortType == TokensSortType.BALANCE,
isGrouped = accountStatusList.groupType == TokensGroupType.NETWORK,
),
actions = prevState.actions.copy(
canApply = tokenListUM !is OrganizeTokensListUM.EmptyList,
),
)
}
}
internal class AccountTokenItemConverter(
private val appCurrency: AppCurrency,
private val isAccountsMode: Boolean,
) : Converter<AccountStatusList, OrganizeTokensListUM> {
private val organizedTokenListConverter by lazy {
OrganizedTokenListConverter(appCurrency)
}
override fun convert(value: AccountStatusList): OrganizeTokensListUM {
val isGrouping = value.groupType == TokensGroupType.NETWORK
return if (isAccountsMode) {
OrganizeTokensListUM.AccountList(
isGrouped = isGrouping,
items = value.accountStatuses
.asSequence()
.filterIsInstance<AccountStatus.CryptoPortfolio>()
.flatMap { accountStatus ->
if (accountStatus.tokenList != TokenList.Empty) {
buildList {
add(
DraggableItem.Portfolio(
tokenItemState = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = accountStatus.account,
).convert(TotalFiatBalance.Loading),
),
)
if (isGrouping) {
add(getGroupPlaceholder(accountId = accountStatus.accountId.value, index = -1))
}
addAll(organizedTokenListConverter.convert(accountStatus))
}
} else {
emptyList()
}
}.toList()
.uniteItemsV2(true).toPersistentList(),
)
} else {
OrganizeTokensListUM.TokensList(
isGrouped = isGrouping,
items = buildList {
if (isGrouping) {
add(getGroupPlaceholder(accountId = value.mainAccount.accountId.value, index = -1))
}
addAll(organizedTokenListConverter.convert(value.mainAccount))
}.uniteItemsV2(false)
.toPersistentList(),
)
}
}
}

View file

@ -0,0 +1,67 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class CryptoCurrencyToDraggableItemConverterV2(
private val appCurrency: AppCurrency,
) : Converter<AccountCryptoCurrencyStatus, DraggableItem.Token> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
override fun convert(value: AccountCryptoCurrencyStatus): DraggableItem.Token {
return createDraggableToken(value)
}
private fun createDraggableToken(accountCryptoCurrencyStatus: AccountCryptoCurrencyStatus): DraggableItem.Token {
val (account, currencyStatus) = accountCryptoCurrencyStatus
return DraggableItem.Token(
tokenItemState = createTokenItemState(currencyStatus, appCurrency),
groupId = getGroupHeaderId(currencyStatus.currency.network),
accountId = account.accountId.value,
)
}
private fun createTokenItemState(
currencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
): TokenItemState.Draggable {
val currency = currencyStatus.currency
return TokenItemState.Draggable(
id = getTokenItemId(currency.id),
iconState = iconStateConverter.convert(currencyStatus),
titleState = TokenItemState.TitleState.Content(text = stringReference(currency.name)),
subtitle2State = if (currencyStatus.value.isError) {
TokenItemState.Subtitle2State.Unreachable
} else {
TokenItemState.Subtitle2State.TextContent(text = getFormattedFiatAmount(currencyStatus, appCurrency))
},
)
}
private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String {
val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data
val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO
val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId)
?.multiply(fiatRate).orZero()
val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) }
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder
import com.tangem.utils.converter.Converter
internal class NetworkGroupToDraggableItemsConverterV2(
private val itemConverter: CryptoCurrencyToDraggableItemConverterV2,
) : Converter<Pair<Account.CryptoPortfolio, NetworkGroup>, List<DraggableItem>> {
override fun convert(value: Pair<Account.CryptoPortfolio, NetworkGroup>): List<DraggableItem> {
val (account, networkGroup) = value
return buildList {
add(createGroupHeader(account.accountId, networkGroup))
addAll(createTokens(account, networkGroup))
}
}
override fun convertList(
input: Collection<Pair<Account.CryptoPortfolio, NetworkGroup>>,
): List<List<DraggableItem>> {
return input.mapIndexed { index, pair ->
convert(pair).toMutableList()
.also { mutableGroup ->
mutableGroup.add(
getGroupPlaceholder(accountId = pair.first.accountId.value, index = index),
)
}
}
}
private fun createGroupHeader(accountId: AccountId, group: NetworkGroup) = DraggableItem.GroupHeader(
id = getGroupHeaderId(group.network),
accountId = accountId.value,
networkName = group.network.name,
)
private fun createTokens(account: Account.CryptoPortfolio, group: NetworkGroup): List<DraggableItem.Token> {
return itemConverter.convertList(
group.currencies.map {
AccountCryptoCurrencyStatus(
account = account,
status = it,
)
},
)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal class OrganizedTokenListConverter(
private val appCurrency: AppCurrency,
) : Converter<AccountStatus, PersistentList<DraggableItem>> {
private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) }
private val groupsConverter by lazy {
NetworkGroupToDraggableItemsConverterV2(tokensConverter)
}
override fun convert(value: AccountStatus): PersistentList<DraggableItem> {
val cryptoAccount = value.account as? Account.CryptoPortfolio ?: return persistentListOf()
return when (val tokenList = value.getCryptoTokenList()) {
is TokenList.GroupedByNetwork -> groupsConverter.convertList(
tokenList.groups.map { cryptoAccount to it },
)
.flatten()
.toPersistentList()
is TokenList.Ungrouped -> tokensConverter.convertList(
value.flattenCurrencies().map {
AccountCryptoCurrencyStatus(
account = cryptoAccount,
status = it,
)
},
).toPersistentList()
is TokenList.Empty -> persistentListOf()
}
}
}

View file

@ -51,7 +51,9 @@ internal class DragAndDropAdapter(
return when (draggingItem) {
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex)
is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem)
is DraggableItem.Placeholder -> false
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> false
}
}
@ -61,7 +63,9 @@ internal class DragAndDropAdapter(
updateListState(DragOperation.Type.Start) {
when (item) {
is DraggableItem.Placeholder -> items
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> items
is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item)
is DraggableItem.Token -> when (this) {
is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item)
@ -81,7 +85,9 @@ internal class DragAndDropAdapter(
when (draggingItem) {
is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items)
is DraggableItem.Token -> items.uniteItems()
is DraggableItem.Placeholder -> items
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> items
}
}
@ -144,7 +150,9 @@ internal class DragAndDropAdapter(
return when (moveOverItem) {
is DraggableItem.GroupHeader -> false // Token item can not be moved to group item
is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group
is DraggableItem.Placeholder -> false
is DraggableItem.Portfolio,
is DraggableItem.Placeholder,
-> false
}
}

View file

@ -0,0 +1,209 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd
import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems
import com.tangem.utils.Provider
import kotlinx.collections.immutable.mutate
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import org.burnoutcrew.reorderable.ItemPosition
internal class DragAndDropAdapterV2(
private val tokenListUMProvider: Provider<OrganizeTokensListUM>,
) : DragAndDropIntents {
private val tokenListUM: OrganizeTokensListUM
get() = tokenListUMProvider.invoke()
private val draggableGroupsOperations = DraggableGroupsOperations()
private val dragAndDropUpdatesInternal: MutableStateFlow<DragOperation?> = MutableStateFlow(value = null)
private var draggingItem: DraggableItem? = null
private var draggingListState: OrganizeTokensListUM? = null
val dragAndDropUpdates: Flow<DragOperation>
get() = dragAndDropUpdatesInternal.filterNotNull()
override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean {
val items = when (val listState = tokenListUM) {
is OrganizeTokensListUM.AccountList -> listState.items
is OrganizeTokensListUM.TokensList -> {
if (tokenListUM.isGrouped) {
listState.items
} else {
return true // If ungrouped then item can be moved anywhere
}
}
OrganizeTokensListUM.EmptyList -> return true
}
val (dragOverItem, draggingItem) = findItemsToMove(
items = items,
moveOverItemKey = dragOver.key,
movedItemKey = dragging.key,
)
if (dragOverItem == null || draggingItem == null) {
return false
}
val canDrag = when (draggingItem) {
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(
item = draggingItem,
moveOverItem = dragOverItem,
)
is DraggableItem.Token -> checkCanMoveTokenOver(
item = draggingItem,
moveOverItem = dragOverItem,
isAccountsMode = tokenListUM is OrganizeTokensListUM.AccountList,
isGrouped = tokenListUM.isGrouped,
)
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> false
}
return canDrag
}
override fun onItemDraggingStart(item: DraggableItem) {
if (draggingItem != null) return
draggingItem = item
updateListState(DragOperation.Type.Start) {
when (item) {
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> items
is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroupV2(items, item)
.divideMovingItem(item)
is DraggableItem.Token -> items.divideMovingItem(item)
}
}
draggingListState = tokenListUM
}
override fun onItemDraggingEnd() {
val draggingItem = draggingItem ?: return
updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) {
when (draggingItem) {
is DraggableItem.GroupHeader -> {
draggableGroupsOperations.expandGroupsV2(items)
.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList)
}
is DraggableItem.Token -> {
items.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList)
}
is DraggableItem.Placeholder,
is DraggableItem.Portfolio,
-> items
}
}
this.draggingItem = null
}
override fun onItemDragged(from: ItemPosition, to: ItemPosition) {
updateListState(DragOperation.Type.Dragged) {
items.mutate {
it.add(to.index, it.removeAt(from.index))
}
}
}
private fun updateListState(type: DragOperation.Type, block: OrganizeTokensListUM.() -> List<DraggableItem>) {
val updatedState = tokenListUM.updateItems { block(tokenListUM) }
dragAndDropUpdatesInternal.value = DragOperation(type, updatedState)
}
private fun findItemsToMove(
items: List<DraggableItem>,
moveOverItemKey: Any?,
movedItemKey: Any?,
): Pair<DraggableItem?, DraggableItem?> {
var moveOverItem: DraggableItem? = null
var movedItem: DraggableItem? = null
for (item in items) {
if (item.id == moveOverItemKey) {
moveOverItem = item
}
if (item.id == movedItemKey) {
movedItem = item
}
if (moveOverItem != null && movedItem != null) {
break
}
}
return Pair(moveOverItem, movedItem)
}
private fun checkCanMoveHeaderOver(item: DraggableItem.GroupHeader, moveOverItem: DraggableItem): Boolean {
return when (moveOverItem) {
// Header can be moved only in its account
is DraggableItem.Placeholder -> item.accountId == moveOverItem.accountId
else -> false
}
}
private fun checkCanMoveTokenOver(
item: DraggableItem.Token,
moveOverItem: DraggableItem,
isGrouped: Boolean,
isAccountsMode: Boolean,
): Boolean {
return when (moveOverItem) {
is DraggableItem.GroupHeader -> false // Token item can not be moved to group item
is DraggableItem.Token -> when {
// Token item can be moved only in its group
isGrouped -> item.groupId == moveOverItem.groupId
// Token item can be moved only in its account
isAccountsMode -> item.accountId == moveOverItem.accountId
// If ungrouped and not accounts mode then item can be moved anywhere
else -> true
}
is DraggableItem.Portfolio,
is DraggableItem.Placeholder,
-> false // Token item can not be moved to portfolio or placeholder
}
}
private fun checkIsItemsOrderChanged(): Boolean {
fun OrganizeTokensListUM?.getItemsIds(): List<Any>? = this?.items?.mapNotNull { item ->
if (item is DraggableItem.Placeholder) {
null
} else {
item.id
}
}
return tokenListUM.getItemsIds() != draggingListState.getItemsIds()
}
data class DragOperation(
val type: Type,
val listState: OrganizeTokensListUM,
) {
sealed class Type {
data object Start : Type()
data object Dragged : Type()
data class End(val isItemsOrderChanged: Boolean) : Type()
}
}
}

View file

@ -24,6 +24,21 @@ internal class DraggableGroupsOperations {
return itemsWithoutGroupTokens.divideMovingItem(movingGroup)
}
fun collapseGroupV2(items: List<DraggableItem>, movingGroup: DraggableItem.GroupHeader): List<DraggableItem> {
if (!groupIdToTokens.isNullOrEmpty()) return items
groupIdToTokens = items
.asSequence()
.filterIsInstance<DraggableItem.Token>()
.groupBy { it.groupId }
val itemsWithoutGroupTokens = items.filterNot {
it is DraggableItem.Token && it.groupId == movingGroup.id
}
return itemsWithoutGroupTokens.divideMovingItem(movingGroup)
}
fun expandGroups(items: List<DraggableItem>): List<DraggableItem> {
if (groupIdToTokens.isNullOrEmpty()) return items
@ -46,4 +61,49 @@ internal class DraggableGroupsOperations {
return expandedGroups
}
fun expandGroupsV2(items: List<DraggableItem>): List<DraggableItem> {
if (groupIdToTokens.isNullOrEmpty()) return items
val accountList = items.filterIsInstance<DraggableItem.Portfolio>()
val currentGroups = items.filterIsInstance<DraggableItem.GroupHeader>()
val expandedGroups = if (items.any { it is DraggableItem.Portfolio }) {
accountList
.asSequence()
.flatMap { account ->
buildList {
add(account)
currentGroups
.asSequence()
.filter { it.accountId == account.id }
.forEachIndexed { index, group ->
if (index == 0) {
add(getGroupPlaceholder(accountId = group.accountId, index = -1))
}
add(group)
addAll(groupIdToTokens?.get(group.id).orEmpty())
add(getGroupPlaceholder(accountId = group.accountId, index = index))
}
}
}
} else {
currentGroups
.asSequence()
.flatMapIndexed { index, group ->
buildList {
if (index == 0) {
add(getGroupPlaceholder(accountId = group.accountId, index = -1))
}
add(group)
addAll(groupIdToTokens?.get(group.id).orEmpty())
add(getGroupPlaceholder(accountId = group.accountId, index = index))
}
}
}.toList()
groupIdToTokens = null
return expandedGroups
}
}

View file

@ -115,8 +115,8 @@ internal class DefaultWalletRouter @Inject constructor(
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding))
}
override fun openTangemPayDetails(config: TangemPayDetailsConfig) {
router.push(AppRoute.TangemPayDetails(config))
override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {
router.push(AppRoute.TangemPayDetails(userWalletId = userWalletId, config = config))
}
override fun openYieldSupplyBottomSheet(

View file

@ -65,7 +65,7 @@ internal interface InnerWalletRouter {
fun openTangemPayOnboarding()
fun openTangemPayDetails(config: TangemPayDetailsConfig)
fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)
/** Open BS abput yield supply active and all money deposited in AAVE */
fun openYieldSupplyBottomSheet(

View file

@ -31,11 +31,13 @@ import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
@ -62,6 +64,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
private val notificationsRepository: NotificationsRepository,
private val accountDependencies: AccountDependencies,
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
) {
@Suppress("UNCHECKED_CAST", "MagicNumber")
@ -98,6 +101,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.VisaPresale),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa),
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key),
getAccessCodeSkippedUseCase(userWallet.walletId),
) { array -> array }
.combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) }
.map { array ->
@ -110,13 +114,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val shouldShowVisaPromo = array[4] as Boolean
val shouldShowSepaBanner = array[5] as Boolean
val shouldShowEnablePushesReminderNotification = array[6] as Boolean
val accessCodeSkipped = array[7] as Boolean
buildList {
addUsedOutdatedDataNotification(totalFiatBalance)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents)
addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents, accessCodeSkipped)
addVisaPresalePromoNotification(clickIntents, shouldShowVisaPromo)
@ -405,10 +410,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
userWallet: UserWallet,
totalFiatBalance: Lce<TokenListError, TotalFiatBalance>,
clickIntents: WalletClickIntents,
accessCodeSkipped: Boolean,
) {
if (userWallet !is UserWallet.Hot) return
val shouldShowFinishActivation = !userWallet.backedUp
val isBackupExists = userWallet.backedUp
val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword &&
!accessCodeSkipped
val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired
val type = totalFiatBalance.fold(
ifLoading = { it.getFinishWalletActivationType() },
@ -427,11 +436,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
buttonsState = when (type) {
WalletActivationBannerType.Warning -> ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = { clickIntents.onFinishWalletActivationClick(type) },
onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) },
)
else -> ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = { clickIntents.onFinishWalletActivationClick(type) },
onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) },
)
},
),

View file

@ -1,11 +1,17 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.Either
import arrow.core.right
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.producer.SingleAccountStatusProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
@ -22,8 +28,11 @@ import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
@Suppress("LongParameterList")
internal class GetSingleWalletWarningsFactory @Inject constructor(
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val singleAccountStatusSupplier: SingleAccountStatusSupplier,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
@ -40,7 +49,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
return combine(
flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId),
flow = getPrimaryCurrencyStatusFlow(userWallet),
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
flow4 = getWalletsUseCase().conflate(),
@ -217,6 +226,29 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
}
}
private fun getPrimaryCurrencyStatusFlow(
userWallet: UserWallet,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return if (accountsFeatureToggles.isFeatureEnabled) {
getAccountStatusFlow(userWallet).mapNotNull { accountStatus ->
accountStatus.flattenCurrencies().firstOrNull()
}
.distinctUntilChanged()
.conflate()
.map { it.right() }
} else {
getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
}
}
private fun getAccountStatusFlow(userWallet: UserWallet): Flow<AccountStatus> {
val accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId)
return singleAccountStatusSupplier(SingleAccountStatusProducer.Params(accountId))
.distinctUntilChanged()
.conflate()
}
private companion object {
const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10"
const val MAX_REMAINING_SIGNATURES_COUNT = 10

View file

@ -44,7 +44,7 @@ class HasSingleWalletSignedHashesUseCase @Inject constructor(
private fun UserWallet.Cold.isCorrectCardType(): Boolean {
return with(scanResponse.cardTypesResolver) {
!DemoConfig().isDemoCardId(cardId) && isReleaseFirmwareType() && !isMultiwalletAllowed() && !isTangemTwins()
!DemoConfig.isDemoCardId(cardId) && isReleaseFirmwareType() && !isMultiwalletAllowed() && !isTangemTwins()
}
}
}

View file

@ -52,6 +52,7 @@ internal class WalletImageResolver @Inject constructor(
cardTypesResolver.isTangemTwins() -> R.drawable.ill_twins_120_106
cardTypesResolver.isStart2Coin() -> R.drawable.ill_start2coin_120_106
cardTypesResolver.isTangemNote() -> noteImage?.imageResId
DemoConfig.isDemoNoteAsMultiwallet(userWallet.cardId) -> R.drawable.ill_wallet2_cards2_120_106
else -> null
}
}
@ -84,7 +85,7 @@ internal class WalletImageResolver @Inject constructor(
@DrawableRes twoBackupResId: Int = R.drawable.ill_wallet2_cards3_120_106,
): Int? {
return resolveWalletWithBackups { count ->
if (DemoConfig().isDemoCardId(cardId)) return@resolveWalletWithBackups oneBackupResId
if (DemoConfig.isDemoCardId(cardId)) return@resolveWalletWithBackups oneBackupResId
when (count) {
WALLET_WITH_ONE_BACKUP_COUNT -> oneBackupResId

View file

@ -1,21 +1,37 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.common.extensions.isZero
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@ModelScoped
internal class WalletWithFundsChecker @Inject constructor(
private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase,
) {
private val statusByWalletId = ConcurrentHashMap<UserWalletId, Boolean>()
suspend fun check(tokenList: TokenList) {
val hasNonZeroWallets = tokenList.flattenCurrencies().hasNonZeroWallets()
if (hasNonZeroWallets) setWalletWithFundsFoundUseCase()
}
suspend fun check(userWalletId: UserWalletId, currencies: List<CryptoCurrencyStatus>) {
val hasNonZeroWallets = currencies.hasNonZeroWallets()
val prevStatus = statusByWalletId.get(userWalletId)
if (hasNonZeroWallets && prevStatus != true) {
statusByWalletId[userWalletId] = true
setWalletWithFundsFoundUseCase()
}
}
private fun List<CryptoCurrencyStatus>.hasNonZeroWallets(): Boolean {
return any {
val amount = it.value.amount ?: return@any false

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.loaders
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
@ -8,11 +9,16 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.*
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class WalletContentLoaderFactory @Inject constructor(
private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory,
private val multiWalletContentLoaderV2Factory: MultiWalletContentLoaderV2.Factory,
private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory,
private val singleWalletWithTokenContentLoaderV2Factory: SingleWalletWithTokenContentLoaderV2.Factory,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory,
private val singleWalletContentLoaderV2Factory: SingleWalletContentLoaderV2.Factory,
private val visaWalletContentLoaderFactory: VisaWalletContentLoaderFactory,
) {
@ -23,16 +29,28 @@ internal class WalletContentLoaderFactory @Inject constructor(
): WalletContentLoader? {
return when {
userWallet.isMultiCurrency -> {
multiWalletContentLoaderFactory.create(userWallet, clickIntents)
if (accountsFeatureToggles.isFeatureEnabled) {
multiWalletContentLoaderV2Factory.create(userWallet)
} else {
multiWalletContentLoaderFactory.create(userWallet, clickIntents)
}
}
userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> {
singleWalletWithTokenContentLoaderFactory.create(userWallet, clickIntents)
if (accountsFeatureToggles.isFeatureEnabled) {
singleWalletWithTokenContentLoaderV2Factory.create(userWallet)
} else {
singleWalletWithTokenContentLoaderFactory.create(userWallet, clickIntents)
}
}
userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isVisaWallet() -> {
visaWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh)
}
userWallet is UserWallet.Cold && !userWallet.isMultiCurrency -> {
singleWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh)
if (accountsFeatureToggles.isFeatureEnabled) {
singleWalletContentLoaderV2Factory.create(userWallet, isRefresh)
} else {
singleWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh)
}
}
else -> null
}

View file

@ -12,7 +12,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
@ -23,6 +22,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
@Suppress("LongParameterList")
@Deprecated("Use MultiWalletContentLoaderV2 instead")
@ModelScoped
internal class MultiWalletContentLoader(
private val userWallet: UserWallet,
@ -41,7 +41,6 @@ internal class MultiWalletContentLoader(
private val getStoryContentUseCase: GetStoryContentUseCase,
private val walletsRepository: WalletsRepository,
private val currenciesRepository: CurrenciesRepository,
private val accountDependencies: AccountDependencies,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
@ -57,7 +56,6 @@ internal class MultiWalletContentLoader(
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
).let(::add)

View file

@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
@ -19,10 +19,10 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import javax.inject.Inject
@Suppress("LongParameterList")
@Deprecated("Use MultiWalletContentLoaderV2.Factory instead")
@ModelScoped
internal class MultiWalletContentLoaderFactory @Inject constructor(
private val stateHolder: WalletStateController,
@ -39,7 +39,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val walletsRepository: WalletsRepository,
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
private val currenciesRepository: CurrenciesRepository,
private val accountDependencies: AccountDependencies,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) {
@ -62,7 +61,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
walletsRepository = walletsRepository,
getNFTCollectionsUseCase = getNFTCollectionsUseCase,
currenciesRepository = currenciesRepository,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
)

View file

@ -0,0 +1,59 @@
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("LongParameterList")
internal class MultiWalletContentLoaderV2 @AssistedInject constructor(
@Assisted private val userWallet: UserWallet,
private val accountListSubscriberFactory: AccountListSubscriber.Factory,
private val walletNFTListSubscriberV2Factory: WalletNFTListSubscriberV2.Factory,
private val stateController: WalletStateController,
private val clickIntents: WalletClickIntents,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> = listOf(
accountListSubscriberFactory.create(userWallet = userWallet),
walletNFTListSubscriberV2Factory.create(userWallet = userWallet),
checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet),
MultiWalletWarningsSubscriber(
userWallet = userWallet,
stateHolder = stateController,
clickIntents = clickIntents,
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
),
MultiWalletActionButtonsSubscriber(
userWallet = userWallet,
stateHolder = stateController,
getStoryContentUseCase = getStoryContentUseCase,
),
WalletDropDownItemsSubscriber(
stateHolder = stateController,
shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase,
clickIntents = clickIntents,
),
)
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet): MultiWalletContentLoaderV2
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
@ -9,10 +10,8 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -36,7 +35,6 @@ internal class SingleWalletContentLoader(
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val accountDependencies: AccountDependencies,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> {
@ -55,7 +53,6 @@ internal class SingleWalletContentLoader(
clickIntents = clickIntents,
getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase,
getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase,
accountDependencies = accountDependencies,
),
SingleWalletNotificationsSubscriber(
userWallet = userWallet,

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
@ -10,10 +11,8 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -21,6 +20,7 @@ import javax.inject.Inject
@ModelScoped
@Suppress("LongParameterList")
@Deprecated("Use SingleWalletContentLoaderV2.Factory instead")
internal class SingleWalletContentLoaderFactory @Inject constructor(
private val stateHolder: WalletStateController,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
@ -35,14 +35,12 @@ internal class SingleWalletContentLoaderFactory @Inject constructor(
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val accountDependencies: AccountDependencies,
) {
fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader {
return SingleWalletContentLoader(
userWallet = userWallet,
clickIntents = clickIntents,
accountDependencies = accountDependencies,
isRefresh = isRefresh,
stateHolder = stateHolder,
getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase,

View file

@ -0,0 +1,103 @@
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("LongParameterList")
internal class SingleWalletContentLoaderV2 @AssistedInject constructor(
@Assisted private val userWallet: UserWallet.Cold,
@Assisted private val isRefresh: Boolean,
private val clickIntents: WalletClickIntents,
private val stateHolder: WalletStateController,
private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2,
private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory,
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val accountDependencies: AccountDependencies,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> = listOf(
PrimaryCurrencySubscriberV2(
userWallet = userWallet,
singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
stateController = stateHolder,
analyticsEventHandler = analyticsEventHandler,
),
SingleWalletButtonsSubscriberV2(
userWallet = userWallet,
singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier,
stateController = stateHolder,
clickIntents = clickIntents,
getCryptoCurrencyActionsUseCaseV2 = getCryptoCurrencyActionsUseCaseV2,
),
SingleWalletNotificationsSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
clickIntents = clickIntents,
getSingleWalletWarningsFactory = getSingleWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
),
WalletDropDownItemsSubscriber(
stateHolder = stateHolder,
shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase,
clickIntents = clickIntents,
),
SingleWalletExpressStatusesSubscriberV2(
userWallet = userWallet,
singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier,
getOnrampTransactionsUseCase = getOnrampTransactionsUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase,
stateController = stateHolder,
clickIntents = clickIntents,
analyticsEventHandler = analyticsEventHandler,
),
TxHistorySubscriberV2(
userWallet = userWallet,
singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier,
txHistoryItemsCountUseCase = txHistoryItemsCountUseCase,
txHistoryItemsUseCase = txHistoryItemsUseCase,
isRefresh = isRefresh,
stateController = stateHolder,
clickIntents = clickIntents,
),
CheckWalletWithFundsSubscriber(
userWallet = userWallet,
singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier,
walletWithFundsChecker = walletWithFundsChecker,
dispatchers = dispatchers,
),
)
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoaderV2
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
@ -15,8 +15,8 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
import com.tangem.feature.wallet.presentation.account.AccountDependencies
@Deprecated("Use SingleWalletWithTokenContentLoaderV2 instead")
@Suppress("LongParameterList")
internal class SingleWalletWithTokenContentLoader(
private val userWallet: UserWallet.Cold,
@ -31,7 +31,6 @@ internal class SingleWalletWithTokenContentLoader(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val accountDependencies: AccountDependencies,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
@ -46,7 +45,6 @@ internal class SingleWalletWithTokenContentLoader(
walletWithFundsChecker = walletWithFundsChecker,
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
).let(::add)

View file

@ -2,8 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
@ -15,11 +15,11 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import javax.inject.Inject
// TODO: Refactor
@Suppress("LongParameterList")
@Deprecated("Use SingleWalletWithTokenContentLoaderV2.Factory instead")
@ModelScoped
internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val stateHolder: WalletStateController,
@ -32,7 +32,6 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val accountDependencies: AccountDependencies,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) {
@ -51,7 +50,6 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
getStoryContentUseCase = getStoryContentUseCase,
shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
)

View file

@ -0,0 +1,50 @@
package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("LongParameterList")
internal class SingleWalletWithTokenContentLoaderV2 @AssistedInject constructor(
@Assisted private val userWallet: UserWallet.Cold,
private val singleWalletWithTokenSubscriberFactory: SingleWalletWithTokenSubscriber.Factory,
private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory,
private val clickIntents: WalletClickIntents,
private val stateController: WalletStateController,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> = listOf(
singleWalletWithTokenSubscriberFactory.create(userWallet),
MultiWalletWarningsSubscriber(
userWallet = userWallet,
stateHolder = stateController,
clickIntents = clickIntents,
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
),
WalletDropDownItemsSubscriber(
stateHolder = stateController,
shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase,
clickIntents = clickIntents,
),
checkWalletWithFundsSubscriberFactory.create(userWallet),
)
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenContentLoaderV2
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus.CANCELED
import com.tangem.domain.pay.model.OrderStatus.UNKNOWN
@ -35,29 +36,36 @@ internal class TangemPayInitialStateTransformer(
private fun createInitialState(): TangemPayState {
val cardInfo = value?.info?.cardInfo
val productInstance = value?.info?.productInstance
return when {
value == null -> TangemPayState.Empty
!value.info.isKycApproved -> createKycInProgressState(onClickKyc)
cardInfo != null -> getCardInfoState(cardInfo)
cardInfo != null && productInstance != null -> getCardInfoState(cardInfo, productInstance)
value.orderStatus == UNKNOWN || value.orderStatus == CANCELED -> createIssueAvailableState(onClickIssue)
else -> createIssueProgressState()
}
}
private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),
onClick = {
openDetails(
TangemPayDetailsConfig(
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
depositAddress = cardInfo.depositAddress,
),
)
},
)
private fun getCardInfoState(cardInfo: CardInfo, productInstance: ProductInstance): TangemPayState =
TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),
onClick = {
openDetails(
TangemPayDetailsConfig(
cardId = productInstance.cardId,
isCardFrozen = when (productInstance.status) {
ProductInstance.Status.ACTIVE -> false
ProductInstance.Status.INACTIVE -> true
},
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
depositAddress = cardInfo.depositAddress,
),
)
},
)
private fun getBalanceText(cardInfo: CardInfo): String {
val currency = Currency.getInstance(cardInfo.currencyCode)

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.PortfolioId
@ -21,7 +22,6 @@ import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
@ -87,8 +87,6 @@ internal class TokenListStateConverter(
private fun convertAccountList(params: TokenConverterParams.Account): WalletTokensListState {
val accountList = params.accountList
// todo account null for firs iteration?
val organizeTokensButtonConfig: OrganizeTokensButtonConfig? = null
fun AccountStatus.CryptoPortfolio.map(): TokensListItemUM.Portfolio {
val tokenList: TokenList = this.tokenList
@ -105,6 +103,7 @@ internal class TokenListStateConverter(
appCurrency = appCurrency,
account = account,
onItemClick = onItemClick,
priceChangeLce = this.priceChangeLce,
)
val accountItem = converter.convert(tokenList.totalFiatBalance)
val tokenConverter = tokenStatusConverter(account.accountId)
@ -132,7 +131,7 @@ internal class TokenListStateConverter(
}
return WalletTokensListState.ContentState.PortfolioContent(
items = accountItems.toPersistentList(),
organizeTokensButtonConfig = organizeTokensButtonConfig,
organizeTokensButtonConfig = getOrganizeTokensButtonStateV2(accountList = accountList),
)
}
@ -197,6 +196,17 @@ internal class TokenListStateConverter(
}
}
private fun getOrganizeTokensButtonStateV2(accountList: AccountStatusList): WalletOrganizeTokensButtonConfig? {
return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) {
WalletOrganizeTokensButtonConfig(
isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading,
onClick = clickIntents::onOrganizeTokensClick,
)
} else {
null
}
}
private fun isSingleCurrencyWalletWithToken(): Boolean {
return selectedWallet is UserWallet.Cold &&
selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()

View file

@ -0,0 +1,57 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.utils.coroutines.combine6
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
/**
* Subscriber that monitors account list related data and updates the wallet state accordingly.
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class AccountListSubscriber @AssistedInject constructor(
@Assisted override val userWallet: UserWallet,
override val accountDependencies: AccountDependencies,
override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
override val stateController: WalletStateController,
override val clickIntents: WalletClickIntents,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : BasicAccountListSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6(
flow1 = getAccountStatusListFlow(),
flow2 = getAppCurrencyFlow(),
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet),
flow4 = accountDependencies.isAccountsModeEnabledUseCase(),
flow5 = yieldSupplyApyFlow(),
flow6 = stakingApyFlow(),
transform = ::updateState,
)
private fun yieldSupplyApyFlow(): Flow<Map<String, String>> {
return yieldSupplyApyFlowUseCase().distinctUntilChanged()
}
private fun stakingApyFlow(): Flow<Map<String, List<Yield.Validator>>> {
return stakingApyFlowUseCase().distinctUntilChanged()
}
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet): AccountListSubscriber
}
}

View file

@ -0,0 +1,146 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import timber.log.Timber
/**
* Basic implementation of [WalletSubscriber] for wallet with accounts.
*
[REDACTED_AUTHOR]
*/
internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
abstract val accountDependencies: AccountDependencies
abstract val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase
abstract val stateController: WalletStateController
abstract val clickIntents: WalletClickIntents
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier
get() = accountDependencies.singleAccountStatusListSupplier
protected fun getAppCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase.invokeOrDefault()
.distinctUntilChanged()
}
protected fun updateState(
accountList: AccountStatusList,
appCurrency: AppCurrency,
expandedAccounts: Set<AccountId>,
isAccountMode: Boolean,
yieldSupplyApyMap: Map<String, String> = emptyMap(),
stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
) {
val accountFlattenCurrencies = accountList.flattenCurrencies()
val mainAccount = accountList.mainAccount
when {
!isAccountMode -> {
val isMainAccountEmpty = mainAccount.tokenList.flattenCurrencies().isEmpty()
val maybeTokenList = if (isMainAccountEmpty) {
Lce.Error(TokenListError.EmptyTokens)
} else {
Lce.Content(mainAccount.tokenList)
}
singleAccountTransform(
maybeTokenList = maybeTokenList,
appCurrency = appCurrency,
portfolioId = PortfolioId(mainAccount.accountId),
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
)
}
isAccountMode -> {
val isAllAccountsEmpty = accountFlattenCurrencies.isEmpty()
if (isAllAccountsEmpty) {
stateController.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = TokenListError.EmptyTokens,
appCurrency = appCurrency,
),
)
} else {
val convertParams = TokenConverterParams.Account(accountList, expandedAccounts)
updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap)
}
}
}
}
private fun singleAccountTransform(
maybeTokenList: Lce<TokenListError, TokenList>,
appCurrency: AppCurrency,
portfolioId: PortfolioId,
yieldSupplyApyMap: Map<String, String> = emptyMap(),
stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
) {
val tokenList = maybeTokenList.getOrElse(
ifLoading = { maybeContent ->
val isRefreshing = stateController.getWalletState(userWallet.walletId)
?.pullToRefreshConfig
?.isRefreshing == true
maybeContent
?.takeIf { !isRefreshing }
?: return
},
ifError = { e ->
Timber.e("Failed to load token list: $e")
stateController.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = e,
appCurrency = appCurrency,
),
)
return
},
)
updateContent(
params = TokenConverterParams.Wallet(portfolioId, tokenList),
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
)
}
private fun updateContent(
params: TokenConverterParams,
appCurrency: AppCurrency,
yieldSupplyApyMap: Map<String, String> = emptyMap(),
stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
) {
stateController.update(
SetTokenListTransformer(
params = params,
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
),
)
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapNotNull
/**
* Basic implementation of [WalletSubscriber] for single wallet.
*
[REDACTED_AUTHOR]
*/
internal abstract class BasicSingleWalletSubscriber : BasicWalletSubscriber() {
/** Account ID for the main crypto portfolio of the user wallet */
val accountId: AccountId
get() = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId)
/**
* Provides a flow of the primary [CryptoCurrencyStatus] for the associated user wallet.
*
* @return A [Flow] emitting the primary [CryptoCurrencyStatus] of the wallet, distinct and conflated.
*/
protected fun getPrimaryCurrencyStatusFlow(): Flow<CryptoCurrencyStatus> {
return getMainAccountStatusFlow()
.mapNotNull { accountStatus ->
accountStatus.flattenCurrencies().firstOrNull()
}
.distinctUntilChanged()
.conflate()
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import arrow.core.getOrElse
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
@ -9,7 +8,6 @@ import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
@ -18,7 +16,6 @@ import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -26,43 +23,33 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetToken
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.combine6
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import timber.log.Timber
@Deprecated("Use AccountListSubscriber instead")
@Suppress("LongParameterList")
internal abstract class BasicTokenListSubscriber : WalletSubscriber() {
protected abstract val userWallet: UserWallet
protected abstract val stateHolder: WalletStateController
protected abstract val clickIntents: WalletClickIntents
protected abstract val tokenListAnalyticsSender: TokenListAnalyticsSender
protected abstract val walletWithFundsChecker: WalletWithFundsChecker
protected abstract val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase
protected abstract val accountDependencies: AccountDependencies
protected abstract val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase
protected abstract val stakingApyFlowUseCase: StakingApyFlowUseCase
internal abstract class BasicTokenListSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,
private val clickIntents: WalletClickIntents,
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : WalletSubscriber() {
private val sendAnalyticsJobHolder = JobHolder()
private val onTokenListReceivedJobHolder = JobHolder()
protected val isAccountsEnabled get() = accountDependencies.accountsFeatureToggles.isFeatureEnabled
protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList>
protected abstract fun accountListFlow(coroutineScope: CoroutineScope): Flow<AccountStatusList>
protected abstract suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>)
protected open suspend fun onAccountListReceived() = {
// todo account updateSortingIfNeeded? like [onTokenListReceived]
}
override fun create(coroutineScope: CoroutineScope): Flow<*> =
if (isAccountsEnabled) createAccountListFlow(coroutineScope) else createTokenListFlow(coroutineScope)
private fun createTokenListFlow(coroutineScope: CoroutineScope): Flow<*> {
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return combine(
flow = tokenListFlow(coroutineScope)
.onEach { maybeTokenList ->
@ -83,122 +70,41 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() {
flow3 = yieldSupplyApyFlow(),
flow4 = stakingApyFlow(),
transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap ->
singleAccountTransform(
maybeTokenList = maybeTokenList,
val tokenList = maybeTokenList.getOrElse(
ifLoading = { maybeContent ->
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
?.pullToRefreshConfig
?.isRefreshing == true
maybeContent
?.takeIf { !isRefreshing }
?: return@combine
},
ifError = { e ->
Timber.e("Failed to load token list: $e")
stateHolder.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = e,
appCurrency = appCurrency,
),
)
return@combine
},
)
updateContent(
params = TokenConverterParams.Wallet(PortfolioId(userWallet.walletId), tokenList),
appCurrency = appCurrency,
portfolioId = PortfolioId(userWallet.walletId),
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
)
walletWithFundsChecker.check(tokenList)
},
)
}
private suspend fun singleAccountTransform(
maybeTokenList: Lce<TokenListError, TokenList>,
appCurrency: AppCurrency,
portfolioId: PortfolioId,
yieldSupplyApyMap: Map<String, String>,
stakingApyMap: Map<String, List<Yield.Validator>>,
) {
val tokenList = maybeTokenList.getOrElse(
ifLoading = { maybeContent ->
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
?.pullToRefreshConfig
?.isRefreshing == true
maybeContent
?.takeIf { !isRefreshing }
?: return
},
ifError = { e ->
Timber.e("Failed to load token list: $e")
stateHolder.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = e,
appCurrency = appCurrency,
),
)
return
},
)
updateContent(
params = TokenConverterParams.Wallet(portfolioId, tokenList),
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
)
walletWithFundsChecker.check(tokenList)
}
private fun createAccountListFlow(coroutineScope: CoroutineScope): Flow<*> = combine6(
flow1 = accountListFlow(coroutineScope)
.onEach { accountStatusList ->
coroutineScope.launch {
sendTokenListAnalytics(
flattenCurrencies = accountStatusList.flattenCurrencies(),
totalFiatBalance = accountStatusList.totalFiatBalance,
)
}.saveIn(sendAnalyticsJobHolder)
}
.distinctUntilChanged(),
flow2 = appCurrencyFlow(),
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet),
flow4 = accountDependencies.isAccountsModeEnabledUseCase(),
flow5 = yieldSupplyApyFlow(),
flow6 = stakingApyFlow(),
transform = { accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, stakingApyMap ->
val accountFlattenTokensList = accountList.flattenTokens()
val accountFlattenCurrencies = accountFlattenTokensList
.map { it.flattenCurrencies() }
.flatten()
val mainAccount: AccountStatus.CryptoPortfolio = when (val mainAccount = accountList.mainAccount) {
is AccountStatus.CryptoPortfolio -> mainAccount
}
suspend fun singleAccountTransform(maybeTokenList: Lce<TokenListError, TokenList>) =
this.singleAccountTransform(
maybeTokenList,
appCurrency,
PortfolioId(mainAccount.account.accountId),
yieldSupplyApyMap,
stakingApyMap,
)
when {
!isAccountMode -> when (mainAccount.tokenList.flattenCurrencies().isEmpty()) {
true -> singleAccountTransform(Lce.Error(TokenListError.EmptyTokens))
false -> singleAccountTransform(Lce.Content(mainAccount.tokenList))
}
isAccountMode -> when (accountFlattenCurrencies.isEmpty()) {
true -> stateHolder.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = TokenListError.EmptyTokens,
appCurrency = appCurrency,
),
)
false -> {
val convertParams = TokenConverterParams.Account(accountList, expandedAccounts)
updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap)
accountFlattenTokensList
.map { tokenList -> coroutineScope.launch { walletWithFundsChecker.check(tokenList) } }
.joinAll()
}
}
}
},
)
private fun AccountStatusList.flattenTokens(): List<TokenList> = this.accountStatuses.map {
when (it) {
is AccountStatus.CryptoPortfolio -> it.tokenList
}
}
private suspend fun sendTokenListAnalytics(
flattenCurrencies: List<CryptoCurrencyStatus>?,
totalFiatBalance: TotalFiatBalance?,

View file

@ -0,0 +1,64 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapNotNull
/**
* Basic implementation of [WalletSubscriber] for wallet.
*
[REDACTED_AUTHOR]
*/
internal abstract class BasicWalletSubscriber : WalletSubscriber() {
/** User wallet associated with this subscriber */
abstract val userWallet: UserWallet
/** Supplier to get the account status for the wallet */
abstract val singleAccountStatusListSupplier: SingleAccountStatusListSupplier
/**
* Provides a flow of [AccountStatusList] for the associated user wallet.
*
* @return A [Flow] emitting the [AccountStatusList] of the wallet, distinct and conflated.
*/
protected fun getAccountStatusListFlow(): Flow<AccountStatusList> {
return singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWallet.walletId),
)
.distinctUntilChanged()
.conflate()
}
/**
* Provides a flow of [AccountStatus] for the associated user wallet.
*
* @return A [Flow] emitting the [AccountStatus] of the wallet, distinct and conflated.
*/
protected fun getMainAccountStatusFlow(): Flow<AccountStatus.CryptoPortfolio> {
return getAccountStatusListFlow()
.mapNotNull { accountStatusList ->
accountStatusList.accountStatuses.find { accountStatus ->
when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.isMainAccount
}
} as? AccountStatus.CryptoPortfolio
}
.distinctUntilChanged()
.conflate()
}
protected fun getCryptoCurrencyStatusesFlow(): Flow<List<CryptoCurrencyStatus>> {
return getAccountStatusListFlow()
.mapNotNull(AccountStatusList::flattenCurrencies)
.distinctUntilChanged()
.conflate()
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
/**
* Subscriber that checks if the wallet has funds and notifies the [WalletWithFundsChecker].
*
* @property userWallet The user wallet.
* @property singleAccountStatusListSupplier Supplier for account status list.
* @property walletWithFundsChecker The checker to notify when funds are detected.
* @property dispatchers Coroutine dispatcher provider.
*
[REDACTED_AUTHOR]
*/
internal class CheckWalletWithFundsSubscriber @AssistedInject constructor(
@Assisted override val userWallet: UserWallet,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val dispatchers: CoroutineDispatcherProvider,
) : BasicWalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return getAccountStatusListFlow()
.mapNotNull { it.flattenCurrencies() }
.distinctUntilChanged()
.onEach { walletWithFundsChecker.check(userWalletId = userWallet.walletId, currencies = it) }
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet): CheckWalletWithFundsSubscriber
}
}

View file

@ -1,7 +1,5 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
@ -15,28 +13,35 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@Deprecated("Use AccountListSubscriber instead")
@Suppress("LongParameterList")
internal class MultiWalletTokenListSubscriber(
override val userWallet: UserWallet,
private val userWallet: UserWallet,
private val tokenListStore: MultiWalletTokenListStore,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
override val stateHolder: WalletStateController,
override val clickIntents: WalletClickIntents,
override val tokenListAnalyticsSender: TokenListAnalyticsSender,
override val walletWithFundsChecker: WalletWithFundsChecker,
override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
override val accountDependencies: AccountDependencies,
override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
override val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : BasicTokenListSubscriber() {
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
stakingApyFlowUseCase: StakingApyFlowUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
clickIntents = clickIntents,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
) {
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {
tokenListStore.addIfNot(userWallet.walletId, coroutineScope)
@ -44,11 +49,6 @@ internal class MultiWalletTokenListSubscriber(
return tokenListStore.getOrThrow(userWallet.walletId)
}
override fun accountListFlow(coroutineScope: CoroutineScope): Flow<AccountStatusList> {
val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
return accountDependencies.singleAccountStatusListSupplier(params)
}
override suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>) {
updateSortingIfNeeded(maybeTokenList)
}

View file

@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.math.BigDecimal
@Deprecated("Use PrimaryCurrencySubscriberV2 instead")
internal class PrimaryCurrencySubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,

View file

@ -0,0 +1,84 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
import java.math.BigDecimal
internal class PrimaryCurrencySubscriberV2(
override val userWallet: UserWallet,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val stateController: WalletStateController,
private val analyticsEventHandler: AnalyticsEventHandler,
) : BasicSingleWalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return combine(
flow = getPrimaryCurrencyStatusFlow(),
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
transform = ::Pair,
)
.onEach { (status, appCurrency) ->
updateContent(status, appCurrency)
sendAnalyticsEvent(status)
}
}
private fun updateContent(status: CryptoCurrencyStatus, appCurrency: AppCurrency) {
stateController.update(
SetPrimaryCurrencyTransformer(
status = status,
userWallet = userWallet,
appCurrency = appCurrency,
),
)
}
private fun sendAnalyticsEvent(status: CryptoCurrencyStatus) {
val fiatAmount = status.value.fiatAmount
val cardBalanceState = when (status.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.NoAmount,
-> createCardBalanceState(fiatAmount)
is CryptoCurrencyStatus.NoQuote -> AnalyticsParam.CardBalanceState.NoRate
is CryptoCurrencyStatus.Unreachable,
-> AnalyticsParam.CardBalanceState.BlockchainError
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.Custom,
-> null
}
cardBalanceState?.let {
// do not send tokens count for single currency wallet
analyticsEventHandler.send(
event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(
balance = it,
tokensCount = null,
),
)
}
}
private fun createCardBalanceState(fiatAmount: BigDecimal?): AnalyticsParam.CardBalanceState? {
return when {
fiatAmount == null -> null
fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
}
}

View file

@ -7,20 +7,19 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
@Deprecated("Use SingleWalletButtonsSubscriberV2 instead")
internal class SingleWalletButtonsSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,
private val clickIntents: WalletClickIntents,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val accountDependencies: AccountDependencies,
) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<TokenActionsState> {
@ -31,11 +30,10 @@ internal class SingleWalletButtonsSubscriber(
}
}
.onEach { actions ->
val portfolioId = when (accountDependencies.accountsFeatureToggles.isFeatureEnabled) {
true -> TODO("account") // get main account id for single wallet
false -> PortfolioId(userWallet.walletId)
}
updateContent(actions, portfolioId)
updateContent(
tokenActionsState = actions,
portfolioId = PortfolioId(userWallet.walletId),
)
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onEach
internal class SingleWalletButtonsSubscriberV2(
override val userWallet: UserWallet,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val stateController: WalletStateController,
private val clickIntents: WalletClickIntents,
private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2,
) : BasicSingleWalletSubscriber() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun create(coroutineScope: CoroutineScope): Flow<TokenActionsState> {
return getPrimaryCurrencyStatusFlow()
.flatMapLatest {
getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency)
}
.onEach {
updateContent(tokenActionsState = it, portfolioId = PortfolioId(userWallet.walletId))
}
}
private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) {
stateController.update(
SetCryptoCurrencyActionsTransformer(
tokenActionsState = tokenActionsState,
userWallet = userWallet,
clickIntents = clickIntents,
portfolioId = portfolioId,
),
)
}
}

View file

@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.*
import timber.log.Timber
@Suppress("LongParameterList")
@Deprecated("Use SingleWalletExpressStatusesSubscriberV2 instead")
internal class SingleWalletExpressStatusesSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,

View file

@ -0,0 +1,84 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@Suppress("LongParameterList")
internal class SingleWalletExpressStatusesSubscriberV2(
override val userWallet: UserWallet,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val stateController: WalletStateController,
private val clickIntents: WalletClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
) : BasicSingleWalletSubscriber() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun create(coroutineScope: CoroutineScope): Flow<*> {
val getOnrampTransactionsFlow = getPrimaryCurrencyStatusFlow().flatMapLatest { currencyStatus ->
getOnrampTransactionsUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyId = currencyStatus.currency.id,
)
.map { currencyStatus to it }
}
return combine(
flow = getOnrampTransactionsFlow,
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
transform = ::toTriple,
)
.onEach { (status, maybeTransaction, appCurrency) ->
maybeTransaction.fold(
ifRight = { onrampTxs ->
onrampTxs.clearHiddenTerminal()
stateController.update(
SetExpressStatusesTransformer(
userWalletId = userWallet.walletId,
onrampTxs = onrampTxs,
clickIntents = clickIntents,
cryptoCurrencyStatus = status,
appCurrency = appCurrency,
analyticsEventHandler = analyticsEventHandler,
),
)
},
ifLeft = {
stateController.update(
SetExpressStatusesTransformer(
userWalletId = userWallet.walletId,
onrampTxs = listOf(),
clickIntents = clickIntents,
cryptoCurrencyStatus = status,
appCurrency = appCurrency,
analyticsEventHandler = analyticsEventHandler,
),
)
},
)
}
}
private fun <A, B, C> toTriple(firstPair: Pair<A, B>, second: C): Triple<A, B, C> {
return Triple(firstPair.first, firstPair.second, second)
}
private suspend fun List<OnrampTransaction>.clearHiddenTerminal() {
this
.filter { it.status.isHidden && it.status.isTerminal }
.forEach { onrampRemoveTransactionUseCase(txId = it.txId) }
}
}

View file

@ -1,7 +1,5 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
@ -11,27 +9,34 @@ import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@Deprecated("Use SingleWalletWithTokenSubscriber instead")
@Suppress("LongParameterList")
internal class SingleWalletWithTokenListSubscriber(
override val userWallet: UserWallet.Cold,
private val userWallet: UserWallet.Cold,
private val tokenListStore: MultiWalletTokenListStore,
override val stateHolder: WalletStateController,
override val clickIntents: WalletClickIntents,
override val tokenListAnalyticsSender: TokenListAnalyticsSender,
override val walletWithFundsChecker: WalletWithFundsChecker,
override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
override val accountDependencies: AccountDependencies,
override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
override val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : BasicTokenListSubscriber() {
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
stakingApyFlowUseCase: StakingApyFlowUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
clickIntents = clickIntents,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
) {
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {
tokenListStore.addIfNot(userWallet.walletId, coroutineScope)
@ -39,10 +44,5 @@ internal class SingleWalletWithTokenListSubscriber(
return tokenListStore.getOrThrow(userWallet.walletId)
}
override fun accountListFlow(coroutineScope: CoroutineScope): Flow<AccountStatusList> {
val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
return accountDependencies.singleAccountStatusListSupplier(params)
}
override suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>) = Unit
}

View file

@ -0,0 +1,35 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
internal class SingleWalletWithTokenSubscriber @AssistedInject constructor(
@Assisted override val userWallet: UserWallet.Cold,
override val accountDependencies: AccountDependencies,
override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
override val stateController: WalletStateController,
override val clickIntents: WalletClickIntents,
) : BasicAccountListSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<Unit> = combine(
flow = getAccountStatusListFlow(),
flow2 = getAppCurrencyFlow(),
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet),
flow4 = accountDependencies.isAccountsModeEnabledUseCase(),
transform = ::updateState,
)
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenSubscriber
}
}

View file

@ -30,6 +30,7 @@ typealias MaybeTxHistoryCount = Either<TxHistoryStateError, Int>
typealias MaybeTxHistoryItems = Either<TxHistoryListError, Flow<PagingData<TxInfo>>>
@Suppress("LongParameterList")
@Deprecated("Use TxHistorySubscriberV2 instead")
internal class TxHistorySubscriber(
private val userWallet: UserWallet.Cold,
private val isRefresh: Boolean,

View file

@ -0,0 +1,111 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
@Suppress("LongParameterList")
internal class TxHistorySubscriberV2(
override val userWallet: UserWallet.Cold,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val isRefresh: Boolean,
private val stateController: WalletStateController,
private val clickIntents: WalletClickIntents,
) : BasicSingleWalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<PagingData<TxInfo>> {
return flow {
getPrimaryCurrencyStatusFlow().collectLatest { status ->
val maybeTxHistoryItemCount = txHistoryItemsCountUseCase(
userWalletId = userWallet.walletId,
currency = status.currency,
)
setLoadingTxHistoryState(maybeTxHistoryItemCount, status)
maybeTxHistoryItemCount.onRight {
val maybeTxHistoryItems = txHistoryItemsUseCase(
userWalletId = userWallet.walletId,
currency = status.currency,
refresh = isRefresh,
).map { it.cachedIn(coroutineScope) }
setLoadedTxHistoryState(maybeTxHistoryItems)
}
}
}
}
private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) {
stateController.update(
maybeTxHistoryItemCount.fold(
ifLeft = {
SetTxHistoryCountErrorTransformer(
userWallet = userWallet,
error = it,
pendingTransactions = status.value.pendingTransactions,
clickIntents = clickIntents,
)
},
ifRight = {
SetTxHistoryCountTransformer(
userWalletId = userWallet.walletId,
transactionsCount = it,
clickIntents = clickIntents,
)
},
),
)
}
private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) {
stateController.update(
maybeTxHistoryItems.fold(
ifLeft = {
SetTxHistoryItemsErrorTransformer(
userWalletId = userWallet.walletId,
error = it,
clickIntents = clickIntents,
)
},
ifRight = { itemsFlow ->
val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
val itemConverter = TxHistoryItemStateConverter(
symbol = blockchain.currency,
decimals = blockchain.decimals(),
clickIntents = clickIntents,
)
SetTxHistoryItemsTransformer(
userWallet = userWallet,
flow = itemsFlow.map { items ->
items.map(itemConverter::convert)
},
clickIntents = clickIntents,
)
},
),
)
}
}

View file

@ -5,7 +5,9 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWalletCardDropDownItemsTransformer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
internal class WalletDropDownItemsSubscriber(
private val stateHolder: WalletStateController,
@ -13,18 +15,15 @@ internal class WalletDropDownItemsSubscriber(
private val clickIntents: WalletClickIntents,
) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return flow<Any> {
shouldSaveUserWalletsUseCase.invoke()
.distinctUntilChanged()
.onEach {
stateHolder.update(
SetWalletCardDropDownItemsTransformer(
dropdownEnabled = it,
clickIntents = clickIntents,
),
)
}
.launchIn(coroutineScope)
}
return shouldSaveUserWalletsUseCase.invoke()
.distinctUntilChanged()
.onEach {
stateHolder.update(
SetWalletCardDropDownItemsTransformer(
dropdownEnabled = it,
clickIntents = clickIntents,
),
)
}
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.RemoveNFTCollectionsTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
internal class WalletNFTListSubscriberV2 @AssistedInject constructor(
@Assisted override val userWallet: UserWallet,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val walletsRepository: WalletsRepository,
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
private val stateController: WalletStateController,
private val clickIntents: WalletClickIntents,
) : BasicWalletSubscriber() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine(
flow = walletsRepository.nftEnabledStatus(userWallet.walletId),
flow2 = getCryptoCurrencyStatusesFlow(),
transform = ::Pair,
)
.distinctUntilChanged()
.flatMapLatest { (nftEnabled, currencies) ->
// if NFT is enabled for this wallet and there are currencies,
// then start observing changes from store and apply transformer if need
if (nftEnabled && currencies.isNotEmpty()) {
getNFTCollectionsUseCase(userWallet.walletId)
.shareIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
.onEach {
stateController.update(
SetNFTCollectionsTransformer(
userWalletId = userWallet.walletId,
nftCollections = it,
onItemClick = { clickIntents.onNFTClick(userWallet) },
),
)
}
} else {
// otherwise, hide NFT from wallet
stateController.update(
RemoveNFTCollectionsTransformer(userWallet.walletId),
)
emptyFlow()
}
}
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet): WalletNFTListSubscriberV2
}
}

View file

@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -55,6 +56,7 @@ import com.tangem.core.ui.components.rememberIsKeyboardVisible
import com.tangem.core.ui.components.sheetscaffold.*
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.stringResourceSafe
@ -69,12 +71,14 @@ import com.tangem.core.ui.utils.moveTo
import com.tangem.core.ui.utils.toPx
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.*
import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
@ -142,6 +146,7 @@ private fun WalletContent(
*/
val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] }
val (expandedState, collapsedState) = getExpandPortfolioStates(selectedWallet)
val listState = rememberLazyListState()
@ -216,7 +221,7 @@ private fun WalletContent(
item(
key = "TangemPayMainScreenBlock",
contentType = state.tangemPayState::class.java,
) { TangemPayMainScreenBlock(state.tangemPayState, itemModifier) }
) { TangemPayMainScreenBlock(state.tangemPayState, isBalanceHidden = state.isHidingMode, itemModifier) }
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
@ -242,6 +247,13 @@ private fun WalletContent(
txHistoryItems = txHistoryItems,
isBalanceHidden = state.isHidingMode,
modifier = movableItemModifier,
portfolioVisibleState = {
findPortfolioVisibleState(
portfolio = it,
expandedState = expandedState,
collapsedState = collapsedState,
)
},
)
nftCollections(state = selectedWallet, itemModifier = itemModifier)
@ -289,6 +301,61 @@ private fun WalletContent(
)
}
private fun findPortfolioVisibleState(
portfolio: TokensListItemUM.Portfolio,
expandedState: SnapshotStateMap<String, MutableTransitionState<Boolean>>,
collapsedState: SnapshotStateMap<String, MutableTransitionState<Boolean>>,
): MutableTransitionState<Boolean> {
val portfolioKey = portfolio.id
fun forceVisible() = MutableTransitionState(true).apply { targetState = true }
val shouldExpand = portfolio.isExpanded
return if (shouldExpand) {
collapsedState[portfolioKey] ?: forceVisible()
} else {
expandedState[portfolioKey] ?: forceVisible()
}
}
@Composable
private fun getExpandPortfolioStates(
state: WalletState,
): Pair<
SnapshotStateMap<String, MutableTransitionState<Boolean>>,
SnapshotStateMap<String, MutableTransitionState<Boolean>>,
> {
val expandedTransitionState =
remember { mutableStateMapOf<String, MutableTransitionState<Boolean>>() }
val collapsedTransitionState =
remember { mutableStateMapOf<String, MutableTransitionState<Boolean>>() }
val portfolioContent = state is WalletState.MultiCurrency.Content &&
state.tokensListState is WalletTokensListState.ContentState.PortfolioContent
if (!portfolioContent) {
return expandedTransitionState to collapsedTransitionState
}
state.tokensListState.items.fastForEach { portfolio ->
val portfolioKey = portfolio.id
val shouldExpand = portfolio.isExpanded
fun toggleVisible() = MutableTransitionState(false).apply { targetState = true }
fun forceVisible() = MutableTransitionState(true).apply { targetState = true }
val isFirstCall = collapsedTransitionState[portfolioKey] == null ||
expandedTransitionState[portfolioKey] == null
when {
isFirstCall -> {
collapsedTransitionState[portfolioKey] = forceVisible()
expandedTransitionState[portfolioKey] = forceVisible()
}
shouldExpand -> expandedTransitionState[portfolioKey] = toggleVisible()
else -> collapsedTransitionState[portfolioKey] = toggleVisible()
}
}
return expandedTransitionState to collapsedTransitionState
}
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -743,6 +810,7 @@ private class WalletScreenPreviewProvider : PreviewParameterProvider<WalletScree
walletScreenState,
walletScreenState.copy(selectedWalletIndex = 1),
accountScreenState.copy(selectedWalletIndex = 1),
accountScreenWithEmptyTokensState.copy(selectedWalletIndex = 1),
)
}
// endregion

View file

@ -1,8 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import androidx.paging.compose.LazyPagingItems
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -22,10 +24,11 @@ internal fun LazyListScope.contentItems(
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) {
when (state) {
is WalletState.MultiCurrency -> {
tokensListItems(state.tokensListState, modifier, isBalanceHidden)
tokensListItems(state.tokensListState, modifier, isBalanceHidden, portfolioVisibleState)
}
is WalletState.SingleCurrency -> {
txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)

View file

@ -8,9 +8,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
@ -27,6 +25,7 @@ internal fun LazyListScope.portfolioContentItems(
items: ImmutableList<TokensListItemUM.Portfolio>,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) {
items.forEachIndexed { index, item ->
portfolioTokensList(
@ -34,6 +33,7 @@ internal fun LazyListScope.portfolioContentItems(
modifier = modifier,
portfolioIndex = index,
isBalanceHidden = isBalanceHidden,
portfolioVisibleState = portfolioVisibleState,
)
}
}
@ -43,6 +43,7 @@ internal fun LazyListScope.portfolioTokensList(
modifier: Modifier,
portfolioIndex: Int,
isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
@ -52,8 +53,33 @@ internal fun LazyListScope.portfolioTokensList(
modifier = modifier,
portfolioIndex = portfolioIndex,
isBalanceHidden = isBalanceHidden,
portfolioVisibleState = portfolioVisibleState,
)
if (!isExpanded) return
if (tokens.isEmpty()) {
item(
key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}",
contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}",
) {
val appear = portfolioVisibleState(portfolio)
SlideInItemVisibility(
modifier = modifier
.animateItem()
.roundedShapeItemDecoration(
radius = TangemTheme.dimens.radius14,
currentIndex = 1,
lastIndex = 1,
backgroundColor = TangemTheme.colors.background.primary,
),
visibleState = appear,
) {
NonContentItemContent(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing28),
)
}
}
return
}
itemsIndexed(
items = tokens,
key = { _, item -> item.id },
@ -61,15 +87,13 @@ internal fun LazyListScope.portfolioTokensList(
itemContent = { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc()
val lastIndex = tokens.lastIndex.inc()
val isPreview = LocalInspectionMode.current
val appear = remember {
MutableTransitionState(isPreview).apply { targetState = true }
}
val appear = portfolioVisibleState(portfolio)
SlideInItemVisibility(
modifier = modifier
.testModifier(indexWithHeader)
.animateItem()
.roundedShapeItemDecoration(
radius = TangemTheme.dimens.radius14,
currentIndex = indexWithHeader,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
@ -92,10 +116,17 @@ private fun LazyListScope.portfolioItem(
modifier: Modifier,
portfolioIndex: Int,
isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
val lastIndex = when {
isExpanded && tokens.isEmpty() -> 1
isExpanded -> tokens.lastIndex.inc()
else -> 0
}
item(
key = "account-${portfolio.id}-isExpanded$isExpanded",
contentType = "account-isExpanded$isExpanded",
@ -105,27 +136,20 @@ private fun LazyListScope.portfolioItem(
.animateItem()
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = if (isExpanded) tokens.lastIndex.inc() else 0,
radius = TangemTheme.dimens.radius14,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
)
val isPreview = LocalInspectionMode.current
val appear = remember {
MutableTransitionState(isPreview).apply { targetState = true }
}
val appear = portfolioVisibleState(portfolio)
if (isExpanded) {
SlideInItemVisibility(
modifier = anchorModifier,
visibleState = appear,
) {
val modifier = if (portfolio.tokens.isEmpty()) {
Modifier.padding(vertical = 8.dp)
} else {
Modifier.padding(top = 8.dp)
}
PortfolioListItem(
state = portfolio,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
modifier = Modifier.padding(vertical = 8.dp),
)
}
} else {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -8,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@ -25,7 +27,7 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import kotlinx.collections.immutable.ImmutableList
private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
/**
* LazyList extension for [WalletTokensListState]
@ -39,12 +41,14 @@ internal fun LazyListScope.tokensListItems(
state: WalletTokensListState,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
portfolioVisibleState: (portfolio: TokensListItemUM.Portfolio) -> MutableTransitionState<Boolean>,
) {
when (state) {
is WalletTokensListState.ContentState.PortfolioContent -> portfolioContentItems(
items = state.items,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
portfolioVisibleState = portfolioVisibleState,
)
is WalletTokensListState.ContentState.Content,
is WalletTokensListState.ContentState.Loading,
@ -89,27 +93,34 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) {
key = NON_CONTENT_TOKENS_LIST_KEY,
contentType = NON_CONTENT_TOKENS_LIST_KEY,
) {
Column(
NonContentItemContent(
modifier = modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(top = TangemTheme.dimens.spacing96),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(id = R.drawable.ic_empty_64),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size64),
tint = TangemTheme.colors.icon.inactive,
)
)
}
}
Text(
text = stringResourceSafe(id = R.string.main_empty_tokens_list_message),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
)
}
@Composable
internal fun NonContentItemContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(id = R.drawable.ic_empty_64),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size64),
tint = TangemTheme.colors.icon.inactive,
)
Text(
text = stringResourceSafe(id = R.string.main_empty_tokens_list_message),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
)
}
}

View file

@ -13,13 +13,18 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
@Composable
internal fun TangemPayCardMainBlock(state: TangemPayState.Card, modifier: Modifier = Modifier) {
internal fun TangemPayCardMainBlock(
state: TangemPayState.Card,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = TangemTheme.shapes.roundedCornersXMedium,
@ -72,7 +77,7 @@ internal fun TangemPayCardMainBlock(state: TangemPayState.Card, modifier: Modifi
contentAlignment = Alignment.TopEnd,
) {
Text(
text = state.balanceText.resolveReference(),
text = state.balanceText.resolveReference().orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,

View file

@ -19,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock
@Composable
internal fun TangemPayMainScreenBlock(state: TangemPayState, modifier: Modifier = Modifier) {
internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
when (state) {
is Progress -> {
Notification(
@ -38,7 +38,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, modifier: Modifier
),
)
}
is TangemPayState.Card -> TangemPayCardMainBlock(state, modifier)
is TangemPayState.Card -> TangemPayCardMainBlock(state, isBalanceHidden, modifier)
is TangemPayState.Empty -> Unit
}
}
@ -56,6 +56,7 @@ private fun ResetCardScreenPreview() {
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {},
),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(
@ -65,6 +66,7 @@ private fun ResetCardScreenPreview() {
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(
@ -75,6 +77,7 @@ private fun ResetCardScreenPreview() {
onButtonClick = {},
showProgress = true,
),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(
@ -83,6 +86,7 @@ private fun ResetCardScreenPreview() {
balanceText = TextReference.Str("$ 0.00"),
onClick = {},
),
isBalanceHidden = false,
)
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.feature.wallet.utils
import arrow.core.Either
import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.artwork.ArtworkUM
@ -47,12 +46,11 @@ class DefaultUserWalletImageFetcher @Inject constructor(
override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> =
getUserWalletUseCase.invokeFlow(walletId)
.transform { either ->
if (either.isLeft()) {
emit(UserWalletItemUM.ImageState.Loading)
} else if (either is Either.Right) {
emitAll(walletImage(either.value, size))
}
.transform {
it.fold(
ifLeft = { emit(UserWalletItemUM.ImageState.Loading) },
ifRight = { wallet -> emitAll(walletImage(wallet, size)) },
)
}
.distinctUntilChanged()

View file

@ -6,6 +6,8 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetWalletTotalBalanceUseCaseV2
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError
import com.tangem.domain.appcurrency.model.AppCurrency
@ -37,7 +39,9 @@ import kotlinx.coroutines.flow.*
@Suppress("LongParameterList")
internal class DefaultUserWalletsFetcher @AssistedInject constructor(
getWalletsUseCase: GetWalletsUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getWalletTotalBalanceUseCaseV2: GetWalletTotalBalanceUseCaseV2,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@Assisted private val onWalletClick: (UserWalletId) -> Unit,
@ -97,7 +101,11 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
// We should not load balances in auth mode
flowOf(Lce.Loading(walletIds.associateWith { TotalFiatBalance.Loading }))
} else {
getWalletTotalBalanceUseCase(walletIds).distinctUntilChanged()
if (accountsFeatureToggles.isFeatureEnabled) {
getWalletTotalBalanceUseCaseV2(userWalletIds = walletIds)
} else {
getWalletTotalBalanceUseCase(walletIds).distinctUntilChanged()
}
}
}