Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-03 21:52:57 +03:00
commit a0c9ace219
1607 changed files with 40580 additions and 16776 deletions

View file

@ -15,6 +15,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableDialogComponent
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.child.wallet.model.WalletModel
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
@ -25,6 +26,7 @@ import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -39,6 +41,7 @@ internal class WalletComponent @AssistedInject constructor(
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: WalletModel = getOrCreateModel()
@ -68,7 +71,7 @@ internal class WalletComponent @AssistedInject constructor(
askBiometryComponentFactory.create(
context = childByContext(componentContext),
params = AskBiometryComponent.Params(
bottomSheetVariant = true,
isBottomSheetVariant = true,
modelCallbacks = model.askBiometryModelCallbacks,
),
)
@ -90,6 +93,21 @@ internal class WalletComponent @AssistedInject constructor(
),
)
}
is WalletDialogConfig.YieldSupplyWarning -> {
yieldSupplyDepositedWarningComponent.create(
context = childByContext(componentContext),
params = YieldSupplyDepositedWarningComponent.Params(
cryptoCurrency = dialogConfig.cryptoCurrency,
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
tokenAction = dialogConfig.tokenAction,
modelCallback = object : YieldSupplyDepositedWarningComponent.ModelCallback {
override fun onYieldSupplyWarningAcknowledged(tokenAction: TokenAction) {
dialogConfig.onWarningAcknowledged(tokenAction)
}
},
),
)
}
}
},
)

View file

@ -0,0 +1,6 @@
package com.tangem.feature.wallet.child.wallet.model;
public enum WalletActivationBannerType {
Attention,
Warning,
}

View file

