Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-26 11:46:02 +03:00
parent 0a442d0a72
commit 5299629221
21 changed files with 111 additions and 3 deletions

View file

@ -694,6 +694,7 @@
<string name="scan_card_settings_message">Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свой Tangem!</string>
<string name="security_alert_title">Уведомление безопасности</string>
<string name="seed_warning_yes">Да</string>
<string name="selling_insufficient_balance_alert_message">На вашем балансе недостаточно средств для продажи криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить.</string>
<string name="selling_insufficient_balance_alert_title">Недостаточно средств</string>
<string name="selling_regional_restriction_alert_message">Продажа криптовалюты в вашем регионе временно недоступна. Мы активно работаем над тем, чтобы добавить эту возможность. Следите за нашими новостями!</string>
@ -897,6 +898,8 @@
<string name="story_meet_title">Встречайте Tangem</string>
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
<string name="story_web3_title">Поддержка Web 3.0</string>
<string name="sui_not_enough_coin_for_fee_description">Для отправки требуется входящая транзакция на сумму не менее %1$s</string>
<string name="sui_not_enough_coin_for_fee_title">Недостаточно средств</string>
<string name="swap_give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
<string name="swap_promo_text">Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке.</string>
<string name="swap_promo_title">Новый провайдер обмена!</string>
@ -1110,6 +1113,7 @@
<string name="warning_rate_app_title">Нравится Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его</string>
<string name="warning_rent_fee_title">Необходима плата за аренду сети</string>
<string name="warning_seedphrase_contacted_support">Вы обращались в службу поддержки через приложение в течение 7 дней после создания кошелька? Если обращались или не уверены, нажмите «Да» и следуйте инструкциям.</string>
<string name="warning_seedphrase_issue_answer_no">Спасибо! Всё в порядке! Дальнейшие действия не требуются.</string>
<string name="warning_seedphrase_issue_answer_yes">Вы будете перенаправлены на официальный сайт Tangem. Пожалуйста, прочитайте и выполните указанные там инструкции.</string>
<string name="warning_seedphrase_issue_message">Вы когда-либо обращались в службу поддержки Tangem напрямую из этого приложения?</string>

View file

@ -886,6 +886,8 @@
<string name="story_meet_title">Meet Tangem</string>
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
<string name="story_web3_title">Web 3.0 Compatible</string>
<string name="sui_not_enough_coin_for_fee_description">An incoming transaction of at least %1$s is required to proceed</string>
<string name="sui_not_enough_coin_for_fee_title">Insufficient funds</string>
<string name="swap_give_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
<string name="swap_promo_text">Exchange more tokens at better rates directly in your wallet.</string>
<string name="swap_promo_title">New Swap Provider Available!</string>
@ -985,6 +987,9 @@
<string name="user_wallet_list_rename_popup_title">Rename wallet</string>
<string name="user_wallet_list_unlock_all">Unlock all</string>
<string name="user_wallet_list_unlock_all_with">Unlock all with %s</string>
<string name="visa_unlock_notification_button">Unlock</string>
<string name="visa_unlock_notification_subtitle">Scan your card to unlock access</string>
<string name="visa_unlock_notification_title">Needed unlock</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>
<string name="wallet_balance_missing_derivation">Scan card or ring</string>
<string name="wallet_been_activated_message">This wallet has already been activated earlier.\nIf it was not done by you, please contact support.\nTangem never sells wallets along with pre-generated access codes.</string>
@ -1099,7 +1104,7 @@
<string name="warning_receive_blocked_hedera_token_association_required_message">You must associate your token before receiving tokens</string>
<string name="warning_rent_fee_title">Network rent fee required</string>
<string name="warning_seedphrase_action_required_title">Action required</string>
<string name="warning_seedphrase_contacted_support">Did you contact support via the app or email within 7 days of creating a wallet? If you did or are unsure, follow and complete the instructions.</string>
<string name="warning_seedphrase_contacted_support">Did you contact support via the app within 7 days of creating your wallet? If yes, or if you\'re unsure, click \'Yes\' and follow the instructions.</string>
<string name="warning_seedphrase_issue_answer_no">Thank you! All set! No further actions required.</string>
<string name="warning_seedphrase_issue_answer_yes">You will now be redirected to the official Tangem website. Please read and follow instructions there.</string>
<string name="warning_seedphrase_issue_message">Have you ever contacted the Tangem support team directly through this application?</string>

