Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-18 12:36:46 +05:00
parent 26201c5c0b
commit 0be1358760
39 changed files with 581 additions and 124 deletions

View file

@ -11,6 +11,7 @@ import com.tangem.common.ui.tokens.getUnavailabilityReasonText
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic.ButtonSupport
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
@ -27,10 +28,12 @@ import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.AccountId
@ -58,6 +61,7 @@ import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -144,6 +148,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val isCryptoCurrencyCouldHideUseCase: IsCryptoCurrencyCouldHideUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val uiMessageSender: UiMessageSender,
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
override fun onSendClick(
@ -196,6 +203,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
event?.let { analyticsEventHandler.send(it) }
if (isTopUpBlockedByBackupError(accountId.userWalletId)) return
modelScope.launch(dispatchers.main) {
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
stateHolder.hideBottomSheet()
@ -339,6 +348,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
if (handleUnavailabilityReason(unavailabilityReason)) return
if (isTopUpBlockedByBackupError(accountId.userWalletId)) return
appRouter.push(
AppRoute.Onramp(
@ -363,6 +373,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
if (handleUnavailabilityReason(unavailabilityReason)) return
if (isTopUpBlockedByBackupError(accountId.userWalletId)) return
modelScope.launch(dispatchers.main) {
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
@ -451,6 +462,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {
if (isTopUpBlockedByBackupError(userWalletId)) return
if (!isMultiWalletTokensLoaded()) return
modelScope.launch {
@ -469,6 +481,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
override fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String) {
if (isTopUpBlockedByBackupError(userWalletId)) return
onMultiWalletActionClick(
statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId),
route = AppRoute.BuyCrypto(userWalletId = userWalletId),
@ -565,6 +579,26 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
return true
}
private fun isTopUpBlockedByBackupError(userWalletId: UserWalletId): Boolean {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return false
if (!isWalletBackupProblematicUseCase(userWallet)) return false
stateHolder.hideBottomSheet()
uiMessageSender.send(
WalletAlertUM.addFundsDisabledForBackupError(
onContactSupport = { contactBackupSupport(userWallet) },
),
)
return true
}
private fun contactBackupSupport(userWallet: UserWallet) {
analyticsEventHandler.send(ButtonSupport(source = AnalyticsParam.ScreensSources.Main))
modelScope.launch {
sendBackupProblemEmailUseCase(userWallet.walletId)
}
}
private fun isMultiWalletTokensLoaded(): Boolean {
return if (stateHolder.value.isRedesignEnabled) {
val selectedWalletUM = stateHolder.getSelectedWalletUM() as? WalletUM.Content ?: return false

View file

@ -1,47 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO
import javax.inject.Inject
class BackupValidator @Inject constructor() {
fun isValidFull(cardDTO: CardDTO): Boolean {
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
}
fun isValidBackupStatus(cardDTO: CardDTO): Boolean {
return validateBackupStatus(cardDTO)
}
private fun validateCurves(cardDTO: CardDTO): Boolean {
val config = CardConfig.createConfig(cardDTO)
// / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
// we cannot determine whether this curve is missing due to an error or because the user
// did not want to recreate the wallet.
val expectedCurves = config.mandatoryCurves
.filterNot { it == EllipticCurve.Bls12381G2Aug }
val curves = cardDTO.wallets.map { it.curve }
for (expectedCurve in expectedCurves) {
val cardCurvesCount = curves.count { it == expectedCurve }
// missing curve
if (cardCurvesCount == 0) {
return false
}
// duplicated curve
if (cardCurvesCount > 1) {
return false
}
}
return true
}
private fun validateBackupStatus(cardDTO: CardDTO): Boolean {
val backupStatus = cardDTO.backupStatus
backupStatus ?: return true // for card with null backup status, validation should always returns true
return backupStatus !is CardDTO.BackupStatus.CardLinked
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase
@ -56,7 +57,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val backupValidator: BackupValidator,
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
private val notificationsRepository: NotificationsRepository,
private val accountDependencies: AccountDependencies,
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
@ -118,6 +119,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val isAddFundsBannerShown = isAddFundsBannerVisible(accountStatusList.totalFiatBalance)
buildList {
addBackupErrorNotification(userWallet, clickIntents)
addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance)
addAddFundsBanner(
@ -126,7 +129,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
clickIntents = clickIntents,
)
addCriticalNotifications(userWallet, clickIntents)
addCriticalNotifications(userWallet)
addUpgradeHotWalletPromoNotification(
userWallet = userWallet,
@ -270,20 +273,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addCriticalNotifications(
private fun MutableList<WalletNotification>.addBackupErrorNotification(
userWallet: UserWallet,
clickIntents: WalletClickIntents,
) {
addIf(
element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() },
condition = isWalletBackupProblematicUseCase(userWallet),
)
}
private fun MutableList<WalletNotification>.addCriticalNotifications(userWallet: UserWallet) {
if (userWallet !is UserWallet.Cold) {
return
}
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
addIf(
element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() },
condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError,
)
addIf(
element = WalletNotification.Critical.DevCard,
condition = !cardTypesResolver.isReleaseFirmwareType(),

View file

@ -8,6 +8,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
@ -44,7 +45,7 @@ import javax.inject.Inject
internal class GetWalletNotificationsFactory @Inject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val backupValidator: BackupValidator,
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
private val accountDependencies: AccountDependencies,
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
@ -167,7 +168,7 @@ internal class GetWalletNotificationsFactory @Inject constructor(
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
addIf(
element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() },
condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError,
condition = isWalletBackupProblematicUseCase(userWallet),
)
addIf(

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.WrappedList
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.dialog.Dialogs
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.wallet.impl.R
@ -55,6 +56,10 @@ internal object WalletAlertUM {
)
}
fun addFundsDisabledForBackupError(onContactSupport: () -> Unit): DialogMessage {
return Dialogs.backupErrorAddFundsDisabled(onContactSupport = onContactSupport)
}
fun insufficientTokensCountForSwapping(): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.action_buttons_swap_no_tokens_added_alert_title),

View file

@ -50,9 +50,9 @@ sealed class WalletNotification(val config: NotificationConfig) {
)
data class BackupError(val onSupportClick: () -> Unit) : Critical(
title = resourceReference(R.string.warning_backup_errors_title),
subtitle = resourceReference(R.string.warning_backup_errors_message),
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
title = resourceReference(R.string.warning_backup_error_attention_title),
subtitle = resourceReference(R.string.warning_backup_error_attention_message),
buttonsState = ButtonsState.PrimaryButtonConfig(
text = resourceReference(id = R.string.common_contact_support),
onClick = onSupportClick,
),

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.WalletNotificationTestTags
import com.tangem.feature.wallet.impl.R
@ -66,6 +67,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
subtitleColor = TangemTheme.colors.text.secondary,
)
}
is WalletNotification.Critical.BackupError -> {
Notification(
config = item.config,
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
containerColor = TangemColorPalette.Amaranth_30,
borderColor = TangemColorPalette.Amaranth,
iconTint = TangemTheme.colors.icon.warning,
subtitleColor = TangemTheme.colors.text.primary1,
)
}
is WalletNotification.CreateTangemPayAccount -> {
CreatePaymentAccountNotification(
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),