Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-15 11:22:40 +03:00
commit c2cd7e6b4c
675 changed files with 9577 additions and 4375 deletions

View file

@ -95,7 +95,7 @@ internal class OrganizeTokensModel @Inject constructor(
val onBack = MutableSharedFlow<Unit>()
init {
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened())
getBalanceHidingSettingsUseCase()
.onEach {
@ -117,7 +117,7 @@ internal class OrganizeTokensModel @Inject constructor(
val list = cachedAccountStatusList ?: return
if (list.sortType == TokensSortType.BALANCE) return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance())
modelScope.launch {
toggleTokenListSortingUseCaseV2(list).fold(
@ -132,7 +132,7 @@ internal class OrganizeTokensModel @Inject constructor(
val list = cachedTokenList ?: return
if (list.sortedBy == TokensSortType.BALANCE) return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance())
modelScope.launch {
toggleTokenListSortingUseCase(list).fold(
@ -150,7 +150,7 @@ internal class OrganizeTokensModel @Inject constructor(
if (accountsFeatureToggles.isFeatureEnabled) {
val list = cachedAccountStatusList ?: return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group())
modelScope.launch {
toggleTokenListGroupingUseCaseV2(list).fold(
@ -164,7 +164,7 @@ internal class OrganizeTokensModel @Inject constructor(
} else {
val list = cachedTokenList ?: return
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group())
modelScope.launch {
toggleTokenListGroupingUseCase(list).fold(
@ -228,7 +228,7 @@ internal class OrganizeTokensModel @Inject constructor(
}
override fun onCancelClick() {
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel)
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel())
modelScope.launch { onBack.emit(Unit) }
}

View file

@ -1,17 +1,20 @@
package com.tangem.feature.wallet.child.wallet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.essenty.lifecycle.doOnResume
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableDialogComponent
@ -22,6 +25,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
@ -36,18 +41,28 @@ import kotlinx.coroutines.launch
internal class WalletComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted navigate: (WalletRoute) -> Unit,
marketsEntryComponentFactory: MarketsEntryComponent.Factory,
feedEntryComponentFactory: FeedEntryComponent.Factory,
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory,
private val feedFeatureToggle: FeedFeatureToggle,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: WalletModel = getOrCreateModel()
private val feedEntryComponent by lazy {
feedEntryComponentFactory.create(child("feedEntryComponent"))
}
private val marketsEntryComponent by lazy {
marketsEntryComponentFactory.create(child("marketsEntryComponent"))
}
init {
lifecycle.subscribe(model.screenLifecycleProvider)
doOnResume { model.onResume() }
componentScope.launch { model.innerWalletRouter.navigateToFlow.collect { navigate(it) } }
}
@ -112,15 +127,23 @@ internal class WalletComponent @AssistedInject constructor(
},
)
private val marketsEntryComponent = marketsEntryComponentFactory.create(child("marketsEntryComponent"))
@Composable
override fun Content(modifier: Modifier) {
val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) }
var headerSize by remember { mutableStateOf(0.dp) }
val dialog by dialog.subscribeAsState()
WalletScreen(
state = model.uiState.collectAsStateWithLifecycle().value,
marketsEntryComponent = marketsEntryComponent,
bottomSheetContent = {
BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = { headerSize = it },
modifier = modifier,
)
},
bottomSheetHeaderHeightProvider = { headerSize },
onBottomSheetStateChange = { bottomSheetState.value = it },
)
when (val dialog = dialog.child?.instance) {
@ -130,6 +153,27 @@ internal class WalletComponent @AssistedInject constructor(
}
}
@Composable
private fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier = Modifier,
) {
if (feedFeatureToggle.isFeedEnabled) {
feedEntryComponent.BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
} else {
marketsEntryComponent.BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
}
@AssistedFactory
interface Factory {
fun create(appComponentContext: AppComponentContext, navigate: (WalletRoute) -> Unit): WalletComponent

View file

@ -7,9 +7,12 @@ import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
@ -40,6 +43,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.tangempay.TangemPayFeatureToggles
@ -96,6 +100,10 @@ internal class WalletModel @Inject constructor(
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val trackingContextProxy: TrackingContextProxy,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val feedFeatureToggle: FeedFeatureToggle,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
) : Model() {
@ -113,15 +121,11 @@ internal class WalletModel @Inject constructor(
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
init {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened)
screenLifecycleProvider.isBackgroundState
.onEach { isBackground ->
if (isBackground.not()) {
suggestToEnableBiometrics()
}
}.launchIn(modelScope)
if (!hotWalletFeatureToggles.isHotWalletEnabled) {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy())
}
updateMarketToggle()
suggestToOpenMarkets()
maybeMigrateNames()
@ -138,6 +142,18 @@ internal class WalletModel @Inject constructor(
clickIntents.initialize(innerWalletRouter, modelScope)
}
fun onResume() {
modelScope.launch(dispatchers.main) {
suggestToEnableBiometrics()
}
}
private fun updateMarketToggle() {
stateHolder.update {
it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled)
}
}
private fun updateYieldSupplyApy() {
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) {
modelScope.launch(dispatchers.default) {
@ -193,7 +209,6 @@ internal class WalletModel @Inject constructor(
private suspend fun shouldShowAskBiometryBottomSheet(): Boolean {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } &&
innerWalletRouter.isWalletLastScreen() &&
shouldShowAskBiometryUseCase() &&
canUseBiometryUseCase()
} else {
@ -271,11 +286,34 @@ internal class WalletModel @Inject constructor(
// It's okay here because we need to be able to observe the selected wallet changes
@Suppress("DEPRECATION")
private fun subscribeOnSelectedWalletFlow() {
getSelectedWalletUseCase().onRight {
it
getSelectedWalletUseCase().onRight { walletFlow ->
walletFlow
.conflate()
.distinctUntilChanged()
.onEach { selectedWallet ->
trackingContextProxy.setContext(selectedWallet)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
val hasMobileWallet = userWalletsListRepository.userWalletsSync()
.any { it is UserWallet.Hot }
val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) {
singleAccountListSupplier(selectedWallet.walletId)
.first()
.accounts
.size
} else {
null
}
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(
hasMobileWallet = hasMobileWallet,
accountsCount = accountsCount,
),
)
}
}
if (selectedWallet.isMultiCurrency) {
selectedWalletAnalyticsSender.send(selectedWallet)
}
@ -416,6 +454,7 @@ internal class WalletModel @Inject constructor(
when (action) {
is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action)
is WalletsUpdateActionResolver.Action.ReinitializeWallet -> reinitializeWallet(action)
is WalletsUpdateActionResolver.Action.ReinitializeWallets -> reinitializeWallets(action)
is WalletsUpdateActionResolver.Action.AddWallet -> addWallet(action)
is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action)
is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action)
@ -520,6 +559,32 @@ internal class WalletModel @Inject constructor(
)
}
private fun reinitializeWallets(action: WalletsUpdateActionResolver.Action.ReinitializeWallets) {
action.wallets.forEach { userWallet ->
walletScreenContentLoader.cancel(userWallet.walletId)
tokenListStore.remove(userWallet.walletId)
walletScreenContentLoader.load(
userWallet = userWallet,
clickIntents = clickIntents,
coroutineScope = modelScope,
)
modelScope.launch(dispatchers.main) {
fetchWalletContent(userWallet = userWallet)
}
stateHolder.update(
ReinitializeWalletTransformer(
prevWalletId = userWallet.walletId,
newUserWallet = userWallet,
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
),
)
}
}
private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
if (accountsFeatureToggles.isFeatureEnabled) {
fetchWalletContent(userWallet = action.selectedWallet)

View file

@ -64,8 +64,11 @@ internal class WalletsUpdateActionResolver @Inject constructor(
selectedWallet: UserWallet,
): Action {
return when {
isHotWalletUpgraded(state, wallets) -> {
getHotWalletsUpgradedAction(state, wallets)
isAnyHotWalletUpgraded(state, wallets) -> {
getHotWalletsUpgradedAction(state, wallets, selectedWallet)
}
isAnyHotWalletBackedUpChange(state, wallets) -> {
getHotWalletsBackedUpAction(state, wallets)
}
isWalletsCountChanged(state, wallets) -> {
getChangeWalletsListAction(state, wallets, selectedWallet)
@ -79,31 +82,35 @@ internal class WalletsUpdateActionResolver @Inject constructor(
isAnyWalletNameChanged(state, wallets) -> {
getRenameWalletsAction(state, wallets)
}
isAnyHotWalletBackedUpChange(state, wallets) -> {
getHotWalletsBackedUpAction(state, wallets)
isAnyWalletUnlocked(state, wallets) -> {
Action.UnlockWallet(
selectedWallet = selectedWallet,
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
)
}
else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet)
isSelectedWalletCardsCountChanged(state, selectedWallet) -> {
Action.UpdateWalletCardCount(selectedWallet)
}
else -> Action.Unknown
}
}
private fun isAnyHotWalletBackedUpChange(state: WalletScreenState, wallets: List<UserWallet>): Boolean {
val incompleteActivationWalletIds = state.incompleteActivationWalletIds()
val walletsToUpdate = wallets.filter {
it is UserWallet.Hot && it.backedUp == incompleteActivationWalletIds.contains(it.walletId)
return wallets.any {
it is UserWallet.Hot && it.backedUp && incompleteActivationWalletIds.contains(it.walletId)
}
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
private fun isAnyHotWalletUpgraded(state: WalletScreenState, wallets: List<UserWallet>): Boolean {
return state.wallets.any { walletState ->
when (walletState) {
is WalletState.MultiCurrency -> {
val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id }
walletState.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold
}
else -> false
}
else -> false
}
}
@ -189,9 +196,15 @@ internal class WalletsUpdateActionResolver @Inject constructor(
private fun getHotWalletsUpgradedAction(
state: WalletScreenState,
wallets: List<UserWallet>,
): Action.ReloadWallets {
val walletsToUpdate = wallets.filter { it.walletId == state.getPrevSelectedWallet().id }
return Action.ReloadWallets(walletsToUpdate)
selectedWallet: UserWallet,
): Action.ReinitializeWallets {
val walletsToUpdate = wallets.filter { wallet ->
val previousState = state.wallets.firstOrNull { it.walletCardState.id == wallet.walletId }
?: return@filter false
wallet is UserWallet.Cold && previousState is WalletState.MultiCurrency &&
previousState.type == WalletState.MultiCurrency.WalletType.Hot
}
return Action.ReinitializeWallets(selectedWallet, walletsToUpdate)
}
private fun getRenameWalletsAction(state: WalletScreenState, wallets: List<UserWallet>): Action.RenameWallets {
@ -205,29 +218,14 @@ internal class WalletsUpdateActionResolver @Inject constructor(
)
}
private fun getUpdateSelectedWalletAction(
state: WalletScreenState,
wallets: List<UserWallet>,
selectedWallet: UserWallet,
): Action {
return when {
isSelectedWalletUnlocked(state, selectedWallet) -> {
Action.UnlockWallet(
selectedWallet = selectedWallet,
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
)
}
isSelectedWalletCardsCountChanged(state, selectedWallet) -> {
Action.UpdateWalletCardCount(selectedWallet)
}
else -> Action.Unknown
private fun isAnyWalletUnlocked(state: WalletScreenState, wallets: List<UserWallet>): Boolean {
return state.wallets.any { walletState ->
val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } ?: return@any false
!wallet.isLocked &&
(walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked)
}
}
private fun isSelectedWalletUnlocked(state: WalletScreenState, selectedWallet: UserWallet): Boolean {
return state.isSelectedWalletLocked() && !selectedWallet.isLocked
}
private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean {
if (selectedWallet !is UserWallet.Cold) return false
val prevSelectedWallet = state.getPrevSelectedWallet()
@ -235,12 +233,6 @@ internal class WalletsUpdateActionResolver @Inject constructor(
prevSelectedWallet.cardCount != selectedWallet.getCardsCount()
}
private fun WalletScreenState.isSelectedWalletLocked(): Boolean {
val selectedWalletState = wallets.getOrNull(selectedWalletIndex) ?: error("Selected wallet is not found")
return selectedWalletState is WalletState.MultiCurrency.Locked ||
selectedWalletState is WalletState.SingleCurrency.Locked
}
private fun WalletScreenState.getPrevSelectedWallet(): WalletCardState {
return wallets
.map(WalletState::walletCardState)
@ -249,9 +241,12 @@ internal class WalletsUpdateActionResolver @Inject constructor(
}
private fun WalletScreenState.incompleteActivationWalletIds(): List<UserWalletId> {
return wallets.mapNotNull {
if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) {
it.walletCardState.id
return wallets.mapNotNull { wallet ->
if (wallet.warnings.any { it is WalletNotification.FinishWalletActivation } ||
wallet.walletCardState is WalletState.MultiCurrency &&
wallet.walletCardState.additionalInfo?.isHotBackedUp == false
) {
wallet.walletCardState.id
} else {
null
}
@ -306,6 +301,19 @@ internal class WalletsUpdateActionResolver @Inject constructor(
}
}
/**
* Reinitialize wallets
*/
data class ReinitializeWallets(
val selectedWallet: UserWallet,
val wallets: List<UserWallet>,
) : Action() {
override fun toString(): String {
return "ReinitializeWallets(wallets = ${wallets.joinToString { it.walletId.toString() }}"
}
}
/**
* Rename wallets
*

View file

@ -51,7 +51,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor(
}
override fun onBalancesAndLimitsClick() {
analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked)
analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked())
modelScope.launch(dispatchers.main) {
val userWalletId = stateController.getSelectedWalletId()
val balancesAndLimits = getVisaCurrencyUseCase(userWalletId)
@ -92,7 +92,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor(
}
override fun onExploreClick(exploreUrl: String) {
analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore)
analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore())
router.openUrl(exploreUrl)
}

View file

@ -54,7 +54,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
) : BaseWalletClickIntents(), WalletCardClickIntents {
override fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) {
analyticsEventHandler.send(MainScreen.EditWalletTapped)
analyticsEventHandler.send(MainScreen.EditWalletTapped())
router.dialogNavigation.activate(
configuration = WalletDialogConfig.RenameWallet(
@ -65,7 +65,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
}
override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) {
analyticsEventHandler.send(MainScreen.DeleteWalletTapped)
analyticsEventHandler.send(MainScreen.DeleteWalletTapped())
walletEventSender.send(
event = WalletEvent.ShowAlert(

View file

@ -4,11 +4,12 @@ import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.tokens.TokenItemStateConverter.ApySource
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
@ -205,11 +206,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
override fun onAccountExpandClick(account: Account) {
val userWalletId = stateHolder.getSelectedWalletId()
analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountShowTokens())
accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId)
}
override fun onAccountCollapseClick(account: Account) {
val userWalletId = stateHolder.getSelectedWalletId()
analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountHideTokens())
accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId)
}
@ -260,7 +263,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
token = currencyStatus.currency.symbol,
blockchain = currencyStatus.currency.network.name,
action = "Staking",
state = if (currencyStatus.value.yieldBalance is YieldBalance.Data) {
state = if (currencyStatus.value.stakingBalance is StakingBalance.Data) {
"Enabled"
} else {
"Disabled"
@ -364,7 +367,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
is WalletNFTItemUM.Content -> {
analyticsEventHandler.send(
NFTAnalyticsEvent.NFTListScreenOpened(
state = NFTAnalyticsEvent.NFTListScreenOpened.State.Full,
state = AnalyticsParam.EmptyFull.Full,
allAssetsCount = state.allAssetsCount,
collectionsCount = state.collectionsCount,
noCollectionAssetsCount = state.noCollectionAssetsCount,
@ -374,7 +377,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
is WalletNFTItemUM.Empty -> {
analyticsEventHandler.send(
NFTAnalyticsEvent.NFTListScreenOpened(
state = NFTAnalyticsEvent.NFTListScreenOpened.State.Empty,
state = AnalyticsParam.EmptyFull.Empty,
allAssetsCount = 0,
collectionsCount = 0,
noCollectionAssetsCount = 0,

View file

@ -11,9 +11,7 @@ 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.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
@ -33,14 +31,13 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
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.*
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
@ -106,7 +103,7 @@ internal interface WalletWarningsClickIntents {
fun onDenyPermissions()
fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean)
fun onFinishWalletActivationClick(isBackupExists: Boolean)
}
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ -131,7 +128,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val urlOpener: UrlOpener,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val appRouter: AppRouter,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
@ -144,7 +141,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
override fun onAddBackupCardClick() {
analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped)
analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped())
prepareAndStartOnboardingProcess()
}
@ -172,7 +169,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>) {
analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main))
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped)
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped())
modelScope.launch {
val userWallet = getSelectedUserWallet() ?: return@launch
@ -190,7 +187,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
override fun onOpenUnlockWalletsBottomSheetClick() {
analyticsEventHandler.send(MainScreen.WalletUnlockTapped)
analyticsEventHandler.send(MainScreen.WalletUnlockTapped())
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
@ -202,6 +199,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
error.handle(
onAlreadyUnlocked = {},
onUserCancelled = {},
analyticsEventHandler = analyticsEventHandler,
showMessage = uiMessageSender::send,
)
}
@ -221,7 +219,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
@Deprecated("Will be removed with hot wallet release")
override fun onUnlockWalletClick() {
analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics)
analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics())
modelScope.launch(dispatchers.main) {
unlockWalletsUseCase(type = UnlockType.ALL_WITHOUT_SELECT)
@ -249,7 +247,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
@Deprecated("Will be removed with hot wallet release")
override fun onScanToUnlockWalletClick() {
analyticsEventHandler.send(MainScreen.UnlockWithCardScan)
analyticsEventHandler.send(MainScreen.UnlockWithCardScan())
openScanCardDialog()
}
@ -307,22 +305,22 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onClosePromoClick(promoId: PromoId) {
analyticsEventHandler.send(
when (promoId) {
PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss
PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss()
PromoId.Sepa -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Sepa,
action = PromotionBannerClicked.BannerAction.Closed,
action = PromotionBannerClicked.BannerAction.Closed(),
)
PromoId.VisaPresale -> PromoAnalyticsEvent.VisaWaitlistPromoDismiss
PromoId.VisaPresale -> PromoAnalyticsEvent.VisaWaitlistPromoDismiss()
PromoId.BlackFriday -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.BlackFriday,
action = PromotionBannerClicked.BannerAction.Closed,
action = PromotionBannerClicked.BannerAction.Closed(),
)
PromoId.OnePlusOne -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
action = PromotionBannerClicked.BannerAction.Closed,
action = PromotionBannerClicked.BannerAction.Closed(),
)
},
@ -336,7 +334,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
val userWallet = getSelectedUserWallet() ?: return
when (promoId) {
PromoId.Referral -> {
analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate)
analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate())
appRouter.push(ReferralProgram(userWalletId = userWallet.walletId))
}
PromoId.Sepa -> {
@ -344,7 +342,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Sepa,
action = PromotionBannerClicked.BannerAction.Clicked,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
cryptoCurrency ?: return
@ -358,7 +356,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
)
}
PromoId.VisaPresale -> {
analyticsEventHandler.send(PromoAnalyticsEvent.VisaWaitlistPromoJoin)
analyticsEventHandler.send(PromoAnalyticsEvent.VisaWaitlistPromoJoin())
urlOpener.openUrl(VISA_PROMO_LINK)
}
PromoId.BlackFriday -> {
@ -366,7 +364,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.BlackFriday,
action = PromotionBannerClicked.BannerAction.Clicked,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
urlOpener.openUrl(BLACK_FRIDAY_PROMO_LINK)
@ -376,7 +374,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
action = PromotionBannerClicked.BannerAction.Clicked,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
urlOpener.openUrl(ONE_PLUS_ONE_PROMO_LINK)
@ -394,7 +392,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
override fun onNoteMigrationButtonClick(url: String) {
analyticsEventHandler.send(MainScreen.NotePromoButton)
analyticsEventHandler.send(MainScreen.NotePromoButton())
modelScope.launch(dispatchers.main) {
router.openUrl(url)
}
@ -403,7 +401,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onSeedPhraseNotificationConfirm() {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes)
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes())
walletEventSender.send(
event = WalletEvent.ShowAlert(
@ -426,7 +424,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onSeedPhraseNotificationDecline() {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo)
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo())
walletEventSender.send(
event = WalletEvent.ShowAlert(
@ -445,7 +443,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onSeedPhraseSecondNotificationAccept() {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed)
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed())
walletEventSender.send(
event = WalletEvent.ShowAlert(
@ -468,51 +466,21 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onSeedPhraseSecondNotificationReject() {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined)
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined())
modelScope.launch {
seedPhraseNotificationUseCase.rejectSecond(userWalletId = userWallet.walletId)
}
}
override fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean) {
when (bannerType) {
WalletActivationBannerType.Attention -> {
val userWallet = getSelectedUserWallet() ?: return
appRouter.push(WalletActivation(userWallet.walletId, isBackupExists))
}
WalletActivationBannerType.Warning -> {
val message = bottomSheetMessage {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUMV2.Icon.Type.Warning
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_activation_need_title)
body = resourceReference(R.string.hw_activation_need_description)
}
secondaryButton {
text = resourceReference(R.string.common_later)
onClick {
closeBs()
}
}
primaryButton {
text = resourceReference(R.string.hw_activation_need_backup)
onClick {
val userWallet = getSelectedUserWallet() ?: return@onClick
appRouter.push(WalletActivation(userWallet.walletId, isBackupExists))
closeBs()
}
}
}
uiMessageSender.send(message)
}
}
override fun onFinishWalletActivationClick(isBackupExists: Boolean) {
analyticsEventHandler.send(MainScreen.ButtonFinalizeActivation())
val userWalletId = stateHolder.getSelectedWalletId()
appRouter.push(WalletActivation(userWalletId, isBackupExists))
}
override fun onAllowPermissions() {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonAllowPush)
analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonAllowPush())
walletEventSender.send(
event = WalletEvent.RequestPushPermissions(
onAllow = {
@ -538,7 +506,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onDenyPermissions() {
modelScope.launch {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonLaterPush)
analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonLaterPush())
updateSubscribeOnPushPermissions(false)
}
}
@ -577,8 +545,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
multiYieldBalanceFetcher(
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
multiStakingBalanceFetcher(
params = MultiStakingBalanceFetcher.Params(
userWalletId = userWalletId,
stakingIds = stakingIds,
),
)
.onLeft { Timber.e("Unable to fetch yield balances: $it") }
},

View file

@ -49,7 +49,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
) : PromoDeeplinkHandler {
init {
analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart)
analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart())
val promoCode = queryParams[PROMO_CODE_KEY].orEmpty()
if (promoCode.isEmpty()) {
showAlert(InvalidPromoCode)

View file

@ -8,7 +8,7 @@ sealed class PromoActivationAnalytics(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
data object PromoDeepLinkActivationStart : PromoActivationAnalytics(
class PromoDeepLinkActivationStart : PromoActivationAnalytics(
event = "Bitcoin Promo Deep Link Activation",
params = emptyMap(),
)

View file

@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.common.preview
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.components.token.AccountItemPreviewData
import com.tangem.core.ui.components.token.state.TokenItemState
@ -14,6 +13,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.*
@ -167,11 +167,12 @@ internal object WalletScreenPreviewData {
warnings = persistentListOf(
WalletNotification.Warning.SomeNetworksUnreachable,
WalletNotification.FinishWalletActivation(
iconTint = NotificationConfig.IconTint.Attention,
type = WalletActivationBannerType.Attention,
buttonsState = ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = { },
),
isBackupExists = false,
),
),
bottomSheetConfig = null,
@ -222,6 +223,7 @@ internal object WalletScreenPreviewData {
isHidingMode = false,
showMarketsOnboarding = false,
onDismissMarketsOnboarding = {},
isNewMarketEnabled = false,
)
internal val accountScreenState =

View file

@ -8,11 +8,11 @@ sealed class PortfolioOrganizeTokensAnalyticsEvent(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Portfolio / Organize Tokens", event, params) {
object ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened")
class ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened")
object ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance")
class ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance")
object Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group")
class Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group")
class Apply(
grouping: AnalyticsParam.OnOffState,
@ -25,5 +25,5 @@ sealed class PortfolioOrganizeTokensAnalyticsEvent(
),
)
object Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel")
class Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel")
}

View file

@ -8,7 +8,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
@ -63,12 +63,12 @@ internal class CryptoCurrencyToDraggableItemConverter(
}
private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String {
val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data
val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data
val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO
val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId)
val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId)
?.multiply(fiatRate).orZero()
val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) }
return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) }
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
@ -56,12 +56,12 @@ internal class CryptoCurrencyToDraggableItemConverterV2(
}
private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String {
val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data
val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data
val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO
val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId)
val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId)
?.multiply(fiatRate).orZero()
val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) }
return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) }
}
}

View file

@ -37,7 +37,7 @@ sealed class WalletScreenAnalyticsEvent {
},
)
class TokenBalance(balance: AnalyticsParam.TokenBalanceState, token: String) : Basic(
class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic(
event = "Token Balance",
params = mapOf(
AnalyticsParam.STATE to balance.value,
@ -51,7 +51,40 @@ sealed class WalletScreenAnalyticsEvent {
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
data object ScreenOpened : MainScreen(event = "Screen opened")
class ScreenOpenedLegacy : MainScreen(
event = "Screen opened",
)
data class ScreenOpened(
private val hasMobileWallet: Boolean,
private val accountsCount: Int?,
) : MainScreen(
event = "Screen opened",
params = buildMap {
put("Mobile Wallet", if (hasMobileWallet) "Yes" else "No")
if (accountsCount != null) put("Accounts Count", accountsCount.toString())
},
)
data class NoticeFinishActivation(
private val activationState: ActivationState,
private val balanceState: AnalyticsParam.EmptyFull,
) : MainScreen(
event = "Notice - Finish Activation",
params = mapOf(
"Activation State" to activationState.value,
"Balance State" to balanceState.value,
),
) {
enum class ActivationState(val value: String) {
NotStarted("Not Started"),
Unfinished("Unfinished"),
}
}
class ButtonFinalizeActivation : MainScreen(
event = "Button - Finalize Activation",
)
class WalletSelected(val isImported: Boolean) : MainScreen(
event = "Wallet Selected",
@ -70,9 +103,9 @@ sealed class WalletScreenAnalyticsEvent {
params = mapOf("Result" to result.value),
)
data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
class NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
class NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
class WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
class NetworksUnreachable(
tokens: List<String>,
@ -81,54 +114,54 @@ sealed class WalletScreenAnalyticsEvent {
params = mapOf("Tokens" to tokens.joinToString()),
)
data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
class MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
class CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
class HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
class ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
data object TestnetCard : MainScreen(event = "Notice - Testnet Card")
class TestnetCard : MainScreen(event = "Notice - Testnet Card")
data object DemoCard : MainScreen(event = "Notice - Demo Card")
class DemoCard : MainScreen(event = "Notice - Demo Card")
data object DevelopmentCard : MainScreen(event = "Notice - Development Card")
class DevelopmentCard : MainScreen(event = "Notice - Development Card")
data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
class WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
class BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
data object BackupError : MainScreen(event = "Notice - Backup Error")
class BackupError : MainScreen(event = "Notice - Backup Error")
data object NotePromo : MainScreen(event = "Notice - Note Promo")
class NotePromo : MainScreen(event = "Notice - Note Promo")
data object NotePromoButton : MainScreen(event = "Note Promo Button")
class NotePromoButton : MainScreen(event = "Note Promo Button")
data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
class UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
class UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
class EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
class DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
data object NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support")
class NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support")
data object NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2")
class NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2")
data object NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No")
class NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No")
data object NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes")
class NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes")
data object NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used")
class NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used")
data object NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined")
class NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined")
// region Referral Promo
data object ReferralPromo : MainScreen(event = "Referral Banner")
data object ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate")
data object ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss")
class ReferralPromo : MainScreen(event = "Referral Banner")
class ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate")
class ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss")
//endregion
}
@ -137,8 +170,8 @@ sealed class WalletScreenAnalyticsEvent {
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")
class PushBanner : PushBannerPromo(event = "Push Banner")
class ButtonAllowPush : PushBannerPromo(event = "Button - Allow Push")
class ButtonLaterPush : PushBannerPromo(event = "Button - Later Push")
}
}

View file

@ -28,7 +28,7 @@ internal class SelectedWalletAnalyticsSender @Inject constructor(
* that cannot be processed in [WalletWarningsAnalyticsSender].
* */
private fun getEvent(userWallet: UserWallet): AnalyticsEvent? = when {
userWallet.isLocked -> WalletScreenAnalyticsEvent.MainScreen.WalletUnlock
userWallet.isLocked -> WalletScreenAnalyticsEvent.MainScreen.WalletUnlock()
else -> WalletScreenAnalyticsEvent.MainScreen.WalletSelected(userWallet.isImported())
}

View file

@ -156,9 +156,9 @@ internal class TokenListAnalyticsSender @Inject constructor(
-> {
if (balanceWasSentMap[blockchain.currency] != true) {
val tokenBalance = if (balanceStatus.amount.isZero()) {
AnalyticsParam.TokenBalanceState.Empty
AnalyticsParam.EmptyFull.Empty
} else {
AnalyticsParam.TokenBalanceState.Full
AnalyticsParam.EmptyFull.Full
}
analyticsEventHandler.send(
Basic.TokenBalance(

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
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.feature.wallet.child.wallet.model.WalletActivationBannerType
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
@ -39,16 +40,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
@Suppress("CyclomaticComplexMethod")
private fun getEvent(warning: WalletNotification): AnalyticsEvent? {
return when (warning) {
is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard
is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard
is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet
is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions
is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard
is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
is WalletNotification.NoteMigration -> MainScreen.NotePromo
is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard()
is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard()
is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet()
is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions()
is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard()
is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard()
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses()
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem()
is WalletNotification.Critical.BackupError -> MainScreen.BackupError()
is WalletNotification.NoteMigration -> MainScreen.NotePromo()
is WalletNotification.SwapPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Empty, // Use it on new promo action
@ -65,8 +66,8 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
)
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo
is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo()
is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo()
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
is WalletNotification.Informational.NoAccount,
is WalletNotification.Warning.LowSignatures,
@ -74,12 +75,26 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.Warning.NetworksUnreachable,
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
is WalletNotification.FinishWalletActivation -> {
val activationState = if (warning.isBackupExists) {
MainScreen.NoticeFinishActivation.ActivationState.Unfinished
} else {
MainScreen.NoticeFinishActivation.ActivationState.NotStarted
}
val balanceState = when (warning.type) {
WalletActivationBannerType.Attention -> AnalyticsParam.EmptyFull.Empty
WalletActivationBannerType.Warning -> AnalyticsParam.EmptyFull.Full
}
MainScreen.NoticeFinishActivation(
activationState = activationState,
balanceState = balanceState,
)
}
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport()
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond()
is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner()
is WalletNotification.Warning.TangemPayRefreshNeeded -> null
WalletNotification.Warning.TangemPayUnreachable -> null
}

View file

@ -1,8 +1,24 @@
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import arrow.atomic.AtomicBoolean
import com.tangem.common.routing.AppRoute.WalletActivation
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.primaryButton
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
@ -12,8 +28,13 @@ import javax.inject.Inject
internal class WalletWarningsSingleEventSender @Inject constructor(
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val screenLifecycleProvider: ScreenLifecycleProvider,
private val uiMessageSender: UiMessageSender,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val router: Router,
) {
private val isActivationBottomSheetShown: AtomicBoolean = AtomicBoolean(false)
suspend fun send(
userWalletId: UserWalletId,
displayedUiState: WalletState?,
@ -26,9 +47,49 @@ internal class WalletWarningsSingleEventSender @Inject constructor(
val events = newWarnings.filter { it !in displayedUiState.warnings }
events.forEach { event ->
if (event is WalletNotification.Critical.SeedPhraseNotification) {
seedPhraseNotificationUseCase.notified(userWalletId = userWalletId)
when (event) {
is WalletNotification.Critical.SeedPhraseNotification -> {
seedPhraseNotificationUseCase.notified(userWalletId = userWalletId)
}
is WalletNotification.FinishWalletActivation -> {
if (event.type == WalletActivationBannerType.Warning && !isActivationBottomSheetShown.get()) {
showFinishActivationBottomSheet(userWalletId)
}
isActivationBottomSheetShown.set(true)
}
else -> Unit
}
}
}
private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return
if (userWallet !is UserWallet.Hot) return
val message = bottomSheetMessage {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUMV2.Icon.Type.Warning
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_activation_need_title)
body = resourceReference(R.string.hw_activation_need_description)
}
secondaryButton {
text = resourceReference(R.string.common_later)
onClick {
closeBs()
}
}
primaryButton {
text = resourceReference(R.string.hw_activation_need_backup)
onClick {
router.push(WalletActivation(userWallet.walletId, userWallet.backedUp))
closeBs()
}
}
}
uiMessageSender.send(message)
}
}

View file

@ -6,7 +6,6 @@ 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
@ -36,6 +35,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificat
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
@ -59,7 +59,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
) {
@Suppress("UNCHECKED_CAST", "MagicNumber")
@Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod")
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
@ -115,7 +115,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addFinishWalletActivationNotification(
userWallet = userWallet,
totalFiatBalance = totalFiatBalance,
flattenCurrencies = flattenCurrencies,
clickIntents = clickIntents,
shouldAccessCodeSkipped = shouldAccessCodeSkipped,
)
@ -361,8 +361,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun TotalFiatBalance?.getFinishWalletActivationType(): WalletActivationBannerType {
return if ((this as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) {
private fun List<CryptoCurrencyStatus>?.getFinishWalletActivationType(): WalletActivationBannerType {
return if (this?.any { it.value.amount.orZero().isPositive() } == true) {
WalletActivationBannerType.Warning
} else {
WalletActivationBannerType.Attention
@ -371,7 +371,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addFinishWalletActivationNotification(
userWallet: UserWallet,
totalFiatBalance: Lce<TokenListError, TotalFiatBalance>,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
shouldAccessCodeSkipped: Boolean,
) {
@ -382,30 +382,26 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
!shouldAccessCodeSkipped
val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired
val type = totalFiatBalance.fold(
ifLoading = { it.getFinishWalletActivationType() },
val type = flattenCurrencies.fold(
ifLoading = { return },
ifContent = { it.getFinishWalletActivationType() },
ifError = { WalletActivationBannerType.Attention },
)
val tint = when (type) {
WalletActivationBannerType.Attention -> IconTint.Attention
WalletActivationBannerType.Warning -> IconTint.Warning
}
addIf(
element = WalletNotification.FinishWalletActivation(
iconTint = tint,
type = type,
buttonsState = when (type) {
WalletActivationBannerType.Warning -> ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) },
onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) },
)
else -> ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.hw_activation_need_finish),
onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) },
onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) },
)
},
isBackupExists = isBackupExists,
),
condition = shouldShowFinishActivation,
)

View file

@ -50,6 +50,7 @@ internal object WalletAdditionalInfoFactory {
backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup)
else -> TextReference.Str("")
},
isHotBackedUp = backedUp,
)
}

View file

@ -110,6 +110,7 @@ internal class WalletStateController @Inject constructor() {
isHidingMode = false,
showMarketsOnboarding = false,
onDismissMarketsOnboarding = {},
isNewMarketEnabled = false,
)
}
}

View file

@ -7,4 +7,5 @@ import com.tangem.core.ui.extensions.TextReference
data class WalletAdditionalInfo(
val hideable: Boolean,
val content: TextReference,
val isHotBackedUp: Boolean = false,
)

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.impl.R
import org.joda.time.DateTime
@ -287,14 +288,22 @@ sealed class WalletNotification(val config: NotificationConfig) {
)
data class FinishWalletActivation(
val iconTint: IconTint,
val type: WalletActivationBannerType,
val buttonsState: ButtonsState,
val isBackupExists: Boolean,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.hw_activation_need_title),
subtitle = resourceReference(R.string.hw_activation_need_description),
subtitle = if (isBackupExists) {
resourceReference(R.string.hw_activation_need_warning_description)
} else {
resourceReference(R.string.hw_activation_need_description)
},
iconResId = R.drawable.img_knight_shield_32,
iconTint = iconTint,
iconTint = when (type) {
WalletActivationBannerType.Attention -> IconTint.Attention
WalletActivationBannerType.Warning -> IconTint.Warning
},
buttonsState = buttonsState,
),
)

View file

@ -13,5 +13,6 @@ internal data class WalletScreenState(
val event: StateEvent<WalletEvent>,
val isHidingMode: Boolean,
val showMarketsOnboarding: Boolean,
val isNewMarketEnabled: Boolean,
val onDismissMarketsOnboarding: () -> Unit,
)

View file

@ -137,7 +137,7 @@ internal class SetVisaInfoTransformer(
fiatAmount = visaCurrency.balances.available.multiply(visaCurrency.fiatRate),
fiatRate = visaCurrency.fiatRate,
priceChange = visaCurrency.priceChange,
yieldBalance = null,
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
@ -154,7 +154,7 @@ internal class SetVisaInfoTransformer(
clickIntents.onReceiveClick(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
event = MainScreenAnalyticsEvent.ButtonReceive,
event = MainScreenAnalyticsEvent.ButtonReceive(),
)
},
onLongClick = {

View file

@ -44,12 +44,12 @@ internal class BalancesAndLimitsBottomSheetConverter(
}
private fun balanceInfoOnClick() {
analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo)
analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo())
eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo))
}
private fun limitInfoOnClick(totalLimit: String, otherLimit: String) {
analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo)
analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo())
eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit)))
}
}

View file

@ -82,9 +82,9 @@ internal class SingleWalletOnrampTransactionConverter(
fallbackResId = R.drawable.ic_currency_24,
),
iconState = getIconState(value.status),
onGoToProviderClick = {
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider)
clickIntents.onGoToProviderClick(it)
onGoToProviderClick = { url ->
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider())
clickIntents.onGoToProviderClick(url)
},
onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus,
onClick = {

View file

@ -51,7 +51,6 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
shouldShowMainPromo: Boolean = false,
) {
val accountFlattenCurrencies = accountList.flattenCurrencies()
val mainAccount = accountList.mainAccount
when {
@ -73,26 +72,14 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
)
}
isAccountMode -> {
val isAllAccountsEmpty = accountFlattenCurrencies.isEmpty()
if (isAllAccountsEmpty) {
stateController.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = TokenListError.EmptyTokens,
appCurrency = appCurrency,
),
)
} else {
val convertParams = TokenConverterParams.Account(accountList, expandedAccounts)
updateContent(
params = convertParams,
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
val convertParams = TokenConverterParams.Account(accountList, expandedAccounts)
updateContent(
params = convertParams,
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
}
}

View file

@ -51,6 +51,7 @@ import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.components.rememberIsKeyboardVisible
import com.tangem.core.ui.components.sheetscaffold.*
@ -87,15 +88,18 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPa
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
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.MarketsEntryComponent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent) {
internal fun WalletScreen(
state: WalletScreenState,
bottomSheetContent: @Composable (() -> Unit),
bottomSheetHeaderHeightProvider: () -> Dp,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
) {
// It means that screen is still initializing
if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return
@ -116,8 +120,10 @@ internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: Marke
snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll,
onAutoScrollReset = { isAutoScroll.value = false },
marketsEntryComponent = marketsEntryComponent,
bottomSheetContent = bottomSheetContent,
alertConfig = alertConfig,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
onBottomSheetStateChange = onBottomSheetStateChange,
)
WalletEventEffect(
@ -136,9 +142,11 @@ private fun WalletContent(
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>,
marketsEntryComponent: MarketsEntryComponent,
alertConfig: WalletAlertState?,
onAutoScrollReset: () -> Unit,
bottomSheetHeaderHeightProvider: () -> Dp,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
bottomSheetContent: @Composable (() -> Unit),
) {
/*
* Don't pass key to remember, because it will brake scroll animation.
@ -286,25 +294,15 @@ private fun WalletContent(
)
}
val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) }
var headerSize by remember { mutableStateOf(0.dp) }
BaseScaffoldWithMarkets(
state = state,
listState = listState,
selectedWallet = selectedWallet,
snackbarHostState = snackbarHostState,
bottomSheetHeaderHeightProvider = { headerSize },
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
alertConfig = alertConfig,
onBottomSheetStateChange = { bottomSheetState.value = it },
bottomSheetContent = {
marketsEntryComponent.BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = { headerSize = it },
modifier = Modifier,
)
},
onBottomSheetStateChange = onBottomSheetStateChange,
bottomSheetContent = bottomSheetContent,
content = scaffoldContent,
)
}
@ -394,7 +392,11 @@ private inline fun BaseScaffoldWithMarkets(
val maxHeight = LocalWindowSize.current.height
val coroutineScope = rememberCoroutineScope()
val backgroundPrimary = TangemTheme.colors.background.primary
val background = if (state.isNewMarketEnabled) {
TangemTheme.colors.background.tertiary
} else {
TangemTheme.colors.background.primary
}
val showMarketsHint by remember {
derivedStateOf {
@ -407,7 +409,7 @@ private inline fun BaseScaffoldWithMarkets(
}
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) },
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
val backgroundColor = LocalMainBottomSheetColor.current
var isSearchFieldFocused by remember { mutableStateOf(false) }
@ -798,16 +800,11 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
TangemThemePreview {
WalletScreen(
state = data,
marketsEntryComponent = object : MarketsEntryComponent {
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
Text("Markets Content")
}
bottomSheetContent = {
Text("Markets Content")
},
bottomSheetHeaderHeightProvider = { 10.dp },
onBottomSheetStateChange = {},
)
}
}

View file

@ -151,7 +151,7 @@ class YieldSupplyPromoBannerKeyConverterTest {
fiatAmount = null,
fiatRate = null,
priceChange = null,
yieldBalance = null,
stakingBalance = null,
yieldSupplyStatus = if (isYieldActive) {
YieldSupplyStatus(
isActive = true,

View file

@ -115,7 +115,9 @@ class DefaultPromoDeeplinkHandlerTest {
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated),
@ -143,7 +145,9 @@ class DefaultPromoDeeplinkHandlerTest {
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode),
@ -172,7 +176,9 @@ class DefaultPromoDeeplinkHandlerTest {
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed),
@ -209,7 +215,9 @@ class DefaultPromoDeeplinkHandlerTest {
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
@ -248,7 +256,9 @@ class DefaultPromoDeeplinkHandlerTest {
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated),
@ -264,7 +274,9 @@ class DefaultPromoDeeplinkHandlerTest {
expectedMessage = resourceReference(R.string.bitcoin_promo_invalid_code),
)
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode),
@ -280,7 +292,9 @@ class DefaultPromoDeeplinkHandlerTest {
expectedMessage = resourceReference(R.string.bitcoin_promo_activation_error),
)
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed),
@ -296,7 +310,9 @@ class DefaultPromoDeeplinkHandlerTest {
expectedMessage = resourceReference(R.string.bitcoin_promo_no_address),
)
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
@ -312,7 +328,9 @@ class DefaultPromoDeeplinkHandlerTest {
expectedMessage = resourceReference(R.string.bitcoin_promo_already_activated),
)
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.PromoCodeAlreadyUsed),
@ -425,7 +443,9 @@ class DefaultPromoDeeplinkHandlerTest {
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
@ -539,7 +559,9 @@ class DefaultPromoDeeplinkHandlerTest {
coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) }
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated),
@ -594,7 +616,9 @@ class DefaultPromoDeeplinkHandlerTest {
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
@ -649,7 +673,9 @@ class DefaultPromoDeeplinkHandlerTest {
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
@ -700,7 +726,9 @@ class DefaultPromoDeeplinkHandlerTest {
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
@ -751,7 +779,9 @@ class DefaultPromoDeeplinkHandlerTest {
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(
exactly = 1,
) { analyticsEventHandler.send(ofType<PromoActivationAnalytics.PromoDeepLinkActivationStart>()) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),