View file

@ -91,6 +91,11 @@ internal class VisaApiRequestMaker @Inject constructor(
private suspend fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens {
val userWallet = findVisaUserWallet(userWalletId)
val status = userWallet.scanResponse.visaCardActivationStatus ?: error("Visa card activation status not found")
if (status is VisaCardActivationStatus.RefreshTokenExpired) {
throw RefreshTokenExpiredException()
}
return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated")
}

View file

@ -59,6 +59,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.Warning.SomeNetworksUnreachable,
is WalletNotification.Warning.NetworksUnreachable,
is WalletNotification.UsedOutdatedData,
is WalletNotification.UnlockVisaAccess,
-> null
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond

View file

@ -191,6 +191,19 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
)
data class UnlockVisaAccess(val onUnlockClick: () -> Unit) : WalletNotification(
config = NotificationConfig(
title = resourceReference(id = R.string.visa_unlock_notification_title),
subtitle = resourceReference(id = R.string.visa_unlock_notification_subtitle),
iconResId = R.drawable.ic_locked_24,
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(id = R.string.visa_unlock_notification_button),
iconResId = R.drawable.ic_tangem_24,
onClick = onUnlockClick,
),
),
)
data class RateApp(
val onLikeClick: () -> Unit,
val onDislikeClick: () -> Unit,

View file

@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistor
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
internal const val NOT_INITIALIZED_WALLET_INDEX = -1
@ -115,5 +116,27 @@ internal sealed interface WalletState : WalletStateHolder {
override val balancesAndLimitBlockState: BalancesAndLimitsBlockState? = null
}
data class AccessTokenLocked(
override val walletCardState: WalletCardState,
override val buttons: PersistentList<WalletManageButton>,
override val bottomSheetConfig: TangemBottomSheetConfig?,
val onExploreClick: () -> Unit,
val onUnlockVisaAccessNotificationClick: () -> Unit,
) : Visa(),
TxHistoryStateHolder by LockedTxHistoryStateHolder(onExploreClick),
WalletStateHolder by LockedWalletStateHolder(
walletCardState = walletCardState,
buttons = buttons,
bottomSheetConfig = bottomSheetConfig,
onUnlockNotificationClick = {},
) {
override val warnings: ImmutableList<WalletNotification> = persistentListOf(
WalletNotification.UnlockVisaAccess(onUnlockClick = onUnlockVisaAccessNotificationClick),
)
override val balancesAndLimitBlockState: BalancesAndLimitsBlockState? = null
}
}
}

View file

@ -25,6 +25,9 @@ internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletS
is WalletState.Visa.Locked -> prevState.copy(
bottomSheetConfig = updateConfig(prevState),
)
is WalletState.Visa.AccessTokenLocked -> prevState.copy(
bottomSheetConfig = updateConfig(prevState),
)
}
}

View file

@ -31,6 +31,9 @@ internal class OpenBottomSheetTransformer(
is WalletState.Visa.Locked -> prevState.copy(
bottomSheetConfig = updateConfig(),
)
is WalletState.Visa.AccessTokenLocked -> prevState.copy(
bottomSheetConfig = updateConfig(),
)
}
}

View file