@ -10,7 +10,7 @@ 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.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
@ -18,9 +18,12 @@ 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.usecase.TangemPayIssueOrderUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.usecase.*
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
@ -38,8 +41,9 @@ import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvid
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import kotlinx.coroutines.*
@ -48,6 +52,8 @@ import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
private const val TANGEM_PAY_UPDATE_INTERVAL = 60_000L
@Suppress("LongParameterList", "LargeClass")
@Stable
@ModelScoped
@ -69,7 +75,6 @@ internal class WalletModel @Inject constructor(
private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender,
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val walletImageResolver: WalletImageResolver,
private val tokenListStore: MultiWalletTokenListStore,
private val onrampStatusFactory: OnrampStatusFactory,
@ -84,6 +89,11 @@ internal class WalletModel @Inject constructor(
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
) : Model() {
@ -96,6 +106,7 @@ internal class WalletModel @Inject constructor(
private val refreshWalletJobHolder = JobHolder()
private var needToRefreshWallet = false
private val clearNFTCacheJobHolder = JobHolder()
private val updateTangemPayJobHolder = JobHolder()
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
@ -107,16 +118,26 @@ internal class WalletModel @Inject constructor(
maybeMigrateNames()
maybeSetWalletFirstTimeUsage()
updateYieldSupplyApy()
subscribeToUserWalletsUpdates()
subscribeOnBalanceHiding()
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
subscribeToTangemPayInfo()
enableNotificationsIfNeeded()
clickIntents.initialize(innerWalletRouter, modelScope)
}
private fun updateYieldSupplyApy() {
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) {
modelScope.launch(dispatchers.default) {
yieldSupplyApyUpdateUseCase()
}
}
}
private fun maybeMigrateNames() {
modelScope.launch {
walletNameMigrationUseCase()
@ -215,19 +236,23 @@ internal class WalletModel @Inject constructor(
private fun subscribeOnPushNotificationsPermission() {
modelScope.launch {
val shouldAskPermission = shouldAskPermissionUseCase(PUSH_PERMISSION)
val afterUpdate = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
val shouldAskNotificationPermissionsViaBs = notificationsRepository.shouldAskNotificationPermissionsViaBs()
val shouldShow = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
val isBiometricsEnabled = shouldSaveUserWalletsSyncUseCase()
val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase()
val shouldShowBottomSheet = shouldAskPermission || afterUpdate
Timber.d(
"push BS afterUpdate: $afterUpdate," +
"shouldAskPermission $shouldAskPermission," +
"push BS afterUpdate: $shouldShow," +
"isBiometricsEnabled $isBiometricsEnabled," +
"isHuaweiDevice $isHuaweiDevice",
)
if (!shouldAskNotificationPermissionsViaBs) {
notificationsRepository.setShouldAskNotificationPermissionsViaBs(true)
return@launch
}
if (!isBiometricsEnabled) return@launch
if (!shouldShowBottomSheet) return@launch
if (!shouldShow) {
return@launch
}
delay(timeMillis = 1_800)
@ -313,6 +338,48 @@ internal class WalletModel @Inject constructor(
.saveIn(clearNFTCacheJobHolder)
}
private fun subscribeToTangemPayInfo() {
/**
* Update state each time a user opens/returns to wallet screen
* and every minute while user stays on the main screen
*/
screenLifecycleProvider.isBackgroundState.onEach { inBackground ->
updateTangemPayJobHolder.cancel()
if (!inBackground && tangemPayFeatureToggles.isTangemPayEnabled) {
modelScope.launch {
refreshTangemPayInfo()
while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL)
refreshTangemPayInfo()
}
}.saveIn(updateTangemPayJobHolder)
}
}.launchIn(modelScope)
}
private suspend fun refreshTangemPayInfo() {
val info = tangemPayMainScreenCustomerInfoUseCase()
if (info != null) {
stateHolder.update(
transformer = TangemPayInitialStateTransformer(
value = info,
onClickIssue = ::issueOrder,
onClickKyc = innerWalletRouter::openTangemPayOnboarding,
openDetails = innerWalletRouter::openTangemPayDetails,
),
)
}
}
private fun issueOrder() {
modelScope.launch {
stateHolder.update(TangemPayIssueProgressStateTransformer())
tangemPayIssueOrderUseCase().onLeft {
stateHolder.update(TangemPayIssueAvailableStateTransformer(onClickIssue = ::issueOrder))
}
}
}
private fun needToRefreshTimer() {
modelScope.launch {
delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS)
@ -366,7 +433,7 @@ internal class WalletModel @Inject constructor(
is WalletsUpdateActionResolver.Action.RenameWallets -> {
stateHolder.update(transformer = RenameWalletsTransformer(renamedWallets = action.renamedWallets))
}
is WalletsUpdateActionResolver.Action.ReloadWarningsForWallets -> {
is WalletsUpdateActionResolver.Action.ReloadWallets -> {
reloadWarnings(action)
}
WalletsUpdateActionResolver.Action.EmptyWallets -> {
@ -378,7 +445,7 @@ internal class WalletModel @Inject constructor(
}
}
private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWarningsForWallets) {
private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWallets) {
action.wallets.forEach {
walletScreenContentLoader.load(
userWallet = it,

View file

@ -64,6 +64,9 @@ internal class WalletsUpdateActionResolver @Inject constructor(
selectedWallet: UserWallet,
): Action {
return when {
isHotWalletUpgraded(state, wallets) -> {
getHotWalletsUpgradedAction(state, wallets)
}
isWalletsCountChanged(state, wallets) -> {
getChangeWalletsListAction(state, wallets, selectedWallet)
}
@ -91,6 +94,19 @@ internal class WalletsUpdateActionResolver @Inject constructor(
return walletsToUpdate.isNotEmpty()
}
private fun isHotWalletUpgraded(state: WalletScreenState, wallets: List<UserWallet>): Boolean {
val previousWallet = state
.wallets
.getOrNull(state.selectedWalletIndex)
return when (previousWallet) {
is WalletState.MultiCurrency -> {
val wallet = wallets.firstOrNull { it.walletId == previousWallet.walletCardState.id }
previousWallet.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold
}
else -> false
}
}
private fun isWalletsCountChanged(state: WalletScreenState, wallets: List<UserWallet>): Boolean {
val prevWalletsSize = state.wallets.size
val walletsSize = wallets.size
@ -160,14 +176,22 @@ internal class WalletsUpdateActionResolver @Inject constructor(
private fun getHotWalletsBackedUpAction(
state: WalletScreenState,
wallets: List<UserWallet>,
): Action.ReloadWarningsForWallets {
): Action.ReloadWallets {
val incompleteActivationWalletIds = state.incompleteActivationWalletIds()
val walletsToUpdate = wallets.filter {
it is UserWallet.Hot && it.backedUp == incompleteActivationWalletIds.contains(it.walletId)
}
return Action.ReloadWarningsForWallets(walletsToUpdate)
return Action.ReloadWallets(walletsToUpdate)
}
private fun getHotWalletsUpgradedAction(
state: WalletScreenState,
wallets: List<UserWallet>,
): Action.ReloadWallets {
val walletsToUpdate = wallets.filter { it.walletId == state.getPrevSelectedWallet().id }
return Action.ReloadWallets(walletsToUpdate)
}
private fun getRenameWalletsAction(state: WalletScreenState, wallets: List<UserWallet>): Action.RenameWallets {
@ -358,7 +382,7 @@ internal class WalletsUpdateActionResolver @Inject constructor(
}
}
data class ReloadWarningsForWallets(
data class ReloadWallets(
val wallets: List<UserWallet>,
) : Action()

View file

@ -4,18 +4,19 @@ import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -36,15 +37,17 @@ internal interface WalletContentClickIntents {
fun onDetailsClick()
fun onManageTokensClick()
fun onOrganizeTokensClick()
fun onDismissMarketsOnboarding()
fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus)
fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus)
fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onAccountExpandClick(account: Account)
fun onAccountCollapseClick(account: Account)
fun onTransactionClick(txHash: String)
@ -74,10 +77,10 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val reduxStateHolder: ReduxStateHolder,
private val walletEventSender: WalletEventSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val accountDependencies: AccountDependencies,
) : BaseWalletClickIntents(), WalletContentClickIntents {
override fun onDetailsClick() {
@ -114,11 +117,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
}
override fun onManageTokensClick() {
reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess)
router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
override fun onOrganizeTokensClick() {
router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
@ -130,13 +128,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
}
override fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus) {
router.openTokenDetails(stateHolder.getSelectedWalletId(), currencyStatus)
override fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
router.openTokenDetails(userWalletId, currencyStatus)
}
override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
modelScope.launch(dispatchers.main) {
val userWalletId = stateHolder.getSelectedWalletId()
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
Timber.e(
"""
@ -157,6 +154,16 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
}
override fun onAccountExpandClick(account: Account) {
val userWalletId = stateHolder.getSelectedWalletId()
accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId)
}
override fun onAccountCollapseClick(account: Account) {
val userWalletId = stateHolder.getSelectedWalletId()
accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId)
}
private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) {
stateHolder.showBottomSheet(
ActionsBottomSheetConfig(

View file

@ -22,6 +22,9 @@ import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.resourceReference
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.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.core.lce.Lce
@ -29,7 +32,6 @@ import com.tangem.domain.core.utils.lceError
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -42,10 +44,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
import com.tangem.domain.tokens.RemoveCurrencyUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
@ -54,7 +53,8 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -68,21 +68,31 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensLis
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
interface WalletCurrencyActionsClickIntents {
fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason)
fun onSendClick(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
)
fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason)
fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason)
fun onBuyClick(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
)
fun onSwapClick(
cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -90,17 +100,21 @@ interface WalletCurrencyActionsClickIntents {
unavailabilityReason: ScenarioUnavailabilityReason,
)
fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent? = null)
fun onReceiveClick(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
event: AnalyticsEvent? = null,
)
fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?)
fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?)
fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference?
fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onExploreClick()
@ -122,8 +136,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
@ -136,16 +148,23 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val appRouter: AppRouter,
private val rampStateManager: RampStateManager,
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getEnsNameUseCase: GetEnsNameUseCase,
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
private val receiveAddressesFactory: ReceiveAddressesFactory,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
override fun onSendClick(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
) {
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonSend(
token = cryptoCurrencyStatus.currency.symbol,
@ -156,18 +175,31 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
if (handleUnavailabilityReason(unavailabilityReason)) return
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId))
val route = AppRoute.Send(
currency = cryptoCurrencyStatus.currency,
userWalletId = userWallet.walletId,
)
appRouter.push(route)
modelScope.launch(dispatchers.main) {
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
stateHolder.hideBottomSheet()
router.openYieldSupplyBottomSheet(
cryptoCurrency = cryptoCurrencyStatus.currency,
tokenAction = TokenAction.Send,
onWarningAcknowledged = { tokenAction ->
modelScope.launch {
saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name)
stateHolder.hideBottomSheet()
navigateToSend(cryptoCurrencyStatus, userWalletId)
}
},
)
} else {
navigateToSend(cryptoCurrencyStatus, userWalletId)
}
}
}
override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent?) {
val userWalletId = stateHolder.getSelectedWalletId()
override fun onReceiveClick(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
event: AnalyticsEvent?,
) {
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive(
token = cryptoCurrencyStatus.currency.symbol,
@ -178,24 +210,24 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
event?.let { analyticsEventHandler.send(it) }
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
stateHolder.hideBottomSheet()
modelScope.launch {
configureReceiveAddresses(cryptoCurrencyStatus = cryptoCurrencyStatus)?.let {
router.openTokenReceiveBottomSheet(it)
}
modelScope.launch(dispatchers.main) {
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
stateHolder.hideBottomSheet()
router.openYieldSupplyBottomSheet(
cryptoCurrency = cryptoCurrencyStatus.currency,
tokenAction = TokenAction.Receive,
onWarningAcknowledged = { tokenAction ->
modelScope.launch {
saveViewedTokenReceiveWarningUseCase(cryptoCurrencyStatus.currency.name)
saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name)
stateHolder.hideBottomSheet()
navigateToReceive(cryptoCurrencyStatus)
}
},
)
} else {
navigateToReceive(cryptoCurrencyStatus)
}
} else {
analyticsEventHandler.send(
event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrencyStatus.currency.symbol),
)
stateHolder.showBottomSheet(
createReceiveBottomSheetContent(
currency = cryptoCurrencyStatus.currency,
addresses = cryptoCurrencyStatus.value.networkAddress ?: return,
),
userWalletId,
)
}
}
@ -240,7 +272,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
}
override fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
analyticsEventHandler.send(
event = TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
token = cryptoCurrencyStatus.currency.symbol,
@ -251,10 +283,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
modelScope.launch(dispatchers.main) {
walletManagersFacade.getDefaultAddress(
userWalletId = stateHolder.getSelectedWalletId(),
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
)?.let { address ->
stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()))
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
clipboardManager.setText(text = address, isSensitive = true)
walletEventSender.send(event = WalletEvent.CopyAddress)
@ -262,7 +294,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
override fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol),
)
@ -270,7 +302,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
modelScope.launch(dispatchers.main) {
walletEventSender.send(
event = WalletEvent.ShowAlert(
state = getHideTokeAlertConfig(stateHolder.getSelectedWalletId(), cryptoCurrencyStatus),
state = getHideTokeAlertConfig(userWalletId, cryptoCurrencyStatus),
),
)
}
@ -281,7 +313,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): WalletAlertState.DefaultAlert {
val currency = cryptoCurrencyStatus.currency
return if (currency is CryptoCurrency.Coin && !isCryptoCurrencyCoinCouldHide(userWalletId, currency)) {
val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin &&
!isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency)
return if (isCryptoCurrencyCoinCouldHide) {
WalletAlertState.DefaultAlert(
title = resourceReference(
id = R.string.token_details_unable_hide_alert_title,
@ -306,16 +340,30 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)),
),
message = resourceReference(R.string.token_details_hide_alert_message),
onConfirmClick = { onPerformHideToken(cryptoCurrencyStatus) },
onConfirmClick = { onPerformHideToken(userWalletId, cryptoCurrencyStatus) },
)
}
}
override fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val userWalletId = stateHolder.getSelectedWalletId()
override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
modelScope.launch(dispatchers.io) {
removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency)
if (accountsFeatureToggles.isFeatureEnabled) {
val accountId = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = userWalletId,
currency = cryptoCurrencyStatus.currency,
)
.map { it.account.accountId }
.getOrNull()
if (accountId == null) {
Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}")
return@launch
}
saveCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency)
} else {
removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency)
}
.fold(
ifLeft = {
walletEventSender.send(
@ -356,11 +404,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
override fun onBuyClick(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
unavailabilityReason: ScenarioUnavailabilityReason,
) {
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy(
token = cryptoCurrencyStatus.currency.symbol,
@ -373,7 +420,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
appRouter.push(
AppRoute.Onramp(
userWalletId = userWallet.walletId,
userWalletId = userWalletId,
currency = cryptoCurrencyStatus.currency,
source = OnrampSource.TOKEN_LONG_TAP,
),
@ -395,13 +442,24 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
if (handleUnavailabilityReason(unavailabilityReason)) return
appRouter.push(
AppRoute.Swap(
currencyFrom = cryptoCurrencyStatus.currency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.LongTap.value,
),
)
modelScope.launch(dispatchers.main) {
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
stateHolder.hideBottomSheet()
router.openYieldSupplyBottomSheet(
cryptoCurrency = cryptoCurrencyStatus.currency,
tokenAction = TokenAction.Swap,
onWarningAcknowledged = { tokenAction ->
modelScope.launch {
saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name)
stateHolder.hideBottomSheet()
navigateToSwap(cryptoCurrencyStatus, userWalletId)
}
},
)
} else {
navigateToSwap(cryptoCurrencyStatus, userWalletId)
}
}
}
override fun onExploreClick() {
@ -428,7 +486,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
AppRoute.MarketsTokenDetails(
token = tokenMarketParams,
appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
showPortfolio = false,
shouldShowPortfolio = false,
analyticsParams = AppRoute.MarketsTokenDetails.AnalyticsParams(
blockchain = cryptoCurrencyStatus.currency.network.name,
source = "Main",
@ -440,12 +498,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) {
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId))
override fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
modelScope.launch {
val userWalletId = stateHolder.getSelectedWalletId()
val cryptoCurrency = cryptoCurrencyStatus.currency
appRouter.push(
@ -468,15 +524,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {
val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return
val tokenListState = selectedWallet.tokensListState as? WalletTokensListState.ContentState.Content ?: return
val tokenListState = selectedWallet.tokensListState
if (tokenListState.items.count { it is TokensListItemUM.Token } < 2) {
handleError(
alertState = WalletAlertState.InsufficientTokensCountForSwapping,
eventCreator = MainScreenAnalyticsEvent::ButtonSwap,
when (tokenListState) {
is WalletTokensListState.ContentState.Content -> checkSwapCryptoAvailability(
tokenCount = tokenListState.items.count { it is TokensListItemUM.Token },
)
return
is WalletTokensListState.ContentState.PortfolioContent -> checkSwapCryptoAvailability(
tokenCount = tokenListState.items.sumOf { it.tokens.count { it is TokensListItemUM.Token } },
)
WalletTokensListState.ContentState.Loading,
WalletTokensListState.ContentState.Locked,
WalletTokensListState.Empty,
-> return
}
modelScope.launch {
@ -669,44 +729,69 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? {
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return null
val userWalletId = stateHolder.getSelectedWalletId()
val ensName = getEnsNameUseCase.invoke(
return receiveAddressesFactory.create(
status = cryptoCurrencyStatus,
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
address = networkAddress.defaultAddress.value,
)
val receiveAddresses = buildList {
ensName?.let { ens ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
value = ens,
),
)
}
networkAddress.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
},
value = address.value,
),
)
}
}
return TokenReceiveConfig(
shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(),
cryptoCurrency = cryptoCurrencyStatus.currency,
userWalletId = userWalletId,
showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network
.TransactionExtrasType.NONE,
receiveAddress = receiveAddresses,
)
}
private suspend fun needShowYieldSupplyWarning(cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled &&
needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
}
private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
val route = AppRoute.Send(
currency = cryptoCurrencyStatus.currency,
userWalletId = userWalletId,
)
appRouter.push(route)
}
private fun navigateToSwap(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) {
appRouter.push(
AppRoute.Swap(
currencyFrom = cryptoCurrencyStatus.currency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.LongTap.value,
),
)
}
private fun navigateToReceive(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val userWalletId = stateHolder.getSelectedWalletId()
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
stateHolder.hideBottomSheet()
modelScope.launch {
configureReceiveAddresses(cryptoCurrencyStatus = cryptoCurrencyStatus)?.let {
router.openTokenReceiveBottomSheet(it)
}
}
} else {
analyticsEventHandler.send(
event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrencyStatus.currency.symbol),
)
stateHolder.showBottomSheet(
createReceiveBottomSheetContent(
currency = cryptoCurrencyStatus.currency,
addresses = cryptoCurrencyStatus.value.networkAddress ?: return,
),
userWalletId,
)
}
}
private fun checkSwapCryptoAvailability(tokenCount: Int) {
if (tokenCount < 2) {
handleError(
alertState = WalletAlertState.InsufficientTokensCountForSwapping,
eventCreator = MainScreenAnalyticsEvent::ButtonSwap,
)
return
}
}
}

View file

@ -2,17 +2,19 @@ package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse
import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.Onramp
import com.tangem.common.routing.AppRoute.ReferralProgram
import com.tangem.common.routing.AppRoute.*
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.notifications.NotificationId
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
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.resourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.card.SetCardWasScannedUseCase
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -21,6 +23,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.notifications.SetShouldShowNotificationUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
@ -34,11 +38,10 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UnlockWalletsError
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
@ -50,6 +53,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -58,6 +62,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("TooManyFunctions")
internal interface WalletWarningsClickIntents {
fun onAddBackupCardClick()
@ -96,10 +101,14 @@ internal interface WalletWarningsClickIntents {
fun onSeedPhraseSecondNotificationReject()
fun onFinishWalletActivationClick()
fun onAllowPermissions()
fun onDenyPermissions()
fun onFinishWalletActivationClick(type: WalletActivationBannerType)
}
@Suppress("LargeClass", "LongParameterList")
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ModelScoped
internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
@ -125,8 +134,39 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val appRouter: AppRouter,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase,
private val notificationsRepository: NotificationsRepository,
private val messageSender: UiMessageSender,
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
) : 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)
@ -324,7 +364,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
source = OnrampSource.SEPA_BANNER,
launchSepa = true,
shouldLaunchSepa = true,
),
)
}
@ -426,9 +466,56 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onFinishWalletActivationClick() {
val userWallet = getSelectedUserWallet() ?: return
appRouter.push(AppRoute.WalletActivation(userWallet.walletId))
override fun onFinishWalletActivationClick(type: WalletActivationBannerType) {
when (type) {
WalletActivationBannerType.Attention -> {
val userWallet = getSelectedUserWallet() ?: return
appRouter.push(WalletActivation(userWallet.walletId))
}
WalletActivationBannerType.Warning -> {
messageSender.send(finalizeWalletSetupAlertBS)
}
}
}
override fun onAllowPermissions() {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonAllowPush)
walletEventSender.send(
event = WalletEvent.RequestPushPermissions(
onAllow = {
modelScope.launch {
updateSubscribeOnPushPermissions(true)
analyticsEventHandler.send(
PushNotificationAnalyticEvents.PermissionStatus(isAllowed = true),
)
enableNotificationsIfNeeded()
}
},
onDeny = {
modelScope.launch {
updateSubscribeOnPushPermissions(false)
analyticsEventHandler.send(
PushNotificationAnalyticEvents.PermissionStatus(isAllowed = false),
)
}
},
),
)
}
override fun onDenyPermissions() {
modelScope.launch {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonLaterPush)
updateSubscribeOnPushPermissions(false)
}
}
private suspend fun updateSubscribeOnPushPermissions(shouldAllow: Boolean) {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(shouldAllow)
setShouldShowNotificationUseCase(
key = NotificationId.EnablePushesReminderNotification.key,
value = false,
)
}
private suspend fun fetchCryptoCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
@ -482,6 +569,20 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
private suspend fun enableNotificationsIfNeeded() {
val alreadyEnabledWallets = notificationsRepository.getWalletAutomaticallyEnabledList().map {
UserWalletId(it)
}
val walletsListWhichShouldBeEnabled = getWalletsListForEnablingUseCase(alreadyEnabledWallets)
walletsListWhichShouldBeEnabled.forEach { userWalletId ->
setNotificationsEnabledUseCase(userWalletId, true).onRight {
notificationsRepository.setNotificationsWasEnabledAutomatically(userWalletId.stringValue)
}.onLeft {
Timber.e(it)
}
}
}
private companion object {
const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" +
"&utm_medium=banner" +

View file

@ -0,0 +1,15 @@
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.usecase.IsAccountsModeEnabledUseCase
import javax.inject.Inject
@ModelScoped
internal class AccountDependencies @Inject constructor(
val accountsFeatureToggles: AccountsFeatureToggles,
val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
val expandedAccountsHolder: ExpandedAccountsHolder,
val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
)

View file

@ -0,0 +1,54 @@
package com.tangem.feature.wallet.presentation.account
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.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class ExpandedAccountsHolder @Inject constructor(
private val singleAccountListSupplier: SingleAccountListSupplier,
) {
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
val defaultExpanded = when {
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
map.plus(userWallet.walletId to expandedSet)
}
}.launchIn(this)
expandedAccounts
.mapNotNull { map -> map[userWallet.walletId] }
.onEach { expanded -> channel.send(expanded) }
.collect()
}
fun expandAccount(userWalletId: UserWalletId, accountId: AccountId) = expandedAccounts.update { map ->
val expandedSet = map[userWalletId]?.plus(accountId) ?: return@update map
map.plus(userWalletId to expandedSet)
}
fun collapseAccount(userWalletId: UserWalletId, accountId: AccountId) = expandedAccounts.update { map ->
val expandedSet = map[userWalletId]?.minus(accountId) ?: return@update map
map.plus(userWalletId to expandedSet)
}
private fun walletAccounts(userWallet: UserWallet): Flow<AccountList> =
singleAccountListSupplier(SingleAccountListProducer.Params(userWallet.walletId))
}

View file

@ -79,7 +79,7 @@ internal object WalletPreviewData {
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
showCustomBadge = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"),

View file

@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarCon
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal object WalletScreenPreviewData {
private val tokenItemState = TokenItemState.Content(
@ -86,15 +87,17 @@ internal object WalletScreenPreviewData {
private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent(
items = persistentListOf(
TokensListItemUM.Portfolio(
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>(),
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
isExpanded = false,
state = AccountItemPreviewData.accountItem
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem
.copy(iconState = AccountItemPreviewData.accountLetterIcon),
),
TokensListItemUM.Portfolio(
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>(),
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
isExpanded = true,
state = AccountItemPreviewData.accountItem,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem,
),
),
organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig(
@ -159,6 +162,7 @@ internal object WalletScreenPreviewData {
isFlickering = false,
onItemClick = { },
),
type = WalletState.MultiCurrency.WalletType.Cold,
)
}

View file

@ -123,6 +123,14 @@ private fun TokenList(
end = TangemTheme.dimens.spacing16,
)
// workaround to force recomposition of list items when the list is updated
// 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) {
forceRecompose.intValue++
}
LazyColumn(
modifier = Modifier
.align(Alignment.TopCenter)
@ -143,13 +151,15 @@ private fun TokenList(
}
}
DraggableItem(
index = index,
item = item,
reorderableState = reorderableListState,
onDragStart = onDragStart,
isBalanceHidden = isBalanceHidden,
)
key(forceRecompose.intValue) {
DraggableItem(
index = index,
item = item,
reorderableState = reorderableListState,
onDragStart = onDragStart,
isBalanceHidden = isBalanceHidden,
)
}
}
}
@ -260,9 +270,9 @@ private fun TopBar(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.organize_tokens_sort_by_balance),
iconResId = R.drawable.ic_sort_24,
enabled = config.isEnabled,
isEnabled = config.isEnabled,
onClick = config.onSortClick,
dimContent = config.isSortedByBalance,
shouldDimContent = config.isSortedByBalance,
),
color = TangemTheme.colors.background.primary,
)
@ -279,7 +289,7 @@ private fun TopBar(
},
),
iconResId = R.drawable.ic_group_24,
enabled = config.isEnabled,
isEnabled = config.isEnabled,
onClick = config.onGroupClick,
),
color = TangemTheme.colors.background.primary,

View file

@ -3,17 +3,19 @@ package com.tangem.feature.wallet.presentation.router
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import kotlinx.coroutines.channels.BufferOverflow
@ -84,10 +86,6 @@ internal class DefaultWalletRouter @Inject constructor(
return router.stack.lastOrNull() is AppRoute.Wallet
}
override fun openManageTokensScreen(userWalletId: UserWalletId) {
router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId))
}
override fun openScanFailedDialog(onTryAgain: () -> Unit) {
reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain))
}
@ -106,4 +104,26 @@ internal class DefaultWalletRouter @Inject constructor(
configuration = WalletDialogConfig.TokenReceive(tokenReceiveConfig),
)
}
override fun openTangemPayOnboarding() {
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding))
}
override fun openTangemPayDetails(config: TangemPayDetailsConfig) {
router.push(AppRoute.TangemPayDetails(config))
}
override fun openYieldSupplyBottomSheet(
cryptoCurrency: CryptoCurrency,
tokenAction: TokenAction,
onWarningAcknowledged: (TokenAction) -> Unit,
) {
dialogNavigation.activate(
configuration = WalletDialogConfig.YieldSupplyWarning(
cryptoCurrency = cryptoCurrency,
tokenAction = tokenAction,
onWarningAcknowledged = onWarningAcknowledged,
),
)
}
}

View file

@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.router
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import kotlinx.coroutines.flow.SharedFlow
@ -47,9 +50,6 @@ internal interface InnerWalletRouter {
/** Is wallet last screen */
fun isWalletLastScreen(): Boolean
/** Open manage tokens screen */
fun openManageTokensScreen(userWalletId: UserWalletId)
/** Open scan failed dialog */
fun openScanFailedDialog(onTryAgain: () -> Unit)
@ -57,4 +57,15 @@ internal interface InnerWalletRouter {
fun openNFT(userWallet: UserWallet)
fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig)
fun openTangemPayOnboarding()
fun openTangemPayDetails(config: TangemPayDetailsConfig)
/** Open BS abput yield supply active and all money deposited in AAVE */
fun openYieldSupplyBottomSheet(
cryptoCurrency: CryptoCurrency,
tokenAction: TokenAction,
onWarningAcknowledged: (TokenAction) -> Unit,
)
}

View file

@ -73,6 +73,13 @@ sealed class WalletScreenAnalyticsEvent {
data object ScreenOpened : MainScreen(event = "Screen opened")
class WalletSelected(val isImported: Boolean) : MainScreen(
event = "Wallet Selected",
params = mapOf(
"Wallet Type" to if (isImported) "Seed Phrase" else "Seedless",
),
)
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",
params = mapOf("State" to state.value),
@ -144,4 +151,14 @@ sealed class WalletScreenAnalyticsEvent {
data object ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss")
//endregion
}
sealed class PushBannerPromo(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Promo", event = event, params = params) {
data object PushBanner : PushBannerPromo(event = "Push Banner")
data object ButtonAllowPush : PushBannerPromo(event = "Button - Allow Push")
data object ButtonLaterPush : PushBannerPromo(event = "Button - Later Push")
}
}

View file

@ -29,6 +29,14 @@ internal class SelectedWalletAnalyticsSender @Inject constructor(
* */
private fun getEvent(userWallet: UserWallet): AnalyticsEvent? = when {
userWallet.isLocked -> WalletScreenAnalyticsEvent.MainScreen.WalletUnlock
else -> null
else -> WalletScreenAnalyticsEvent.MainScreen.WalletSelected(userWallet.isImported())
}
private fun UserWallet.isImported(): Boolean {
return when (this) {
is UserWallet.Cold -> isImported
is UserWallet.Hot -> true
}
}
}

View file

@ -14,7 +14,6 @@ import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
@ -38,33 +37,41 @@ internal class TokenListAnalyticsSender @Inject constructor(
private val mutex = Mutex()
private val loadingTraces = mutableMapOf<UserWalletId, Trace>()
suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) {
suspend fun send(
displayedUiState: WalletState?,
userWallet: UserWallet,
totalFiatBalance: TotalFiatBalance,
flattenCurrencies: List<CryptoCurrencyStatus>,
) {
if (screenLifecycleProvider.isBackgroundState.value) return
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) {
startLoadingTraceIfNeeded(userWallet.walletId, tokenList)
if (totalFiatBalance is TotalFiatBalance.Loading) {
startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies)
return
}
if (isTerminalState(tokenList.totalFiatBalance)) {
stopLoadingTraceIfNeeded(userWallet.walletId, tokenList.totalFiatBalance)
if (isTerminalState(totalFiatBalance)) {
stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance)
}
val currenciesStatuses = tokenList.flattenCurrencies()
val currenciesStatuses = flattenCurrencies
sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses)
sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses)
sendBalanceLoadedEventIfNeeded(totalFiatBalance, currenciesStatuses)
sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, currenciesStatuses)
sendUnreachableNetworksEventIfNeeded(currenciesStatuses)
sendTokenBalancesIfNeeded(currenciesStatuses)
}
private suspend fun startLoadingTraceIfNeeded(userWalletId: UserWalletId, tokenList: TokenList) {
private suspend fun startLoadingTraceIfNeeded(
userWalletId: UserWalletId,
flattenCurrencies: List<CryptoCurrencyStatus>,
) {
mutex.withLock {
if (!loadingTraces.containsKey(userWalletId)) {
val trace = FirebasePerformance.getInstance().newTrace(BALANCE_LOADED_TRACE_NAME)
trace.start()
trace.putAttribute(TOKENS_COUNT, tokenList.flattenCurrencies().size.toString())
trace.putAttribute(TOKENS_COUNT, flattenCurrencies.size.toString())
loadingTraces[userWalletId] = trace
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -67,9 +68,11 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.UsedOutdatedData,
is WalletNotification.UnlockVisaAccess,
is WalletNotification.FinishWalletActivation,
is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event
-> null
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond
is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner
}
}
}

View file

@ -3,21 +3,24 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.getOrElse
import com.tangem.common.ui.notifications.NotificationId
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.onramp.GetOnrampCountryUseCase
import com.tangem.domain.onramp.OnrampSepaAvailableUseCase
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
@ -28,19 +31,24 @@ 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.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.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.coroutines.combine6
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class GetMultiWalletWarningsFactory @Inject constructor(
private val tokenListStore: MultiWalletTokenListStore,
@ -53,56 +61,103 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val onrampSepaAvailableUseCase: OnrampSepaAvailableUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
private val notificationsRepository: NotificationsRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountDependencies: AccountDependencies,
) {
@Suppress("UNCHECKED_CAST", "MagicNumber")
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
return combine6(
flow1 = tokenListStore.getOrThrow(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
flow5 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.VisaPresale),
flow6 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus, shouldShowVisaPresalePromo, shouldShowSepaBanner ->
buildList {
addUsedOutdatedDataNotification(maybeTokenList)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addFinishWalletActivationNotification(userWallet, maybeTokenList, clickIntents)
addVisaPresalePromoNotification(clickIntents, shouldShowVisaPresalePromo)
addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner)
addInformationalNotifications(userWallet, cardTypesResolver, maybeTokenList, clickIntents)
addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents)
val hasCriticalOrWarning = any { notification ->
notification is WalletNotification.Critical || notification is WalletNotification.Warning
}
if (!hasCriticalOrWarning) {
addRateTheAppNotification(isReadyToShowRating, clickIntents)
}
}.toImmutableList()
val accountStatusList by lazy {
val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
accountDependencies.singleAccountStatusListSupplier(params)
.map { it.totalFiatBalance to it.flattenCurrencies() }
.map { Lce.Content(it) }
}
fun tokenListFlow(): LceFlow<TokenListError, Pair<TotalFiatBalance, List<CryptoCurrencyStatus>>> {
return if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) {
accountStatusList
} else {
runCatching { tokenListStore.getOrThrow(userWallet.walletId) }
.map { result -> result.map { lce -> lce.map { it.totalFiatBalance to it.flattenCurrencies() } } }
.getOrNull()
// in case of runtime change ft in tester menu
?: accountStatusList
}
}
// val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
// val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params)
return combine(
// todo account just use it, after delete accountsFeatureToggles
// accountStatusListFlow,
isReadyToShowRateAppUseCase(),
isNeedToBackupUseCase(userWallet.walletId),
seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.VisaPresale),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa),
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key),
) { array -> array }
.combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) }
.map { array ->
val lceTokens = array[0] as Lce<TokenListError, Pair<TotalFiatBalance, List<CryptoCurrencyStatus>>>
val totalFiatBalance = lceTokens.map { it.first }
val flattenCurrencies = lceTokens.map { it.second }
val isReadyToShowRating = array[1] as Boolean
val isNeedToBackup = array[2] as Boolean
val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus
val shouldShowVisaPromo = array[4] as Boolean
val shouldShowSepaBanner = array[5] as Boolean
val shouldShowEnablePushesReminderNotification = array[6] as Boolean
buildList {
addUsedOutdatedDataNotification(totalFiatBalance)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents)
addVisaPresalePromoNotification(clickIntents, shouldShowVisaPromo)
addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner)
addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)
addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents)
addPushReminderNotification(
clickIntents = clickIntents,
shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification &&
!notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
)
addYieldSupplyNotifications(flattenCurrencies)
val hasCriticalOrWarning = any { notification ->
notification is WalletNotification.Critical || notification is WalletNotification.Warning
}
if (!hasCriticalOrWarning) {
addRateTheAppNotification(isReadyToShowRating, clickIntents)
}
}.toImmutableList()
}
}
private fun MutableList<WalletNotification>.addUsedOutdatedDataNotification(
maybeTokenList: Lce<TokenListError, TokenList>,
totalFiatBalance: Lce<TokenListError, TotalFiatBalance>,
) {
addIf(
element = WalletNotification.UsedOutdatedData,
condition = maybeTokenList.fold(
condition = totalFiatBalance.fold(
ifLoading = {
(it?.totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE
(it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE
},
ifContent = {
(it.totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE
(it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE
},
ifError = { false },
),
@ -180,7 +235,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addInformationalNotifications(
userWallet: UserWallet,
cardTypesResolver: CardTypesResolver?,
maybeTokenList: Lce<TokenListError, TokenList>,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
) {
addIf(
@ -188,15 +243,15 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
condition = cardTypesResolver != null && isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
)
addMissingAddressesNotification(userWallet, maybeTokenList, clickIntents)
addMissingAddressesNotification(userWallet, flattenCurrencies, clickIntents)
}
private fun MutableList<WalletNotification>.addMissingAddressesNotification(
userWallet: UserWallet,
maybeTokenList: Lce<TokenListError, TokenList>,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
) {
val currencies = maybeTokenList.getMissingAddressCurrencies()
val currencies = flattenCurrencies.getMissingAddressCurrencies()
.ifEmpty { return }
addIf(
@ -211,11 +266,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun Lce<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
val tokenList = getOrNull(isPartialContentAccepted = true) ?: return emptyList()
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.getMissingAddressCurrencies(): List<CryptoCurrency> {
val flattenCurrencies = getOrNull(isPartialContentAccepted = true) ?: return emptyList()
return tokenList
.flattenCurrencies()
return flattenCurrencies
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency)
}
@ -265,9 +319,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
) {
addIf(
element = WalletNotification.Warning.YeildSupplyApprove,
condition = flattenCurrencies.hasTokensWithActivatedSupplyWithoutApprove(),
)
}
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver?,
tokenList: Lce<TokenListError, TokenList>,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
isNeedToBackup: Boolean,
clickIntents: WalletClickIntents,
) {
@ -285,14 +348,35 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addIf(
element = WalletNotification.Warning.SomeNetworksUnreachable,
condition = tokenList.hasUnreachableNetworks(),
condition = flattenCurrencies.hasUnreachableNetworks(),
)
}
private fun Lce<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false
private fun MutableList<WalletNotification>.addPushReminderNotification(
clickIntents: WalletClickIntents,
shouldShowPushReminderBanner: Boolean,
) {
addIf(
element = WalletNotification.PushNotifications(
onCloseClick = clickIntents::onDenyPermissions,
onEnabledClick = clickIntents::onAllowPermissions,
),
condition = shouldShowPushReminderBanner,
)
}
return tokenList.flattenCurrencies().any { it.value is CryptoCurrencyStatus.Unreachable }
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.hasUnreachableNetworks(): Boolean {
val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false
return flattenCurrencies.any { it.value is CryptoCurrencyStatus.Unreachable }
}
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.hasTokensWithActivatedSupplyWithoutApprove(): Boolean {
val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false
val yieldSupplyEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled
return yieldSupplyEnabled && flattenCurrencies.any {
it.value.yieldSupplyStatus?.isAllowedToSpend == false
}
}
private fun MutableList<WalletNotification>.addRateTheAppNotification(
@ -309,44 +393,45 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun TotalFiatBalance?.getFinishWalletActivationType(): WalletActivationBannerType {
return if ((this as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) {
WalletActivationBannerType.Warning
} else {
WalletActivationBannerType.Attention
}
}
private fun MutableList<WalletNotification>.addFinishWalletActivationNotification(
userWallet: UserWallet,
maybeTokenList: Lce<TokenListError, TokenList>,
totalFiatBalance: Lce<TokenListError, TotalFiatBalance>,
clickIntents: WalletClickIntents,
) {
if (userWallet !is UserWallet.Hot) return
val shouldShowFinishActivation = !userWallet.backedUp
val iconTint = maybeTokenList.fold(
ifLoading = {
if ((it?.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) {
IconTint.Warning
} else {
IconTint.Attention
}
},
ifContent = {
if ((it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) {
IconTint.Warning
} else {
IconTint.Attention
}
},
ifError = { IconTint.Attention },
val type = totalFiatBalance.fold(
ifLoading = { it.getFinishWalletActivationType() },
ifContent = { it.getFinishWalletActivationType() },
ifError = { WalletActivationBannerType.Attention },
)
val tint = when (type) {
WalletActivationBannerType.Attention -> IconTint.Attention
WalletActivationBannerType.Warning -> IconTint.Warning
}
addIf(
element = WalletNotification.FinishWalletActivation(
iconTint = iconTint,
buttonsState = when (iconTint) {
IconTint.Warning -> ButtonsState.PrimaryButtonConfig(
iconTint = tint,
buttonsState = when (type) {
WalletActivationBannerType.Warning -> ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = clickIntents::onFinishWalletActivationClick,
onClick = { clickIntents.onFinishWalletActivationClick(type) },
)
else -> ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = clickIntents::onFinishWalletActivationClick,
onClick = { clickIntents.onFinishWalletActivationClick(type) },
)
},
),
@ -354,10 +439,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) add(element = element)
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
}

View file

@ -15,6 +15,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
@ -204,14 +205,15 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) {
add(element = element)
addIf(condition) {
if (element is WalletNotification.Critical ||
element is WalletNotification.Warning ||
element is WalletNotification.NoteMigration
) {
readyForRateAppNotification = false
}
element
}
}

View file

@ -351,6 +351,12 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF85", "AF86", "AF87", "AF990013", "AF990012", "AF990011"),
),
WinterSakura(
cards2ResId = R.drawable.ill_winter_sakura_card2_120_106,
cards3ResId = R.drawable.ill_winter_sakura_card3_120_106,
batchIds = setOf("AF990053", "AF990054", "AF990055"),
),
LockedMoney(
cards2ResId = R.drawable.ill_locked_money_card2_120_106,
cards3ResId = R.drawable.ill_locked_money_card3_120_106,

View file

@ -99,7 +99,10 @@ internal object WalletAdditionalInfoFactory {
TextReference.EMPTY
}
return WalletAdditionalInfo(hideable = false, content = content)
return WalletAdditionalInfo(
hideable = false,
content = content,
)
}
private fun getBackupInfoTextReference(count: Int): TextReference {
@ -112,12 +115,18 @@ internal object WalletAdditionalInfoFactory {
private fun UserWallet.Cold.resolveSingleCurrencyInfo(currencyAmount: BigDecimal?): WalletAdditionalInfo {
return if (isLocked) {
WalletAdditionalInfo(hideable = false, content = TextReference.Res(R.string.common_locked))
WalletAdditionalInfo(
hideable = false,
content = TextReference.Res(R.string.common_locked),
)
} else {
val blockchain = scanResponse.cardTypesResolver.getBlockchain()
val amount = currencyAmount?.format { crypto(blockchain.currency, blockchain.decimals()) }
WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty()))
WalletAdditionalInfo(
hideable = true,
content = TextReference.Str(value = amount.orEmpty()),
)
}
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.copy
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import timber.log.Timber

View file

@ -8,8 +8,10 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
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
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -19,6 +21,7 @@ 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
@Suppress("LongParameterList")
@ModelScoped
@ -40,6 +43,9 @@ 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) {
override fun create(): List<WalletSubscriber> {
@ -54,6 +60,9 @@ internal class MultiWalletContentLoader(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
).let(::add)
WalletNFTListSubscriber(

View file

@ -8,8 +8,10 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
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
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -18,6 +20,7 @@ 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")
@ -38,6 +41,9 @@ 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,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader {
@ -59,6 +65,9 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
walletsRepository = walletsRepository,
getNFTCollectionsUseCase = getNFTCollectionsUseCase,
currenciesRepository = currenciesRepository,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
)
}
}

View file

@ -12,6 +12,7 @@ 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
@ -35,6 +36,7 @@ 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> {
@ -53,6 +55,7 @@ internal class SingleWalletContentLoader(
clickIntents = clickIntents,
getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase,
getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase,
accountDependencies = accountDependencies,
),
SingleWalletNotificationsSubscriber(
userWallet = userWallet,

View file

@ -13,6 +13,7 @@ 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
@ -34,12 +35,14 @@ 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

@ -4,7 +4,9 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
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.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -14,6 +16,7 @@ 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
@Suppress("LongParameterList")
internal class SingleWalletWithTokenContentLoader(
@ -30,6 +33,9 @@ internal class SingleWalletWithTokenContentLoader(
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val accountDependencies: AccountDependencies,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> {
@ -43,6 +49,9 @@ internal class SingleWalletWithTokenContentLoader(
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
).let(::add)
MultiWalletWarningsSubscriber(
userWallet = userWallet,

View file

@ -5,7 +5,9 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
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.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -14,6 +16,7 @@ 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
@ -31,6 +34,9 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val accountDependencies: AccountDependencies,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
) {
fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader {
@ -48,6 +54,9 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
getStoryContentUseCase = getStoryContentUseCase,
shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase,
accountDependencies = accountDependencies,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class TangemPayState {
object Empty : TangemPayState()
data class Progress(
val title: TextReference,
val buttonText: TextReference,
@DrawableRes val iconRes: Int,
val onButtonClick: () -> Unit,
val showProgress: Boolean = false,
) : TangemPayState()
data class Card(
val lastFourDigits: TextReference,
val balanceText: TextReference,
val onClick: () -> Unit,
) : TangemPayState()
}

View file

@ -1,7 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.details.TokenAction
import kotlinx.serialization.Serializable
/**
@ -23,4 +25,11 @@ internal sealed interface WalletDialogConfig {
@Serializable
data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig
@Serializable
data class YieldSupplyWarning(
val cryptoCurrency: CryptoCurrency,
val tokenAction: TokenAction,
val onWarningAcknowledged: (TokenAction) -> Unit,
) : WalletDialogConfig
}

View file

@ -30,4 +30,9 @@ internal sealed class WalletEvent {
RIGHT,
}
}
data class RequestPushPermissions(
val onAllow: () -> Unit,
val onDeny: () -> Unit,
) : WalletEvent()
}

View file

@ -45,8 +45,8 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
text = TextReference.Res(id = R.string.common_buy),
iconResId = R.drawable.ic_plus_24,
onClick = onClick,
enabled = enabled,
dimContent = dimContent,
isEnabled = enabled,
shouldDimContent = dimContent,
isInProgress = isInProgress,
),
)
@ -67,8 +67,8 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
text = TextReference.Res(id = R.string.common_send),
iconResId = R.drawable.ic_arrow_up_24,
onClick = onClick,
enabled = enabled,
dimContent = dimContent,
isEnabled = enabled,
shouldDimContent = dimContent,
),
)
@ -87,8 +87,8 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_receive),
iconResId = R.drawable.ic_arrow_down_24,
enabled = enabled,
dimContent = dimContent,
isEnabled = enabled,
shouldDimContent = dimContent,
onClick = onClick,
onLongClick = onLongClick,
),
@ -103,8 +103,8 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
text = TextReference.Res(id = R.string.common_stake),
iconResId = R.drawable.ic_staking_24,
onClick = onClick,
enabled = enabled,
dimContent = dimContent,
isEnabled = enabled,
shouldDimContent = dimContent,
),
)
@ -125,8 +125,8 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
text = TextReference.Res(id = R.string.common_sell),
iconResId = R.drawable.ic_currency_24,
onClick = onClick,
enabled = enabled,
dimContent = dimContent,
isEnabled = enabled,
shouldDimContent = dimContent,
isInProgress = isInProgress,
),
)
@ -149,10 +149,10 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
text = TextReference.Res(id = R.string.swapping_swap_action),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = onClick,
enabled = enabled,
dimContent = dimContent,
isEnabled = enabled,
shouldDimContent = dimContent,
isInProgress = isInProgress,
showBadge = showBadge,
shouldShowBadge = showBadge,
),
)
}

View file

@ -138,6 +138,11 @@ sealed class WalletNotification(val config: NotificationConfig) {
formatArgs = wrappedList(count),
),
)
data object YeildSupplyApprove : Warning(
title = resourceReference(R.string.yield_module_main_view_approve_notification_title),
subtitle = resourceReference(R.string.yield_module_main_view_approve_notification_description),
)
}
sealed class Informational(
@ -326,4 +331,23 @@ sealed class WalletNotification(val config: NotificationConfig) {
iconSize = 54.dp,
),
)
data class PushNotifications(
val onCloseClick: () -> Unit,
val onEnabledClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.user_push_notification_banner_title),
subtitle = resourceReference(R.string.user_push_notification_banner_subtitle),
iconResId = R.drawable.img_push_reminder,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
primaryText = resourceReference(id = R.string.common_enable),
onPrimaryClick = onEnabledClick,
secondaryText = resourceReference(id = R.string.common_later),
onSecondaryClick = onCloseClick,
),
iconSize = 54.dp,
),
)
}

View file

@ -14,4 +14,5 @@ internal data class WalletScreenState(
val isHidingMode: Boolean,
val showMarketsOnboarding: Boolean,
val onDismissMarketsOnboarding: () -> Unit,
val tangemPayState: TangemPayState = TangemPayState.Empty,
)

View file

@ -23,6 +23,7 @@ internal sealed interface WalletState : WalletStateHolder {
abstract val tokensListState: WalletTokensListState
abstract val nftState: WalletNFTItemUM
abstract val type: WalletType
data class Content(
override val pullToRefreshConfig: PullToRefreshConfig,
@ -32,12 +33,14 @@ internal sealed interface WalletState : WalletStateHolder {
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val tokensListState: WalletTokensListState,
override val nftState: WalletNFTItemUM,
override val type: WalletType,
) : MultiCurrency()
data class Locked(
override val walletCardState: WalletCardState,
override val buttons: PersistentList<WalletManageButton>,
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val type: WalletType,
val onUnlockNotificationClick: () -> Unit,
) : MultiCurrency(),
WalletStateHolder by LockedWalletStateHolder(
@ -50,6 +53,11 @@ internal sealed interface WalletState : WalletStateHolder {
override val tokensListState = WalletTokensListState.ContentState.Locked
override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden
}
enum class WalletType {
Hot,
Cold,
}
}
sealed class SingleCurrency : WalletState, TxHistoryStateHolder {

View file

@ -9,6 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory
import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState.MultiCurrency.WalletType
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -61,6 +62,10 @@ internal class InitializeWalletsTransformer(
buttons = createMultiWalletEnabledButtons(userWallet),
bottomSheetConfig = null,
onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick,
type = when (userWallet) {
is UserWallet.Cold -> WalletType.Cold
is UserWallet.Hot -> WalletType.Hot
},
)
},
singleCurrencyCreator = {

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.models.wallet.UserWallet
@ -14,6 +15,7 @@ import timber.log.Timber
internal class SetCryptoCurrencyActionsTransformer(
private val tokenActionsState: TokenActionsState,
private val userWallet: UserWallet,
private val portfolioId: PortfolioId,
private val clickIntents: WalletClickIntents,
) : WalletStateTransformer(userWallet.walletId) {
@ -48,6 +50,7 @@ internal class SetCryptoCurrencyActionsTransformer(
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = {
clickIntents.onBuyClick(
userWalletId = portfolioId.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
unavailabilityReason = action.unavailabilityReason,
)
@ -59,7 +62,10 @@ internal class SetCryptoCurrencyActionsTransformer(
enabled = true,
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = {
clickIntents.onReceiveClick(cryptoCurrencyStatus = cryptoCurrencyStatus)
clickIntents.onReceiveClick(
portfolioId.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
},
onLongClick = {
clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus)
@ -84,6 +90,7 @@ internal class SetCryptoCurrencyActionsTransformer(
dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None,
onClick = {
clickIntents.onSendClick(
userWalletId = portfolioId.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
unavailabilityReason = action.unavailabilityReason,
)

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -13,10 +13,12 @@ import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import timber.log.Timber
internal class SetTokenListTransformer(
private val tokenList: TokenList,
private val params: TokenConverterParams,
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val clickIntents: WalletClickIntents,
private val yieldSupplyApyMap: Map<String, String> = emptyMap(),
private val stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
) : WalletStateTransformer(userWallet.walletId) {
override fun transform(prevState: WalletState): WalletState {
@ -42,8 +44,12 @@ internal class SetTokenListTransformer(
}
private fun WalletCardState.toLoadedState(): WalletCardState {
val fiatBalance = when (params) {
is TokenConverterParams.Account -> params.accountList.totalFiatBalance
is TokenConverterParams.Wallet -> params.tokenList.totalFiatBalance
}
return MultiWalletCardStateConverter(
fiatBalance = tokenList.totalFiatBalance,
fiatBalance = fiatBalance,
selectedWallet = userWallet,
appCurrency = appCurrency,
).convert(value = this)
@ -51,10 +57,12 @@ internal class SetTokenListTransformer(
private fun WalletTokensListState.toLoadedState(): WalletTokensListState {
return TokenListStateConverter(
tokenList = tokenList,
params = params,
selectedWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldModuleApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
).convert(value = this)
}
}

View file

@ -59,8 +59,8 @@ internal class SetTxHistoryCountTransformer(
private fun createLoadingItems(): List<TxHistoryState.TxHistoryItemState> {
return buildList {
add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick))
(1..transactionsCount).forEach {
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString())))
for (i in 1..transactionsCount) {
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(i.toString())))
}
}
}

View file

@ -105,7 +105,10 @@ internal class SetVisaInfoTransformer(
},
)
return WalletAdditionalInfo(hideable = true, infoContent)
return WalletAdditionalInfo(
hideable = true,
content = infoContent,
)
}
private fun getRefreshTokenExpiredState(prevState: WalletState.Visa.Content): WalletState {
@ -148,7 +151,11 @@ internal class SetVisaInfoTransformer(
enabled = true,
dimContent = false,
onClick = {
clickIntents.onReceiveClick(cryptoCurrencyStatus, MainScreenAnalyticsEvent.ButtonReceive)
clickIntents.onReceiveClick(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
event = MainScreenAnalyticsEvent.ButtonReceive,
)
},
onLongClick = {
clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus)

View file

@ -0,0 +1,68 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.extensions.TextReference
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.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus.CANCELED
import com.tangem.domain.pay.model.OrderStatus.UNKNOWN
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createKycInProgressState
import java.util.Currency
/**
* Hardcode Polygon chain id only for F&F.
* Later chain id will be fetched from BFF.
*/
private const val POLYGON_CHAIN_ID = 137
internal class TangemPayInitialStateTransformer(
private val value: MainScreenCustomerInfo? = null,
private val onClickIssue: () -> Unit = {},
private val onClickKyc: () -> Unit = {},
private val openDetails: (config: TangemPayDetailsConfig) -> Unit = {},
) : WalletScreenStateTransformer {
override fun transform(prevState: WalletScreenState): WalletScreenState {
val tangemPayState = createInitialState()
return prevState.copy(tangemPayState = tangemPayState)
}
private fun createInitialState(): TangemPayState {
val cardInfo = value?.info?.cardInfo
return when {
value == null -> TangemPayState.Empty
!value.info.isKycApproved -> createKycInProgressState(onClickKyc)
cardInfo != null -> getCardInfoState(cardInfo)
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 getBalanceText(cardInfo: CardInfo): String {
val currency = Currency.getInstance(cardInfo.currencyCode)
return cardInfo.balance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState
internal class TangemPayIssueAvailableStateTransformer(
private val onClickIssue: () -> Unit = {},
) : WalletScreenStateTransformer {
override fun transform(prevState: WalletScreenState): WalletScreenState =
prevState.copy(tangemPayState = createIssueAvailableState(onClickIssue))
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState
internal class TangemPayIssueProgressStateTransformer : WalletScreenStateTransformer {
override fun transform(prevState: WalletScreenState): WalletScreenState =
prevState.copy(tangemPayState = createIssueProgressState())
}

View file

@ -0,0 +1,18 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.tokenlist.TokenList
sealed interface TokenConverterParams {
data class Wallet(
val portfolioId: PortfolioId,
val tokenList: TokenList,
) : TokenConverterParams
data class Account(
val accountList: AccountStatusList,
val expandedAccounts: Set<AccountId>,
) : TokenConverterParams
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents
@ -20,6 +21,8 @@ internal class MultiWalletCurrencyActionsConverter(
private val clickIntents: WalletCurrencyActionsClickIntents,
) : Converter<TokenActionsState, ImmutableList<TokenActionButtonConfig>> {
private val userWalletId: UserWalletId = userWallet.walletId
override fun convert(value: TokenActionsState): ImmutableList<TokenActionButtonConfig> {
return value.states
.filterIfSingleWithToken()
@ -46,7 +49,7 @@ internal class MultiWalletCurrencyActionsConverter(
if (actionsState is TokenActionsState.ActionState.Send && cryptoCurrencyStatus.value.amount.isNullOrZero()) {
return null
}
val noneReason = ScenarioUnavailabilityReason.None
val title: TextReference
val icon: Int
val action: () -> Unit
@ -54,27 +57,27 @@ internal class MultiWalletCurrencyActionsConverter(
is TokenActionsState.ActionState.Buy -> {
title = resourceReference(R.string.common_buy)
icon = R.drawable.ic_plus_24
action = { clickIntents.onBuyClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) }
action = { clickIntents.onBuyClick(userWalletId, cryptoCurrencyStatus, noneReason) }
}
is TokenActionsState.ActionState.Receive -> {
title = resourceReference(R.string.common_receive)
icon = R.drawable.ic_arrow_down_24
action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }
action = { clickIntents.onReceiveClick(userWalletId, cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.Stake -> {
title = resourceReference(R.string.common_stake)
icon = R.drawable.ic_staking_24
action = { clickIntents.onStakeClick(cryptoCurrencyStatus, actionsState.yield) }
action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.yield) }
}
is TokenActionsState.ActionState.Sell -> {
title = resourceReference(R.string.common_sell)
icon = R.drawable.ic_currency_24
action = { clickIntents.onSellClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) }
action = { clickIntents.onSellClick(cryptoCurrencyStatus, noneReason) }
}
is TokenActionsState.ActionState.Send -> {
title = resourceReference(R.string.common_send)
icon = R.drawable.ic_arrow_up_24
action = { clickIntents.onSendClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) }
action = { clickIntents.onSendClick(userWalletId, cryptoCurrencyStatus, noneReason) }
}
is TokenActionsState.ActionState.Swap -> {
title = resourceReference(R.string.swapping_swap_action)
@ -82,20 +85,20 @@ internal class MultiWalletCurrencyActionsConverter(
action = {
clickIntents.onSwapClick(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWalletId = userWallet.walletId,
unavailabilityReason = ScenarioUnavailabilityReason.None,
userWalletId = userWalletId,
unavailabilityReason = noneReason,
)
}
}
is TokenActionsState.ActionState.CopyAddress -> {
title = resourceReference(R.string.common_copy_address)
icon = R.drawable.ic_copy_24
action = { clickIntents.onCopyAddressClick(cryptoCurrencyStatus) }
action = { clickIntents.onCopyAddressClick(userWalletId, cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.HideToken -> {
title = resourceReference(R.string.token_details_hide_token)
icon = R.drawable.ic_hide_24
action = { clickIntents.onHideTokensClick(cryptoCurrencyStatus) }
action = { clickIntents.onHideTokensClick(userWalletId, cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.Analytics -> {
title = resourceReference(R.string.common_analytics)
@ -109,7 +112,7 @@ internal class MultiWalletCurrencyActionsConverter(
iconResId = icon,
onClick = action,
isWarning = actionsState is TokenActionsState.ActionState.HideToken,
enabled = actionsState.unavailabilityReason == ScenarioUnavailabilityReason.None,
enabled = actionsState.unavailabilityReason == noneReason,
)
}
}

View file

@ -1,67 +1,156 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.common.ui.tokens.TokenItemStateConverter
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.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
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.tokenlist.TokenList
import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup
import com.tangem.domain.models.wallet.UserWallet
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
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig
internal class TokenListStateConverter(
appCurrency: AppCurrency,
private val tokenList: TokenList,
private val appCurrency: AppCurrency,
private val params: TokenConverterParams,
private val selectedWallet: UserWallet,
private val clickIntents: WalletClickIntents,
private val yieldModuleApyMap: Map<String, String>,
private val stakingApyMap: Map<String, List<Yield.Validator>>,
) : Converter<WalletTokensListState, WalletTokensListState> {
private val tokenStatusConverter = TokenItemStateConverter(
private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit =
{ accountId, currencyStatus ->
clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus)
}
private val onTokenLongClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit =
{ accountId, currencyStatus ->
clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus)
}
private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter(
appCurrency = appCurrency,
onItemClick = { _, status -> clickIntents.onTokenItemClick(status) },
onItemLongClick = { _, status -> clickIntents.onTokenItemLongClick(status) },
yieldModuleApyMap = yieldModuleApyMap,
stakingApyMap = stakingApyMap,
onItemClick = { _, status -> onTokenClick(accountId, status) },
onItemLongClick = { _, status -> onTokenLongClick(accountId, status) },
)
override fun convert(value: WalletTokensListState): WalletTokensListState {
return when (tokenList) {
return when (params) {
is TokenConverterParams.Account -> convertAccountList(params)
is TokenConverterParams.Wallet -> convertTokenList(
tokenConverter = tokenStatusConverter((params.portfolioId as? PortfolioId.Account)?.accountId),
tokenList = params.tokenList,
)
}
}
private fun convertTokenList(tokenConverter: TokenItemStateConverter, tokenList: TokenList): WalletTokensListState =
when (tokenList) {
is TokenList.Empty -> WalletTokensListState.Empty
is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content(
items = tokenList.toGroupedItems(),
organizeTokensButtonConfig = getOrganizeTokensButtonState(
currenciesSize = tokenList.groups.flatMap(NetworkGroup::currencies).size,
),
items = tokenList.toGroupedItems(tokenConverter),
organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList),
)
is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content(
items = tokenList.toUngroupedItems(),
organizeTokensButtonConfig = getOrganizeTokensButtonState(currenciesSize = tokenList.currencies.size),
items = tokenList.toUngroupedItems(tokenConverter),
organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList),
)
}
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
val account: Account.CryptoPortfolio = this.account
val isExtend = params.expandedAccounts.contains(account.accountId)
val onItemClick: (Account.CryptoPortfolio) -> Unit = {
if (isExtend) {
clickIntents.onAccountCollapseClick(it)
} else {
clickIntents.onAccountExpandClick(it)
}
}
val converter = AccountCryptoPortfolioItemStateConverter(
appCurrency = appCurrency,
account = account,
onItemClick = onItemClick,
)
val accountItem = converter.convert(tokenList.totalFiatBalance)
val tokenConverter = tokenStatusConverter(account.accountId)
val tokensListState = convertTokenList(tokenConverter, tokenList)
val items = when (tokensListState) {
is WalletTokensListState.ContentState.PortfolioContent -> tokensListState.items
is WalletTokensListState.ContentState.Content -> tokensListState.items
is WalletTokensListState.ContentState.Loading -> tokensListState.items
is WalletTokensListState.ContentState.Locked -> tokensListState.items
is WalletTokensListState.Empty -> listOf()
}
return TokensListItemUM.Portfolio(
tokenItemUM = accountItem,
isExpanded = isExtend,
isCollapsable = true,
tokens = items.filterIsInstance<PortfolioTokensListItemUM>().toPersistentList(),
)
}
val accountItems = accountList.accountStatuses
.map { accountStatus ->
when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.map()
}
}
return WalletTokensListState.ContentState.PortfolioContent(
items = accountItems.toPersistentList(),
organizeTokensButtonConfig = organizeTokensButtonConfig,
)
}
private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList<TokensListItemUM> {
private fun TokenList.GroupedByNetwork.toGroupedItems(
tokenConverter: TokenItemStateConverter,
): PersistentList<TokensListItemUM> {
return groups.fold(initial = persistentListOf()) { acc, group ->
acc.mutate { it.addGroup(group) }
acc.mutate { it.addGroup(tokenConverter, group) }
}
}
private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList<TokensListItemUM> {
private fun TokenList.Ungrouped.toUngroupedItems(
tokenConverter: TokenItemStateConverter,
): PersistentList<TokensListItemUM> {
return currencies.fold(initial = persistentListOf()) { acc, token ->
acc.mutate { it.addToken(token) }
acc.mutate { it.addToken(tokenConverter, token) }
}
}
private fun MutableList<TokensListItemUM>.addGroup(group: NetworkGroup): List<TokensListItemUM> {
private fun MutableList<TokensListItemUM>.addGroup(
tokenConverter: TokenItemStateConverter,
group: NetworkGroup,
): List<TokensListItemUM> {
val groupTitle = TokensListItemUM.GroupTitle(
id = group.network.hashCode(),
text = resourceReference(
@ -71,19 +160,28 @@ internal class TokenListStateConverter(
)
add(groupTitle)
group.currencies.forEach { token -> addToken(token) }
group.currencies.forEach { token -> addToken(tokenConverter, token) }
return this
}
private fun MutableList<TokensListItemUM>.addToken(token: CryptoCurrencyStatus): List<TokensListItemUM> {
val tokenItemState = tokenStatusConverter.convert(token)
private fun MutableList<TokensListItemUM>.addToken(
tokenConverter: TokenItemStateConverter,
token: CryptoCurrencyStatus,
): List<TokensListItemUM> {
val tokenItemState = tokenConverter.convert(token)
add(TokensListItemUM.Token(tokenItemState))
return this
}
private fun getOrganizeTokensButtonState(currenciesSize: Int): WalletOrganizeTokensButtonConfig? {
private fun getOrganizeTokensButtonState(tokenList: TokenList): WalletOrganizeTokensButtonConfig? {
val currenciesSize = when (tokenList) {
TokenList.Empty -> return null
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies).size
is TokenList.Ungrouped -> tokenList.currencies.size
}
return if (currenciesSize > 1 && !isSingleCurrencyWalletWithToken()) {
WalletOrganizeTokensButtonConfig(
isEnabled = tokenList.totalFiatBalance !is TotalFiatBalance.Loading,

View file

@ -61,6 +61,7 @@ internal class TxHistoryItemStateConverter(
is TransactionType.Operation,
is TransactionType.Swap,
is TransactionType.Transfer,
is TransactionType.YieldSupply,
is TransactionType.UnknownOperation,
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
}
@ -71,6 +72,7 @@ internal class TxHistoryItemStateConverter(
is TransactionType.Operation -> stringReference(type.name)
is TransactionType.Swap -> resourceReference(R.string.common_swap)
is TransactionType.Transfer -> resourceReference(R.string.common_transfer)
is TransactionType.YieldSupply -> resourceReference(R.string.yield_module_supply)
is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake)
is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake)
is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote)

View file

@ -0,0 +1,31 @@
package com.tangem.feature.wallet.presentation.wallet.state.util
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
internal object TangemPayStateCreator {
fun createKycInProgressState(onClickKyc: () -> Unit): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = onClickKyc,
)
fun createIssueAvailableState(onClickIssue: () -> Unit) = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = onClickIssue,
)
fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
showProgress = true,
)
}

View file

@ -54,6 +54,7 @@ internal class WalletLoadingStateFactory(
bottomSheetConfig = null,
tokensListState = WalletTokensListState.ContentState.Loading,
nftState = WalletNFTItemUM.Hidden,
type = WalletState.MultiCurrency.WalletType.Hot,
)
}
@ -66,6 +67,7 @@ internal class WalletLoadingStateFactory(
bottomSheetConfig = null,
tokensListState = WalletTokensListState.ContentState.Loading,
nftState = WalletNFTItemUM.Hidden,
type = WalletState.MultiCurrency.WalletType.Cold,
)
}

View file

@ -1,55 +1,78 @@
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
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
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
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
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 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.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import timber.log.Timber
@Suppress("LongParameterList")
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 runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletSubscriber() {
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 runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase
protected abstract val accountDependencies: AccountDependencies
protected abstract val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase
protected abstract val stakingApyFlowUseCase: StakingApyFlowUseCase
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 suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>)
protected abstract fun accountListFlow(coroutineScope: CoroutineScope): Flow<AccountStatusList>
override fun create(coroutineScope: CoroutineScope): Flow<*> {
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<*> {
return combine(
flow = tokenListFlow(coroutineScope)
.onEach { maybeTokenList ->
coroutineScope.launch {
sendTokenListAnalytics(maybeTokenList)
sendTokenListAnalytics(
flattenCurrencies = maybeTokenList.getOrNull()?.flattenCurrencies(),
totalFiatBalance = maybeTokenList.getOrNull()?.totalFiatBalance,
)
}.saveIn(sendAnalyticsJobHolder)
}
.distinctUntilChanged()
@ -60,42 +83,60 @@ internal abstract class BasicTokenListSubscriber(
coroutineScope.launch { startCheck(maybeTokenList) }
},
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
transform = { maybeTokenList, maybeAppCurrency ->
val appCurrency = maybeAppCurrency.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
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
},
flow2 = appCurrencyFlow(),
flow3 = yieldSupplyApyFlow(),
flow4 = stakingApyFlow(),
transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap ->
singleAccountTransform(
maybeTokenList = maybeTokenList,
appCurrency = appCurrency,
portfolioId = PortfolioId(userWallet.walletId),
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
)
updateContent(tokenList, appCurrency)
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 suspend fun startCheck(maybeTokenList: Lce<TokenListError, TokenList>) {
// Run Polkadot account health check
maybeTokenList.getOrNull()?.let { tokenList ->
@ -110,24 +151,123 @@ internal abstract class BasicTokenListSubscriber(
}
}
private suspend fun sendTokenListAnalytics(maybeTokenList: Lce<TokenListError, 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()
.onEach { accountList ->
// todo account see[onAccountListReceived]
// coroutineScope.launch { onAccountListReceived() }.saveIn(onTokenListReceivedJobHolder)
accountList.flattenTokens()
.forEach { tokenList -> startCheck(Lce.Content(tokenList)) }
},
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?,
) {
val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId)
tokenListAnalyticsSender.send(
displayedUiState = displayedState,
userWallet = userWallet,
tokenList = maybeTokenList.getOrNull() ?: return,
flattenCurrencies = flattenCurrencies ?: return,
totalFiatBalance = totalFiatBalance ?: return,
)
}
private fun updateContent(tokenList: TokenList, appCurrency: AppCurrency) {
private fun updateContent(
params: TokenConverterParams,
appCurrency: AppCurrency,
yieldSupplyApyMap: Map<String, String>,
stakingApyMap: Map<String, List<Yield.Validator>>,
) {
stateHolder.update(
SetTokenListTransformer(
tokenList = tokenList,
params = params,
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
),
)
}
private fun appCurrencyFlow(): Flow<AppCurrency> = getSelectedAppCurrencyUseCase()
.map {
it.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
}
.distinctUntilChanged()
private fun yieldSupplyApyFlow(): Flow<Map<String, String>> = yieldSupplyApyFlowUseCase()
.distinctUntilChanged()
private fun stakingApyFlow(): Flow<Map<String, List<Yield.Validator>>> = stakingApyFlowUseCase()
.distinctUntilChanged()
}

View file

@ -1,5 +1,7 @@
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
@ -8,36 +10,35 @@ import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
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
@Suppress("LongParameterList")
internal class MultiWalletTokenListSubscriber(
private val userWallet: UserWallet,
override val userWallet: UserWallet,
private val tokenListStore: MultiWalletTokenListStore,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
clickIntents = clickIntents,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override val stateHolder: WalletStateController,
override val clickIntents: WalletClickIntents,
override val tokenListAnalyticsSender: TokenListAnalyticsSender,
override val walletWithFundsChecker: WalletWithFundsChecker,
override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
override val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
override val accountDependencies: AccountDependencies,
override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
override val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : BasicTokenListSubscriber() {
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {
tokenListStore.addIfNot(userWallet.walletId, coroutineScope)
@ -45,6 +46,11 @@ 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

@ -1,10 +1,13 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.TokenActionsState
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.domain.collectLatest
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer
@ -17,28 +20,39 @@ internal class SingleWalletButtonsSubscriber(
private val clickIntents: WalletClickIntents,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val accountDependencies: AccountDependencies,
) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<TokenActionsState> {
return channelFlow {
getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status ->
getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status)
.conflate()
.distinctUntilChanged()
.firstOrNull()
getCryptoCurrencyActionsUseCase(userWallet = userWallet, status = status)
?.let { send(it) }
}
}
.onEach(::updateContent)
.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)
}
}
private fun updateContent(tokenActionsState: TokenActionsState) {
private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) {
stateHolder.update(
SetCryptoCurrencyActionsTransformer(
tokenActionsState = tokenActionsState,
userWallet = userWallet,
clickIntents = clickIntents,
portfolioId = portfolioId,
),
)
}
private suspend fun getCryptoCurrencyActionsUseCase(userWallet: UserWallet, status: CryptoCurrencyStatus) =
this.getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status)
.conflate()
.distinctUntilChanged()
.firstOrNull()
}

View file

@ -1,38 +1,39 @@
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
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
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
@Suppress("LongParameterList")
internal class SingleWalletWithTokenListSubscriber(
private val userWallet: UserWallet.Cold,
override val userWallet: UserWallet.Cold,
private val tokenListStore: MultiWalletTokenListStore,
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
clickIntents = clickIntents,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override val stateHolder: WalletStateController,
override val clickIntents: WalletClickIntents,
override val tokenListAnalyticsSender: TokenListAnalyticsSender,
override val walletWithFundsChecker: WalletWithFundsChecker,
override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
override val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
override val accountDependencies: AccountDependencies,
override val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
override val stakingApyFlowUseCase: StakingApyFlowUseCase,
) : BasicTokenListSubscriber() {
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {
tokenListStore.addIfNot(userWallet.walletId, coroutineScope)
@ -40,5 +41,10 @@ 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

@ -41,7 +41,7 @@ private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) {
if (onActionClick != null) {
confirmButton = DialogButtonUM(
title = state.confirmButtonText.resolveReference(),
warning = state.isWarningConfirmButton,
isWarning = state.isWarningConfirmButton,
onClick = {
onActionClick()
onDismiss()
@ -54,7 +54,7 @@ private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) {
} else {
confirmButton = DialogButtonUM(
title = state.confirmButtonText.resolveReference(),
warning = state.isWarningConfirmButton,
isWarning = state.isWarningConfirmButton,
onClick = onDismiss,
)
dismissButton = null
@ -77,7 +77,7 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U
fieldValue = value,
confirmButton = DialogButtonUM(
title = state.confirmButtonText.resolveReference(),
enabled = value.text.isNotEmpty() &&
isEnabled = value.text.isNotEmpty() &&
value.text != state.text &&
state.errorTextProvider(value.text) == null,
onClick = {

View file

@ -3,19 +3,20 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.utils.requestPermission
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester
import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex
import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
@Composable
internal fun WalletEventEffect(
@ -28,6 +29,13 @@ internal fun WalletEventEffect(
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = LocalContext.current.resources
var showPermissionRequest by remember { mutableStateOf<Pair<() -> Unit, () -> Unit>?>(null) }
HandlePermissionRequest(
permissionRequestParams = showPermissionRequest,
onPermissionRequestResult = { showPermissionRequest = null },
)
EventEffect(
event = event,
onTrigger = { value ->
@ -56,7 +64,34 @@ internal fun WalletEventEffect(
is WalletEvent.DemonstrateWalletsScrollPreview -> {
walletsListState.demonstrateScrolling(coroutineScope = coroutineScope, direction = value.direction)
}
is WalletEvent.RequestPushPermissions -> {
showPermissionRequest = value.onAllow to value.onDeny
}
}
},
)
}
@Composable
private fun HandlePermissionRequest(
permissionRequestParams: Pair<() -> Unit, () -> Unit>?,
onPermissionRequestResult: () -> Unit,
) {
val actualPermissionLauncher = permissionRequestParams?.let { (onAllowExternal, onDenyExternal) ->
requestPermission(
onAllow = {
onAllowExternal()
onPermissionRequestResult()
},
onDeny = {
onDenyExternal()
onPermissionRequestResult()
},
permission = PUSH_PERMISSION,
)
}
LaunchedEffect(permissionRequestParams) {
actualPermissionLauncher?.invoke()
}
}

View file

@ -79,6 +79,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
@ -212,6 +213,11 @@ private fun WalletContent(
notifications(configs = selectedWallet.warnings, modifier = itemModifier)
item(
key = "TangemPayMainScreenBlock",
contentType = state.tangemPayState::class.java,
) { TangemPayMainScreenBlock(state.tangemPayState, itemModifier) }
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
@ -291,10 +297,10 @@ private inline fun BaseScaffoldWithMarkets(
listState: LazyListState,
selectedWallet: WalletState,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp,
crossinline bottomSheetContent: @Composable () -> Unit,
alertConfig: WalletAlertState?,
bottomSheetHeaderHeightProvider: () -> Dp,
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
crossinline bottomSheetContent: @Composable () -> Unit,
crossinline content: @Composable (PaddingValues) -> Unit,
) {
val bottomSheetState = rememberTangemStandardBottomSheetState()

View file

@ -350,6 +350,7 @@ private fun AdditionalInfoText(text: TextReference) {
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption2,
modifier = Modifier.testTag(MainScreenTestTags.DEVICES_COUNT),
)
}

View file

@ -13,6 +13,7 @@ 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
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
@ -59,6 +60,7 @@ internal fun LazyListScope.portfolioTokensList(
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc()
val lastIndex = tokens.lastIndex.inc()
val isPreview = LocalInspectionMode.current
val appear = remember {
MutableTransitionState(isPreview).apply { targetState = true }
@ -69,11 +71,12 @@ internal fun LazyListScope.portfolioTokensList(
.animateItem()
.roundedShapeItemDecoration(
currentIndex = indexWithHeader,
lastIndex = tokens.lastIndex.inc(),
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
),
visibleState = appear,
) {
val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier
PortfolioTokensListItem(
state = token,
isBalanceHidden = isBalanceHidden,
@ -114,10 +117,15 @@ private fun LazyListScope.portfolioItem(
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.padding(top = TangemTheme.dimens.spacing8),
modifier = modifier,
)
}
} else {

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
@ -15,7 +14,6 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.buttons.actions.ActionButtonContent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MainScreenTestTags
/**
[REDACTED_AUTHOR]
@ -50,7 +48,7 @@ internal fun MultiCurrencyAction(
paddingBetweenIconAndText = 4.dp,
)
},
modifier = modifier.testTag(MainScreenTestTags.MULTI_CURRENCY_ACTION_BUTTON),
modifier = modifier,
color = TangemTheme.colors.button.secondary,
)
}

View file

@ -31,7 +31,7 @@ internal fun LazyListScope.organizeTokensButton(
text = resourceReference(id = R.string.organize_tokens_title),
iconResId = R.drawable.ic_filter_24,
onClick = onClick,
enabled = isEnabled,
isEnabled = isEnabled,
),
)
}

View file

@ -0,0 +1,83 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.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) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = TangemTheme.shapes.roundedCornersXMedium,
color = TangemTheme.colors.background.primary,
onClick = state.onClick,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.img_tangem_pay_card_main_36),
contentDescription = null,
modifier = Modifier.size(36.dp).clip(CircleShape),
)
Spacer(Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResourceSafe(R.string.tangempay_kyc_card_ready_notification_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
Spacer(Modifier.height(6.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(R.drawable.img_visa_label_26_16),
contentDescription = null,
modifier = Modifier.size(26.dp, 16.dp),
)
Spacer(Modifier.width(4.dp))
Text(
text = state.lastFourDigits.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.TopEnd,
) {
Text(
text = state.balanceText.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
}
}
}
}

View file

@ -68,11 +68,11 @@ private fun BalancesAndLimitsBlock(state: BalancesAndLimitsBlockState, modifier:
@Composable
private inline fun ContentContainer(
enabled: Boolean,
modifier: Modifier = Modifier,
noinline onClick: () -> Unit,
crossinline title: @Composable () -> Unit,
crossinline content: @Composable () -> Unit,
crossinline endIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier.fillMaxWidth(),

View file

@ -133,10 +133,10 @@ private fun InfoButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
@Composable
private inline fun ContentContainer(
modifier: Modifier = Modifier,
title: @Composable BoxScope.() -> Unit,
firstBlock: @Composable ColumnScope.() -> Unit,
secondBlock: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.background(TangemTheme.colors.background.secondary),

View file

@ -19,8 +19,8 @@ private const val BLOCK_ITEM_VALUE_WEIGHT = .55f
@Composable
internal inline fun BlockContent(
title: TextReference,
content: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
description: @Composable RowScope.() -> Unit = {},
) {
Column(

View file

@ -0,0 +1,89 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock
@Composable
internal fun TangemPayMainScreenBlock(state: TangemPayState, modifier: Modifier = Modifier) {
when (state) {
is Progress -> {
Notification(
modifier = modifier
.fillMaxWidth(),
config = NotificationConfig(
title = state.title,
iconSize = 36.dp,
subtitle = TextReference.EMPTY,
iconResId = state.iconRes,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = state.buttonText,
onClick = state.onButtonClick,
shouldShowProgress = state.showProgress,
),
),
)
}
is TangemPayState.Card -> TangemPayCardMainBlock(state, modifier)
is TangemPayState.Empty -> Unit
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ResetCardScreenPreview() {
TangemThemePreview {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {},
),
)
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
),
)
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
showProgress = true,
),
)
TangemPayMainScreenBlock(
TangemPayState.Card(
lastFourDigits = TextReference.Str("*1234"),
balanceText = TextReference.Str("$ 0.00"),
onClick = {},
),
)
}
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.operations.attestation.ArtworkSize
import kotlinx.coroutines.flow.*
@ -17,12 +18,21 @@ import javax.inject.Inject
class DefaultUserWalletImageFetcher @Inject constructor(
private val getCardImageUseCase: GetCardImageUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val artworkUMConverter: ArtworkUMConverter,
) : UserWalletImageFetcher {
private val smallCache = MutableStateFlow(mapOf<String, ArtworkUM>())
private val largeCache = MutableStateFlow(mapOf<String, ArtworkUM>())
override fun allWallets(size: ArtworkSize): Flow<Map<UserWalletId, UserWalletItemUM.ImageState>> =
getWalletsUseCase.invoke()
.map { list -> list.map { it.walletId } }
.distinctUntilChanged()
.map { list -> list.map { walletId -> walletImage(walletId, size).map { image -> walletId to image } } }
.flatMapLatest { flows -> combine(flows) { it.toMap() } }
.distinctUntilChanged()
override fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> = when (wallet) {
is UserWallet.Cold -> walletImage(wallet.scanResponse.card, size)
is UserWallet.Hot -> flowOf(UserWalletItemUM.ImageState.MobileWallet)
@ -31,23 +41,20 @@ class DefaultUserWalletImageFetcher @Inject constructor(
override fun walletsImage(
wallets: Collection<UserWallet>,
size: ArtworkSize,
): Flow<Map<UserWalletId, UserWalletItemUM.ImageState>> = wallets
.map { userWallet -> walletImage(userWallet, size).map { imageState -> userWallet.walletId to imageState } }
.merge()
.runningFold(mapOf<UserWalletId, UserWalletItemUM.ImageState>()) { map, newState -> map.plus(newState) }
.filter { it.size >= wallets.size } // prevent spam, waiting full map
): Flow<Map<UserWalletId, UserWalletItemUM.ImageState>> = allWallets(size)
.map { allWallets -> allWallets.filter { (walletId, _) -> wallets.any { it.walletId == walletId } } }
.distinctUntilChanged()
override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> = flow {
val imagesFlow = getUserWalletUseCase.invokeFlow(walletId)
// emit Loading and wait wallet
.onEach { if (it.isLeft()) emit(UserWalletItemUM.ImageState.Loading) }
.filterIsInstance<Either.Right<UserWallet>>()
.map { it.value }
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))
}
}
.distinctUntilChanged()
.flatMapLatest { wallet -> walletImage(wallet, size) }
emitAll(imagesFlow)
}.distinctUntilChanged()
override fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> =
internalGetCardImage(

View file

@ -130,6 +130,11 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
balance = balance,
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
artwork = artworks[userWallet.walletId],
endIcon = if (isAuthMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) {
UserWalletItemUM.EndIcon.Warning
} else {
UserWalletItemUM.EndIcon.None
},
isAuthMode = isAuthMode,
)
.convert(userWallet)

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB