Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-17 14:05:58 +03:00
commit 3cbc2dadfb
822 changed files with 12838 additions and 5366 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,18 +7,17 @@ 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.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
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.isImported
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.models.wallet.*
import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
@ -43,6 +42,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
@ -100,6 +100,10 @@ internal class WalletModel @Inject constructor(
private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val getAppThemeModeUseCase: GetAppThemeModeUseCase,
private val trackingContextProxy: TrackingContextProxy,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val feedFeatureToggle: FeedFeatureToggle,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
) : Model() {
@ -117,13 +121,7 @@ internal class WalletModel @Inject constructor(
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
init {
screenLifecycleProvider.isBackgroundState
.onEach { isBackground ->
if (isBackground.not()) {
suggestToEnableBiometrics()
}
}.launchIn(modelScope)
updateMarketToggle()
suggestToOpenMarkets()
maybeMigrateNames()
@ -140,6 +138,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) {
@ -195,7 +205,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 {
@ -278,14 +287,39 @@ internal class WalletModel @Inject constructor(
.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
}
val result = getAppThemeModeUseCase().firstOrNull()
val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(
hasMobileWallet = hasMobileWallet,
accountsCount = accountsCount,
theme = theme.value,
isImported = selectedWallet.isImported(),
),
)
}
}
if (selectedWallet.isMultiCurrency) {
selectedWalletAnalyticsSender.send(selectedWallet)
}
subscribeOnExpressTransactionsUpdates(selectedWallet)
observeAndClearNFTCacheIfNeedUseCase(selectedWallet)
sendMainScreenOpenedAnalytics(selectedWallet.isImported())
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
@ -420,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)
@ -524,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)
@ -695,17 +756,6 @@ internal class WalletModel @Inject constructor(
}
}
private suspend fun sendMainScreenOpenedAnalytics(isImported: Boolean) {
val result = getAppThemeModeUseCase().firstOrNull()
val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(
theme = theme.value,
isImported = isImported,
),
)
}
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))

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

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.child.wallet.model.intents
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.res.R
@ -14,6 +15,7 @@ import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer
import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -29,6 +31,10 @@ internal interface TangemPayIntents {
fun onIssuingFailedClicked()
fun onPaySupportClick()
fun onOnboardingBannerClick(userWalletId: UserWalletId)
fun onOnboardingBannerCloseClick(userWalletId: UserWalletId)
}
@Suppress("LongParameterList")
@ -115,4 +121,15 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
)
}
}
override fun onOnboardingBannerClick(userWalletId: UserWalletId) {
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId))
}
override fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) {
modelScope.launch {
stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId))
onboardingRepository.setHideMainOnboardingBanner(userWalletId)
}
}
}

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

