Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-30 10:56:55 +03:00
commit cc3401a412
943 changed files with 15667 additions and 6451 deletions

View file

@ -126,6 +126,7 @@ dependencies {
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.tangempay.details.api)
implementation(projects.features.feed.api)
/** Common modules */
implementation(projects.common)

View file

@ -19,7 +19,6 @@
<ID>BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Whether to dim content */ abstract val dimContent: Boolean</ID>
<ID>BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton.Swap$val showBadge: Boolean = false</ID>
<ID>BooleanPropertyNaming:WalletModel.kt$WalletModel$private var needToRefreshWallet = false</ID>
<ID>BooleanPropertyNaming:WalletModel.kt$WalletModel$val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced()</ID>
<ID>BooleanPropertyNaming:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase$private val useNewListRepository: Boolean</ID>
<ID>BooleanPropertyNaming:WalletScreen.kt$val portfolioContent = state is WalletState.MultiCurrency.Content &amp;&amp; state.tokensListState is WalletTokensListState.ContentState.PortfolioContent</ID>
<ID>BooleanPropertyNaming:WalletScreen.kt$val showMarketsHint by remember { derivedStateOf { // Show hint only when there are items in the list // and when there a no items to scroll listState.layoutInfo.totalItemsCount &gt; 0 &amp;&amp; !listState.canScrollBackward &amp;&amp; !listState.canScrollForward || listState.canScrollBackward &amp;&amp; !listState.canScrollForward } }</ID>
@ -46,7 +45,6 @@
<ID>MultilineLambdaItParameter:SetRefreshStateTransformer.kt$SetRefreshStateTransformer${ it.mapNotNull { button -&gt; when (button) { is WalletManageButton.Buy -&gt; button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -&gt; button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -&gt; button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -&gt; button is WalletManageButton.Stake -&gt; null is WalletManageButton.Swap -&gt; null } } }</ID>
<ID>MultilineLambdaItParameter:SetVisaInfoTransformer.kt$SetVisaInfoTransformer${ if (it is RefreshTokenExpiredException) { return getRefreshTokenExpiredState(prevState) } return prevState.copy( buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) }</ID>
<ID>MultilineLambdaItParameter:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach }</ID>
<ID>MultilineLambdaItParameter:SingleWalletOnrampTransactionConverter.kt$SingleWalletOnrampTransactionConverter${ analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) clickIntents.onGoToProviderClick(it) }</ID>
<ID>MultilineLambdaItParameter:TokenListAnalyticsSender.kt$TokenListAnalyticsSender${ val status = it.value if (status is CryptoCurrencyStatus.Loaded) { sendTokenBalancesForSpecificBlockchains(it, status) } }</ID>
<ID>MultilineLambdaItParameter:TokenListStateConverter.kt$TokenListStateConverter${ if (isExtend) { clickIntents.onAccountCollapseClick(it) } else { clickIntents.onAccountExpandClick(it) } }</ID>
<ID>MultilineLambdaItParameter:TxHistorySubscriber.kt$TxHistorySubscriber${ SetTxHistoryCountErrorTransformer( userWallet = userWallet, error = it, pendingTransactions = status.value.pendingTransactions, clickIntents = clickIntents, ) }</ID>
@ -72,7 +70,6 @@
<ID>MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) shareManager.shareText(text = it) }</ID>
<ID>MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) }</ID>
<ID>MultilineLambdaItParameter:WalletLoaderStorage.kt$WalletLoaderStorage${ it.forEach(Job::cancel) loaders.remove(id) }</ID>
<ID>MultilineLambdaItParameter:WalletModel.kt$WalletModel${ it .conflate() .distinctUntilChanged() .onEach { selectedWallet -&gt; if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) }</ID>
<ID>MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletScreenContentLoader.load( userWallet = it, clickIntents = clickIntents, coroutineScope = modelScope, isRefresh = true, ) }</ID>
<ID>MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletsUpdateActionResolver.resolve( wallets = it, currentState = stateHolder.value, ) }</ID>
<ID>MultilineLambdaItParameter:WalletNFTListSubscriber.kt$WalletNFTListSubscriber${ stateHolder.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) }</ID>
@ -85,9 +82,7 @@
<ID>MultilineLambdaItParameter:WalletScreen.kt${ nftCollections( modifier = itemModifier, state = it.nftState, ) }</ID>
<ID>MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null }</ID>
<ID>MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) }</ID>
<ID>MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ when (it) { UnlockWalletError.AlreadyUnlocked -&gt; Unit UnlockWalletError.ScannedCardWalletNotMatched -&gt; { uiMessageSender.send( message = DialogMessage( title = resourceReference(R.string.common_warning), message = resourceReference(R.string.error_wrong_wallet_tapped), ), ) } UnlockWalletError.UnableToUnlock -&gt; { Timber.e("Unable to unlock wallet with id: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } UnlockWalletError.UserCancelled -&gt; Unit UnlockWalletError.UserWalletNotFound -&gt; { // This should never happen in this flow Timber.e("User wallet not found for unlock: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } } }</ID>
<ID>MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() }</ID>
<ID>MultilineLambdaItParameter:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { it.walletCardState.id } else { null } }</ID>
<ID>NamedArguments:BasicAccountListSubscriber.kt$BasicAccountListSubscriber$updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap)</ID>
<ID>NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)</ID>
<ID>NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents)</ID>
@ -111,23 +106,6 @@
<ID>NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token }</ID>
<ID>NoNameShadowing:WalletNFTItem.kt$modifier</ID>
<ID>NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -&gt; organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } }</ID>
<ID>NoNameShadowing:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ when (it) { UnlockWalletError.AlreadyUnlocked -&gt; Unit UnlockWalletError.ScannedCardWalletNotMatched -&gt; { uiMessageSender.send( message = DialogMessage( title = resourceReference(R.string.common_warning), message = resourceReference(R.string.error_wrong_wallet_tapped), ), ) } UnlockWalletError.UnableToUnlock -&gt; { Timber.e("Unable to unlock wallet with id: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } UnlockWalletError.UserCancelled -&gt; Unit UnlockWalletError.UserWalletNotFound -&gt; { // This should never happen in this flow Timber.e("User wallet not found for unlock: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } } }</ID>
<ID>NoNameShadowing:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ it is WalletNotification.FinishWalletActivation }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:AccountDependencies.kt$AccountDependencies$val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isDemoCardUseCase: IsDemoCardUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isNeedToBackupUseCase: IsNeedToBackupUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isDemoCardUseCase: IsDemoCardUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isNeedToBackupUseCase: IsNeedToBackupUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:OrganizeTokensModel.kt$OrganizeTokensModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:ScreenLifecycleProvider.kt$ScreenLifecycleProvider$val isBackgroundState: StateFlow&lt;Boolean&gt; = _isBackgroundState</ID>
<ID>NonBooleanPropertyPrefixedWithIs:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor$private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor$private val isDemoCardUseCase: IsDemoCardUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:WalletModel.kt$WalletModel$private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled</ID>
<ID>NonBooleanPropertyPrefixedWithIs:WalletScreen.kt$val isAutoScroll = remember { mutableStateOf(value = false) }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:WalletScreen.kt$val isNavBarVisible = remember { mutableStateOf(true) }</ID>
<ID>NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinCurrency</ID>
<ID>NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinStatus</ID>
<ID>NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${cryptoCurrencies?.size}</ID>

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,9 @@ internal class WalletModel @Inject constructor(
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
init {
screenLifecycleProvider.isBackgroundState
.onEach { isBackground ->
if (isBackground.not()) {
suggestToEnableBiometrics()
}
}.launchIn(modelScope)
trackScreenOpened()
updateMarketToggle()
suggestToOpenMarkets()
maybeMigrateNames()
@ -140,6 +140,16 @@ internal class WalletModel @Inject constructor(
clickIntents.initialize(innerWalletRouter, modelScope)
}
fun onResume() {
suggestToEnableBiometrics()
}
private fun updateMarketToggle() {
stateHolder.update {
it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled)
}
}
private fun updateYieldSupplyApy() {
if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) {
modelScope.launch(dispatchers.default) {
@ -168,13 +178,15 @@ internal class WalletModel @Inject constructor(
walletScreenContentLoader.cancelAll()
}
private suspend fun suggestToEnableBiometrics() {
if (shouldShowAskBiometryBottomSheet()) {
delay(timeMillis = 1_800)
private fun suggestToEnableBiometrics() {
modelScope.launch(dispatchers.main) {
if (shouldShowAskBiometryBottomSheet()) {
delay(timeMillis = 1_800)
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForBiometry,
)
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForBiometry,
)
}
}
}
@ -192,10 +204,45 @@ internal class WalletModel @Inject constructor(
}
}
private fun trackScreenOpened() {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
userWalletsListRepository
.selectedUserWalletSync()
?.let { selectedWallet ->
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(),
),
)
}
}
} else {
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy(),
)
}
}
private suspend fun shouldShowAskBiometryBottomSheet(): Boolean {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } &&
innerWalletRouter.isWalletLastScreen() &&
shouldShowAskBiometryUseCase() &&
canUseBiometryUseCase()
} else {
@ -257,7 +304,9 @@ internal class WalletModel @Inject constructor(
notificationsRepository.setShouldAskNotificationPermissionsViaBs(true)
return@launch
}
if (!isBiometricsEnabled) return@launch
if (!hotWalletFeatureToggles.isHotWalletEnabled && !isBiometricsEnabled) {
return@launch
}
if (!shouldShow) {
return@launch
}
@ -278,14 +327,14 @@ internal class WalletModel @Inject constructor(
.conflate()
.distinctUntilChanged()
.onEach { selectedWallet ->
trackingContextProxy.setContext(selectedWallet)
if (selectedWallet.isMultiCurrency) {
selectedWalletAnalyticsSender.send(selectedWallet)
}
subscribeOnExpressTransactionsUpdates(selectedWallet)
observeAndClearNFTCacheIfNeedUseCase(selectedWallet)
sendMainScreenOpenedAnalytics(selectedWallet.isImported())
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
@ -419,7 +468,8 @@ internal class WalletModel @Inject constructor(
private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) {
when (action) {
is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action)
is WalletsUpdateActionResolver.Action.ReinitializeWallet -> reinitializeWallet(action)
is WalletsUpdateActionResolver.Action.ReinitializeNewWallet -> reinitializeNewWallet(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)
@ -502,7 +552,7 @@ internal class WalletModel @Inject constructor(
}
}
private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) {
private fun reinitializeNewWallet(action: WalletsUpdateActionResolver.Action.ReinitializeNewWallet) {
walletScreenContentLoader.cancel(action.prevWalletId)
tokenListStore.remove(action.prevWalletId)
@ -515,7 +565,7 @@ internal class WalletModel @Inject constructor(
fetchWalletContent(userWallet = action.selectedWallet)
stateHolder.update(
ReinitializeWalletTransformer(
ReinitializeNewWalletTransformer(
prevWalletId = action.prevWalletId,
newUserWallet = action.selectedWallet,
clickIntents = clickIntents,
@ -524,6 +574,29 @@ 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,
)
fetchWalletContent(userWallet = userWallet)
stateHolder.update(
ReinitializeWalletTransformer(
userWallet = userWallet,
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
),
)
}
}
private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
if (accountsFeatureToggles.isFeatureEnabled) {
fetchWalletContent(userWallet = action.selectedWallet)
@ -606,7 +679,7 @@ internal class WalletModel @Inject constructor(
}
private suspend fun unlockWallet(action: WalletsUpdateActionResolver.Action.UnlockWallet) {
withContext(dispatchers.io) { delay(timeMillis = 700) }
delay(timeMillis = 700)
stateHolder.update(
transformer = UnlockWalletTransformer(
@ -623,7 +696,7 @@ internal class WalletModel @Inject constructor(
)
action.unlockedWallets.onEach { userWallet ->
modelScope.launch { fetchWalletContent(userWallet = userWallet) }
fetchWalletContent(userWallet = userWallet)
}
}
@ -695,17 +768,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,46 +64,53 @@ internal class WalletsUpdateActionResolver @Inject constructor(
selectedWallet: UserWallet,
): Action {
return when {
isHotWalletUpgraded(state, wallets) -> {
getHotWalletsUpgradedAction(state, wallets)
}
isWalletsCountChanged(state, wallets) -> {
getChangeWalletsListAction(state, wallets, selectedWallet)
}
isAnotherWalletSelected(state, selectedWallet) -> {
Action.ReinitializeWallet(
prevWalletId = state.getPrevSelectedWallet().id,
isAnyWalletUnlocked(state, wallets) -> {
Action.UnlockWallet(
selectedWallet = selectedWallet,
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
)
}
isAnyWalletNameChanged(state, wallets) -> {
getRenameWalletsAction(state, wallets)
}
isAnyHotWalletUpgraded(state, wallets) -> {
getHotWalletsUpgradedAction(state, wallets, selectedWallet)
}
isAnyHotWalletBackedUpChange(state, wallets) -> {
getHotWalletsBackedUpAction(state, wallets)
}
else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet)
isWalletsCountChanged(state, wallets) -> {
getChangeWalletsListAction(state, wallets, selectedWallet)
}
isAnotherWalletSelected(state, selectedWallet) -> {
Action.ReinitializeNewWallet(
prevWalletId = state.getPrevSelectedWallet().id,
selectedWallet = 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
}
@ -296,7 +291,7 @@ internal class WalletsUpdateActionResolver @Inject constructor(
* @property prevWalletId previous selected wallet id
* @property selectedWallet selected wallet
*/
data class ReinitializeWallet(
data class ReinitializeNewWallet(
val prevWalletId: UserWalletId,
val selectedWallet: UserWallet,
) : Action() {
@ -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,19 +1,29 @@
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
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.TangemPayEligibilityManager
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.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer
import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -22,13 +32,21 @@ internal interface TangemPayIntents {
suspend fun onPullToRefresh()
fun onRefreshPayToken(userWalletId: UserWalletId)
fun onRefreshPayToken(userWallet: UserWallet)
fun openDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)
fun onKycProgressClicked(userWalletId: UserWalletId)
fun onIssuingCardClicked()
fun onIssuingFailedClicked()
fun onPaySupportClick()
fun onOnboardingBannerClick(userWalletId: UserWalletId)
fun onOnboardingBannerCloseClick(userWalletId: UserWalletId)
}
@Suppress("LongParameterList")
@ -41,6 +59,8 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val tangemPayOnboardingRepository: OnboardingRepository,
private val tangemPayEligibilityManager: TangemPayEligibilityManager,
private val uiMessageSender: UiMessageSender,
) : BaseWalletClickIntents(), TangemPayIntents {
@ -54,13 +74,69 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
override fun onRefreshPayToken(userWalletId: UserWalletId) {
override fun onRefreshPayToken(userWallet: UserWallet) {
stateHolder.update(TangemPayRefreshShowProgressTransformer(userWallet.walletId))
modelScope.launch {
produceInitialDataTangemPay.invoke(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
produceInitialDataTangemPay.invoke(userWallet.walletId)
.onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) }
.onLeft {
stateHolder.update(
transformer = TangemPayRefreshNeededStateTransformer(
userWallet = userWallet,
userWalletId = userWallet.walletId,
onRefreshClick = { onRefreshPayToken(userWallet) },
),
)
}
}
}
override fun openDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {
router.openTangemPayDetails(
userWalletId = userWalletId,
config = config,
)
}
override fun onKycProgressClicked(userWalletId: UserWalletId) {
val cancelKycConfirmDialogMessage = DialogMessage(
title = resourceReference(R.string.tangempay_kyc_confirm_cancellation_alert_title),
message = resourceReference(R.string.tangempay_kyc_confirm_cancellation_description),
firstAction = EventMessageAction(
title = resourceReference(R.string.common_not_now),
onClick = { },
),
secondAction = EventMessageAction(
title = resourceReference(R.string.common_confirm),
onClick = { disableTangemPay(userWalletId) },
),
)
val kycInfoBottomSheet = bottomSheetMessage {
infoBlock {
iconImage(res = com.tangem.core.ui.R.drawable.img_visa_notification)
title = resourceReference(R.string.tangempay_kyc_in_progress)
body = resourceReference(R.string.tangempay_kyc_in_progress_popup_description)
}
primaryButton {
text = resourceReference(R.string.tangempay_kyc_in_progress_notification_button)
onClick = {
router.openTangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId))
closeBs()
}
}
secondaryButton {
text = resourceReference(R.string.tangempay_cancel_kyc)
onClick = {
uiMessageSender.send(cancelKycConfirmDialogMessage)
closeBs()
}
}
}
uiMessageSender.send(kycInfoBottomSheet)
}
override fun onIssuingCardClicked() {
val issuingBottomSheet = bottomSheetMessage {
infoBlock {
@ -115,4 +191,30 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
)
}
}
override fun onOnboardingBannerClick(userWalletId: UserWalletId) {
modelScope.launch {
val isEligible = tangemPayEligibilityManager.getTangemPayAvailability()
if (isEligible) {
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId))
} else {
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
}
}
override fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) {
modelScope.launch {
stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId))
onboardingRepository.setHideMainOnboardingBanner(userWalletId)
}
}
private fun disableTangemPay(userWalletId: UserWalletId) {
modelScope.launch {
tangemPayOnboardingRepository.disableTangemPay(userWalletId)
.onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) }
.onLeft { uiMessageSender.send(ToastMessage(resourceReference(R.string.common_something_went_wrong))) }
}
}
}

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,12 +4,13 @@ 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.CryptoCurrency
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
@ -233,11 +234,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)
}
@ -288,7 +291,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"
@ -392,7 +395,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,
@ -402,7 +405,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,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
error.handle(
onAlreadyUnlocked = {},
onUserCancelled = {},
analyticsEventHandler = analyticsEventHandler,
isFromUnlockAll = true,
showMessage = uiMessageSender::send,
)
}
@ -221,7 +220,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 +248,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 +306,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 +335,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 +343,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 +357,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 +365,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 +375,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 +393,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 +402,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 +425,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 +444,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 +467,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 +507,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 +546,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

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AppsFlyerOnlyEvent
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
import com.tangem.domain.models.wallet.UserWalletId
@ -22,6 +23,11 @@ sealed class WalletScreenAnalyticsEvent {
override val oneTimeEventId: String = id + userWalletId.stringValue
}
class AppsFlyerWalletFunded(userWalletId: UserWalletId) : Basic(event = "wallet_funded"),
AppsFlyerOnlyEvent, OneTimeAnalyticsEvent {
override val oneTimeEventId: String = id + userWalletId.stringValue
}
class CardWasScanned(source: AnalyticsParam.ScreensSources) : Basic(
event = "Card Was Scanned",
params = mapOf(
@ -37,7 +43,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 +57,46 @@ sealed class WalletScreenAnalyticsEvent {
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
class ScreenOpened(
class ScreenOpenedLegacy : MainScreen(event = "Screen opened")
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 +116,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 +127,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 +183,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

@ -158,9 +158,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(
@ -208,6 +208,7 @@ internal class TokenListAnalyticsSender @Inject constructor(
}
analyticsEventHandler.send(Basic.WalletToppedUp(userWallet.walletId, walletType))
analyticsEventHandler.send(Basic.AppsFlyerWalletFunded(userWallet.walletId))
}
}

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,18 +1,38 @@
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
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
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@ModelScoped
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: ConcurrentHashMap<UserWalletId, Boolean> = ConcurrentHashMap()
suspend fun send(
userWalletId: UserWalletId,
@ -25,10 +45,60 @@ internal class WalletWarningsSingleEventSender @Inject constructor(
val events = newWarnings.filter { it !in displayedUiState.warnings }
// We must show activation bs only for the first seen wallet when open the app (if need, see conditions below),
// so we keep this wallet id and use for future checks, ignore other wallets during the app session.
if (isActivationBottomSheetShown.isEmpty()) {
isActivationBottomSheetShown[userWalletId] = false
}
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 -> {
// We check that map contains the first seen wallet (will return null instead false/true otherwise)
// and for this wallet we haven't shown the activation bs yet (check that returns false, not true)
if (isActivationBottomSheetShown[userWalletId] == false) {
if (event.type == WalletActivationBannerType.Warning) {
showFinishActivationBottomSheet(userWalletId)
}
isActivationBottomSheetShown[userWalletId] = 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,
@ -39,4 +44,6 @@ internal sealed class TangemPayState {
) : TangemPayState()
data class TemporaryUnavailable(val notification: WalletNotification) : TangemPayState()
data object ExposedDevice : TangemPayState()
}

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
@ -150,15 +151,18 @@ sealed class WalletNotification(val config: NotificationConfig) {
)
data class TangemPayRefreshNeeded(
@DrawableRes val tangemIcon: Int?,
val onRefreshClick: () -> Unit,
@DrawableRes private val tangemIcon: Int?,
private val onRefreshClick: () -> Unit,
private val buttonText: TextReference,
private val shouldShowProgress: Boolean,
) : Warning(
title = resourceReference(id = R.string.tangempay_payment_account_sync_needed),
subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account),
buttonsState = ButtonsState.PrimaryButtonConfig(
text = resourceReference(id = R.string.home_button_scan),
text = buttonText,
iconResId = tangemIcon,
onClick = onRefreshClick,
shouldShowProgress = shouldShowProgress,
),
)
}
@ -287,14 +291,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

@ -0,0 +1,46 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory
import kotlinx.collections.immutable.toImmutableList
/**
* Reinitialize and place new wallet at the end transformer
*
* @property prevWalletId reinitialized wallet id
* @property newUserWallet new user wallet
* @property clickIntents click intents
*
[REDACTED_AUTHOR]
*/
internal class ReinitializeNewWalletTransformer(
private val prevWalletId: UserWalletId,
private val newUserWallet: UserWallet,
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
WalletLoadingStateFactory(
clickIntents = clickIntents,
walletImageResolver = walletImageResolver,
)
}
override fun transform(prevState: WalletScreenState): WalletScreenState {
return prevState.copy(
wallets = prevState.wallets
.filterNot { it.walletCardState.id == prevWalletId }
.plus(
element = walletLoadingStateFactory.create(
userWallet = newUserWallet,
),
)
.toImmutableList(),
)
}
}

View file

@ -1,28 +1,23 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory
import kotlinx.collections.immutable.toImmutableList
/**
* Reinitialize wallet transformer
*
* @property prevWalletId reinitialized wallet id
* @property newUserWallet new user wallet
* @property clickIntents click intents
*
[REDACTED_AUTHOR]
* @property userWallet user wallet to reinitialize
* @property clickIntents click intents
* @property walletImageResolver wallet image resolver
*/
internal class ReinitializeWalletTransformer(
private val prevWalletId: UserWalletId,
private val newUserWallet: UserWallet,
private val userWallet: UserWallet,
private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver,
) : WalletScreenStateTransformer {
) : WalletStateTransformer(userWalletId = userWallet.walletId) {
private val walletLoadingStateFactory by lazy {
WalletLoadingStateFactory(
@ -31,16 +26,9 @@ internal class ReinitializeWalletTransformer(
)
}
override fun transform(prevState: WalletScreenState): WalletScreenState {
return prevState.copy(
wallets = prevState.wallets
.filterNot { it.walletCardState.id == prevWalletId }
.plus(
element = walletLoadingStateFactory.create(
userWallet = newUserWallet,
),
)
.toImmutableList(),
override fun transform(prevState: WalletState): WalletState {
return walletLoadingStateFactory.create(
userWallet = userWallet,
)
}
}

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,17 @@
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 TangemPayExposedDeviceTransformer(
userWalletId: UserWalletId,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(tangemPayState = TangemPayState.ExposedDevice)
} else {
prevState
}
}
}

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

@ -1,5 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
@ -8,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayRefreshNeededStateTransformer(
userWalletId: UserWalletId,
private val userWallet: UserWallet,
private val onRefreshClick: () -> Unit,
) : WalletStateTransformer(userWalletId = userWalletId) {
@ -15,7 +18,12 @@ internal class TangemPayRefreshNeededStateTransformer(
val tangemPayState = TangemPayState.RefreshNeeded(
notification = TangemPayRefreshNeeded(
tangemIcon = R.drawable.ic_tangem_24,
buttonText = when (userWallet) {
is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan)
is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access)
},
onRefreshClick = onRefreshClick,
shouldShowProgress = false,
),
)
return if (prevState is WalletState.MultiCurrency.Content) {

View file

@ -0,0 +1,24 @@
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.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayRefreshShowProgressTransformer(
userWalletId: UserWalletId,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
val multiContentState = prevState as? WalletState.MultiCurrency.Content ?: return prevState
val refreshNeededState = multiContentState.tangemPayState as? TangemPayState.RefreshNeeded ?: return prevState
val refreshNotification =
refreshNeededState.notification as? WalletNotification.Warning.TangemPayRefreshNeeded ?: return prevState
return multiContentState.copy(
tangemPayState = refreshNeededState.copy(
notification = refreshNotification.copy(shouldShowProgress = true),
),
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
@ -11,11 +12,10 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createCancelledState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createKycInProgressState
import java.util.Currency
/**
@ -26,12 +26,9 @@ private const val POLYGON_CHAIN_ID = 137
internal class TangemPayUpdateInfoStateTransformer(
userWalletId: UserWalletId,
private val value: MainScreenCustomerInfo? = null,
private val value: MainScreenCustomerInfo,
private val cardFrozenState: TangemPayCardFrozenState,
private val onClickKyc: () -> Unit = {},
private val onIssuingCard: () -> Unit = {},
private val onIssuingFailed: () -> Unit = {},
private val openDetails: (config: TangemPayDetailsConfig) -> Unit = {},
private val tangemPayClickIntents: TangemPayIntents,
) : WalletStateTransformer(userWalletId = userWalletId) {
override fun transform(prevState: WalletState): WalletState {
@ -44,16 +41,15 @@ internal class TangemPayUpdateInfoStateTransformer(
}
private fun createInitialState(): TangemPayState {
val cardInfo = value?.info?.cardInfo
val productInstance = value?.info?.productInstance
val cardInfo = value.info.cardInfo
val productInstance = value.info.productInstance
// when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing.
return when {
value == null -> TangemPayState.Empty
value.orderStatus == OrderStatus.CANCELED -> createCancelledState(onIssuingFailed)
!value.info.isKycApproved -> createKycInProgressState(onClickKyc)
value.orderStatus == OrderStatus.CANCELED -> createCancelledState()
!value.info.isKycApproved -> createKycInProgressState()
cardInfo != null && productInstance != null -> getCardInfoState(cardInfo, productInstance)
else -> createIssueProgressState(onIssuingCard)
else -> createIssueProgressState()
}
}
@ -63,9 +59,11 @@ internal class TangemPayUpdateInfoStateTransformer(
balanceText = TextReference.Str(getBalanceText(cardInfo)),
balanceSymbol = stringReference("USDC"), // TODO hardcode for now
onClick = {
openDetails(
tangemPayClickIntents.openDetails(
userWalletId,
TangemPayDetailsConfig(
cardId = productInstance.cardId,
isPinSet = cardInfo.isPinSet,
cardFrozenState = cardFrozenState,
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,
@ -81,4 +79,28 @@ internal class TangemPayUpdateInfoStateTransformer(
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
}
private fun createKycInProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_kyc_in_progress),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = { tangemPayClickIntents.onKycProgressClicked(userWalletId) },
)
private fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_issuing_your_card),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = tangemPayClickIntents::onIssuingCardClicked,
showProgress = true,
)
private fun createCancelledState(): TangemPayState = TangemPayState.FailedIssue(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_failed_to_issue_card),
iconRes = R.drawable.ic_alert_24,
onButtonClick = tangemPayClickIntents::onIssuingFailedClicked,
)
}

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

@ -1,33 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.state.util
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
internal object TangemPayStateCreator {
fun createKycInProgressState(onClickKyc: () -> Unit): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_kyc_in_progress),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = onClickKyc,
)
fun createIssueProgressState(onIssuingCardClick: () -> Unit): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_issuing_your_card),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = onIssuingCardClick,
showProgress = true,
)
fun createCancelledState(onIssueFailedClick: () -> Unit): TangemPayState = TangemPayState.FailedIssue(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_failed_to_issue_card),
iconRes = R.drawable.ic_alert_24,
onButtonClick = onIssueFailedClick,
)
}

View file

@ -52,7 +52,6 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
shouldShowMainPromo: Boolean = false,
) {
val accountFlattenCurrencies = accountList.flattenCurrencies()
val mainAccount = accountList.mainAccount
when {
@ -74,26 +73,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,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
val convertParams = TokenConverterParams.Account(accountList, expandedAccounts)
updateContent(
params = convertParams,
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletTangemPayAnalyticsEventSender
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
@ -27,7 +26,6 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
@Assisted private val userWallet: UserWallet,
private val stateController: WalletStateController,
private val clickIntents: WalletClickIntents,
private val innerWalletRouter: InnerWalletRouter,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val analytics: WalletTangemPayAnalyticsEventSender,
@ -48,7 +46,8 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
stateController.update(
transformer = TangemPayRefreshNeededStateTransformer(
userWalletId = userWalletId,
onRefreshClick = { clickIntents.onRefreshPayToken(userWalletId) },
userWallet = userWallet,
onRefreshClick = { clickIntents.onRefreshPayToken(userWallet) },
),
)
}
@ -57,7 +56,10 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
transformer = TangemPayUnavailableStateTransformer(userWalletId),
)
}
else -> {
TangemPayCustomerInfoError.ExposedDeviceError -> {
stateController.update(TangemPayExposedDeviceTransformer(userWalletId))
}
TangemPayCustomerInfoError.UnknownError -> {
// hide TangemPay block
Timber.e("Failed when loading main screen TangemPay info: $tangemPayError")
stateController.update(
@ -79,6 +81,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,15 +100,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
userWalletId = userWalletId,
value = data,
cardFrozenState = cardFrozenState,
onClickKyc = { innerWalletRouter.openTangemPayOnboarding(userWalletId) },
onIssuingCard = clickIntents::onIssuingCardClicked,
onIssuingFailed = clickIntents::onIssuingFailedClicked,
openDetails = { config ->
innerWalletRouter.openTangemPayDetails(
userWalletId = userWalletId,
config = config,
)
},
tangemPayClickIntents = clickIntents,
),
)
}

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

@ -0,0 +1,41 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import com.tangem.core.ui.R
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.inputrow.InputRowImageBase
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
private const val DISABLED_ALPHA = 0.6F
@Composable
internal fun TangemPayExposedDeviceState(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.background(TangemTheme.colors.background.primary)
.alpha(DISABLED_ALPHA),
enabled = false,
onClick = {},
) {
InputRowImageBase(
modifier = Modifier
.padding(
all = TangemTheme.dimens.spacing12,
),
subtitle = resourceReference(R.string.tangempay_payment_account),
caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle),
subtitleColor = TangemTheme.colors.text.primary1,
captionColor = TangemTheme.colors.text.tertiary,
iconResWebp = R.drawable.img_visa_36,
endIconTint = TangemTheme.colors.icon.warning,
)
}
}

View file

@ -23,7 +23,9 @@ 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)
TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier)
is TangemPayState.OnboardingBanner -> TangemPayOnboardingBanner(state, modifier)
is TangemPayState.ExposedDevice -> TangemPayExposedDeviceState(modifier)
is TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier)
}
}
@ -35,6 +37,8 @@ private fun TangemPayMainScreenBlockPreview() {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false)
TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false)
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),

View file

@ -0,0 +1,120 @@
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.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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 androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.SpacerH
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),
) {
ConstraintLayout(
modifier = Modifier.fillMaxWidth(),
) {
val (image, text, close) = createRefs()
Column(
modifier = Modifier
.constrainAs(text) {
top.linkTo(parent.top)
start.linkTo(image.end, margin = 12.dp)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
}
.padding(top = 16.dp, end = 16.dp, bottom = 16.dp),
) {
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,
)
}
Image(
painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner),
contentDescription = null,
modifier = Modifier
.constrainAs(image) {
start.linkTo(parent.start)
top.linkTo(text.top)
bottom.linkTo(text.bottom)
height = Dimension.fillToConstraints
}
.padding(top = 8.dp, start = 24.dp),
)
Image(
painter = painterResource(R.drawable.ic_close_24),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable(onClick = state.closeOnClick)
.constrainAs(close) {
top.linkTo(parent.top, margin = 16.dp)
end.linkTo(parent.end, margin = 16.dp)
},
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
)
}
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewTangemOnboardingBanner() {
TangemThemePreview {
TangemPayOnboardingBanner(
TangemPayState.OnboardingBanner(
onClick = {},
closeOnClick = {},
),
)
}
}

View file

@ -68,7 +68,9 @@ private fun TangemPayRefreshBlockPreview() {
state = TangemPayState.RefreshNeeded(
TangemPayRefreshNeeded(
tangemIcon = R.drawable.ic_tangem_24,
buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access),
onRefreshClick = {},
shouldShowProgress = true,
),
),
modifier = Modifier,

View file

@ -169,7 +169,7 @@ class YieldSupplyPromoBannerKeyConverterTest {
fiatAmount = BigDecimal.ZERO,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
yieldBalance = null,
stakingBalance = null,
yieldSupplyStatus = if (isYieldActive) {
YieldSupplyStatus(
isActive = true,
@ -207,7 +207,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),