@ -44,6 +44,7 @@ internal class RenameWalletsTransformer(
is WalletState.MultiCurrency.Locked,
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.e("Impossible to rename wallet in locked state")
prevState

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState
@ -29,6 +30,10 @@ internal class SetBalancesAndLimitsTransformer(
override fun transformTyped(prevState: WalletState.Visa.Content): WalletState {
val visaCurrency = maybeVisaCurrency.getOrElse {
if (it is RefreshTokenExpiredException) {
return getRefreshTokenExpiredState(prevState)
}
return prevState.copy(
walletCardState = getErrorWalletCardState(prevState.walletCardState),
depositButtonState = prevState.depositButtonState.copy(isEnabled = false),
@ -100,4 +105,14 @@ internal class SetBalancesAndLimitsTransformer(
return WalletAdditionalInfo(hideable = true, infoContent)
}
private fun getRefreshTokenExpiredState(prevState: WalletState.Visa.Content): WalletState {
return WalletState.Visa.AccessTokenLocked(
walletCardState = prevState.walletCardState,
buttons = prevState.buttons,
bottomSheetConfig = prevState.bottomSheetConfig,
onExploreClick = clickIntents::onExploreClick,
onUnlockVisaAccessNotificationClick = clickIntents::onUnlockVisaAccessClick,
)
}
}

View file

@ -35,6 +35,7 @@ internal class SetRefreshStateTransformer(
is WalletState.MultiCurrency.Locked,
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> prevState
}
}

View file

@ -33,6 +33,7 @@ internal class SetTxHistoryCountErrorTransformer(
is WalletState.Visa.Content -> prevState.copy(txHistoryState = createErrorState())
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.w("Impossible to load transactions history for locked wallet")
prevState

View file

@ -26,6 +26,7 @@ internal class SetTxHistoryCountTransformer(
)
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.w("Impossible to load transactions history for locked wallet")
prevState

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
@ -16,9 +17,10 @@ internal class SetTxHistoryItemsErrorTransformer(
override fun transform(prevState: WalletState): WalletState {
return when (prevState) {
is WalletState.SingleCurrency.Content -> prevState.copy(txHistoryState = createErrorState())
is WalletState.Visa.Content -> prevState.copy(txHistoryState = createErrorState())
is WalletState.Visa.Content -> transformVisaContent(prevState)
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.w("Impossible to load transactions history for locked wallet")
prevState
@ -30,6 +32,20 @@ internal class SetTxHistoryItemsErrorTransformer(
}
}
private fun transformVisaContent(prevState: WalletState.Visa.Content): WalletState {
return if (error.cause is RefreshTokenExpiredException) {
WalletState.Visa.AccessTokenLocked(
walletCardState = prevState.walletCardState,
buttons = prevState.buttons,
bottomSheetConfig = prevState.bottomSheetConfig,
onExploreClick = clickIntents::onExploreClick,
onUnlockVisaAccessNotificationClick = clickIntents::onUnlockVisaAccessClick,
)
} else {
prevState.copy(txHistoryState = createErrorState())
}
}
private fun createErrorState(): TxHistoryState.Error = when (error) {
is TxHistoryListError.DataError -> {
TxHistoryState.Error(

View file

@ -26,6 +26,7 @@ internal class SetTxHistoryItemsTransformer(
)
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.w("Impossible to load transactions history for locked wallet")
prevState

View file

@ -54,6 +54,11 @@ internal class SetWalletCardDropDownItemsTransformer(
dropDownItems = constructDropDownItems(prevState.walletCardState.id),
),
)
is WalletState.Visa.AccessTokenLocked -> prevState.copy(
walletCardState = prevState.walletCardState.copySealed(
dropDownItems = constructDropDownItems(prevState.walletCardState.id),
),
)
}
}

View file

@ -19,6 +19,7 @@ internal class SetWarningsTransformer(
is WalletState.MultiCurrency.Locked,
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.w("Impossible to update notifications for locked wallet")
prevState

View file

@ -50,8 +50,9 @@ internal class UnlockWalletTransformer(
is WalletState.MultiCurrency.Content,
is WalletState.SingleCurrency.Content,
is WalletState.Visa.Content,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.e("Impossible to unlock wallet with content state")
Timber.e("Impossible to unlock wallet with not locked state")
prevState
}
}

View file

@ -27,6 +27,7 @@ internal class UpdateWalletCardsCountTransformer(
is WalletState.MultiCurrency.Locked,
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> {
Timber.e("Impossible to update wallet cards count for locked wallet")
prevState

View file

@ -107,6 +107,7 @@ internal class WalletClickIntents @Inject constructor(
is WalletState.MultiCurrency.Locked,
is WalletState.SingleCurrency.Locked,
is WalletState.Visa.Locked,
is WalletState.Visa.AccessTokenLocked,
-> Unit
}
}

View file

@ -55,6 +55,8 @@ internal interface WalletWarningsClickIntents {
fun onUnlockWalletClick()
fun onUnlockVisaAccessClick()
fun onScanToUnlockWalletClick()
fun onLikeAppClick()
@ -176,6 +178,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onUnlockVisaAccessClick() {
openScanCardDialog()
}
private fun handleUnlockWalletsError(error: UnlockWalletsError) {
val event = when (error) {
is UnlockWalletsError.DataError,