@ -111,14 +111,8 @@ internal class DefaultWalletRouter @Inject constructor(
)
}
override fun openTangemPayOnboarding(userWalletId: UserWalletId) {
router.push(
AppRoute.TangemPayOnboarding(
AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(
userWalletId = userWalletId,
),
),
)
override fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode) {
router.push(route = AppRoute.TangemPayOnboarding(mode = mode))
}
override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.common.routing.AppRoute
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -63,7 +64,7 @@ internal interface InnerWalletRouter {
fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig)
fun openTangemPayOnboarding(userWalletId: UserWalletId)
fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode)
fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)

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,15 +51,44 @@ sealed class WalletScreenAnalyticsEvent {
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
class ScreenOpened(
data class ScreenOpened(
private val hasMobileWallet: Boolean,
private val accountsCount: Int?,
val theme: String,
val isImported: Boolean,
) : MainScreen(
event = "Screen opened",
params = buildMap {
put("Mobile Wallet", if (hasMobileWallet) "Yes" else "No")
if (accountsCount != null) put("Accounts Count", accountsCount.toString())
put("App Theme", theme)
val seedPhrase = if (isImported) {
"Seed Phrase"
} else {
"Seedless"
}
put("Wallet Type", seedPhrase)
},
)
data class NoticeFinishActivation(
private val activationState: ActivationState,
private val balanceState: AnalyticsParam.EmptyFull,
) : MainScreen(
event = "Notice - Finish Activation",
params = mapOf(
"App Theme" to theme,
"Wallet Type" to if (isImported) "Seed Phrase" else "Seedless",
"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(
@ -79,9 +108,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>,
@ -90,54 +119,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
}
@ -146,8 +175,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

@ -29,7 +29,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

@ -11,6 +11,11 @@ internal sealed class TangemPayState {
data object Loading : TangemPayState()
data class OnboardingBanner(
val onClick: () -> Unit,
val closeOnClick: () -> Unit,
) : TangemPayState()
data class Progress(
val title: TextReference,
val description: TextReference,

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

@ -0,0 +1,18 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayHideOnboardingStateTransformer(
userWalletId: UserWalletId,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(tangemPayState = TangemPayState.Empty)
} else {
prevState
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayOnboardingBannerStateTransformer(
userWalletId: UserWalletId,
private val onClick: (UserWalletId) -> Unit,
private val closeOnClick: (UserWalletId) -> Unit,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(
tangemPayState = TangemPayState.OnboardingBanner(
onClick = { onClick(userWalletId) },
closeOnClick = { closeOnClick(userWalletId) },
),
)
} else {
prevState
}
}
}

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

@ -42,6 +42,7 @@ internal class MultiWalletCurrencyActionsConverter(
}
}
@Suppress("LongMethod")
private fun mapTokenActionState(
actionsState: TokenActionsState.ActionState,
cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -105,6 +106,11 @@ internal class MultiWalletCurrencyActionsConverter(
icon = R.drawable.ic_analytics_24
action = { clickIntents.onAnalyticsClick(cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.YieldMode -> {
title = resourceReference(R.string.yield_module_start_earning)
icon = R.drawable.ic_analytics_up_mini_24
action = { /* no-op */ }
}
}
return TokenActionButtonConfig(

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

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.common.routing.AppRoute
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.MainCustomerInfoContentState
@ -79,6 +80,13 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
updateTangemPay(data = state.info, userWalletId = userWalletId)
analytics.send(customerInfo = state.info)
}
is MainCustomerInfoContentState.OnboardingBanner -> stateController.update(
transformer = TangemPayOnboardingBannerStateTransformer(
userWalletId = userWalletId,
onClick = clickIntents::onOnboardingBannerClick,
closeOnClick = clickIntents::onOnboardingBannerCloseClick,
),
)
}
}
@ -91,7 +99,11 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
userWalletId = userWalletId,
value = data,
cardFrozenState = cardFrozenState,
onClickKyc = { innerWalletRouter.openTangemPayOnboarding(userWalletId) },
onClickKyc = {
innerWalletRouter.openTangemPayOnboarding(
mode = AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId),
)
},
onIssuingCard = clickIntents::onIssuingCardClicked,
onIssuingFailed = clickIntents::onIssuingFailedClicked,
openDetails = { config ->

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

@ -23,6 +23,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier)
is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier)
is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier)
is TangemPayState.OnboardingBanner -> TangemPayOnboardingBanner(state, modifier)
TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier)
}
}

View file

@ -0,0 +1,129 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
private const val GRADIENT_START_COLOR = 0xFF252934
private const val GRADIENT_END_COLOR = 0xFF12141E
private const val GRADIENT_OFFSET_X = 0f
private const val GRADIENT_OFFSET_Y = 80F
private const val GRADIENT_RADIUS = 200F
@Composable
internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
brush = Brush.radialGradient(
colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)),
center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y),
radius = GRADIENT_RADIUS,
),
)
.clickable(onClick = state.onClick),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 8.dp)
.width(55.dp)
.height(95.dp)
.offset(y = 10.dp),
)
SpacerH4()
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResourceSafe(R.string.tangempay_onboarding_banner_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
SpacerH(6.dp)
Text(
text = stringResourceSafe(R.string.tangempay_onboarding_banner_description),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.TopEnd,
) {
Image(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable(onClick = state.closeOnClick)
.padding(4.dp)
.padding(top = 12.dp)
.size(12.dp),
painter = painterResource(id = R.drawable.ic_close_24),
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
contentDescription = null,
)
}
}
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewTangemOnboardingBanner() {
TangemThemePreview {
TangemPayOnboardingBanner(
TangemPayState.OnboardingBanner(
onClick = {},
closeOnClick = {},
),
)
}
}

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),