Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 05:54:18 -07:00
parent e16b84a3d9
commit 210f392373
10 changed files with 146 additions and 17 deletions

View file

@ -135,6 +135,10 @@
"name": "AND_15364_VISA_PAY_CARD_CLOSE",
"version": "undefined"
},
{
"name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT",
"version": "undefined"
},
{
"name": "TWI_83_ADDRESS_BOOK_ENABLED",
"version": "undefined"

View file

@ -1930,6 +1930,9 @@
<string name="tangempay_reissue_card_insufficient_funds_subtitle">Deposit USDC to payment account to cover the issuing fee</string>
<string name="tangempay_reissue_card_insufficient_funds_title">Unable to cover fee</string>
<string name="tangempay_reissue_card_title">Replace your card?</string>
<string name="tangempay_remove_account">Remove account</string>
<string name="tangempay_remove_account_alert_description">Tangem Pay will be removed from the main screen and won\'t appear again, even after reinstalling the app.</string>
<string name="tangempay_remove_account_alert_title">Remove account?</string>
<string name="tangempay_service_unavailable_description">Were fixing a technical issue. Please try again later.</string>
<string name="tangempay_service_unavailable_title">Service temporarily unavailable</string>
<string name="tangempay_service_unreachable_try_later">Service unreachable. However, card payments are still working.</string>

View file

@ -3,4 +3,5 @@ package com.tangem.features.tangempay
interface TangemPayFeatureToggles {
val isRedesignEnabled: Boolean
val isCloseCardEnabled: Boolean
val isRemoveAccountEnabled: Boolean
}

View file

@ -12,4 +12,7 @@ internal class DefaultTangemPayFeatureToggles(
override val isCloseCardEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15364_VISA_PAY_CARD_CLOSE)
override val isRemoveAccountEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15741_VISA_PAY_REMOVE_ACCOUNT)
}

View file

@ -18,6 +18,8 @@ import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.utils.TangemPayDetailIntents
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import com.tangem.core.ui.R as CoreUiR
@Suppress("LongParameterList")
internal class TangemPayDetailsStateFactory(
@ -25,6 +27,7 @@ internal class TangemPayDetailsStateFactory(
private val onOpenMenu: () -> Unit,
private val intents: TangemPayDetailIntents,
private val isRedesignEnabled: Boolean,
private val isRemoveAccountEnabled: Boolean,
) {
fun getLoadingState(): TangemPayDetailsUM {
return TangemPayDetailsUM(
@ -102,8 +105,8 @@ internal class TangemPayDetailsStateFactory(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
onOpenMenu = onOpenMenu,
items = persistentListOf(),
itemsV2 = persistentListOf(),
items = getDeactivatedMenuItems(),
itemsV2 = getDeactivatedMenuItemsV2(),
),
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
@ -131,6 +134,14 @@ internal class TangemPayDetailsStateFactory(
title = resourceReference(R.string.tangempay_account_deactivated_message_title),
subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle),
iconResId = R.drawable.img_attention_20,
buttonsState = if (isRemoveAccountEnabled) {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.tangempay_remove_account),
onClick = intents::onRemoveAccount,
)
} else {
null
},
)
private fun createRenewSessionNotificationConfig() = NotificationConfig(
@ -159,6 +170,55 @@ internal class TangemPayDetailsStateFactory(
)
}
private fun getDeactivatedMenuItems(): ImmutableList<TangemDropdownMenuItem> {
return buildList {
add(
TangemDropdownMenuItem(
title = resourceReference(R.string.tangempay_pay_support),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = intents::onContactSupportClicked,
),
)
if (isRemoveAccountEnabled) {
add(
TangemDropdownMenuItem(
title = resourceReference(R.string.tangempay_remove_account),
textColor = themedColor { TangemTheme.colors.text.warning },
onClick = intents::onRemoveAccount,
),
)
}
}.toImmutableList()
}
private fun getDeactivatedMenuItemsV2(): ImmutableList<TangemPayDropDownItemUM> {
return buildList {
add(
TangemPayDropDownItemUM(
title = resourceReference(R.string.tangempay_pay_support),
onClick = intents::onContactSupportClicked,
icon = TangemIconUM.Icon(
iconRes = R.drawable.ic_mail_20,
tintReference = { TangemTheme.colors3.icon.primary },
),
),
)
if (isRemoveAccountEnabled) {
add(
TangemPayDropDownItemUM(
title = resourceReference(R.string.tangempay_remove_account),
onClick = intents::onRemoveAccount,
icon = TangemIconUM.Icon(
iconRes = CoreUiR.drawable.ic_trash_24,
tintReference = { TangemTheme.colors3.icon.status.error },
),
titleColor = { TangemTheme.colors3.text.status.error },
),
)
}
}.toImmutableList()
}
private fun getTopBarMenuItemsV2(): ImmutableList<TangemPayDropDownItemUM> {
return persistentListOf(
TangemPayDropDownItemUM(

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.ColorReference2
import com.tangem.core.ui.extensions.TextReference
internal data class TangemPayDropDownItemUM(
@ -9,4 +10,5 @@ internal data class TangemPayDropDownItemUM(
val icon: TangemIconUM,
val subtitle: TextReference? = null,
val isEnabled: Boolean = true,
val titleColor: ColorReference2? = null,
)

View file

@ -16,6 +16,8 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
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.SnackbarMessage
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
@ -27,6 +29,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
@ -75,6 +78,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val produceTangemPayInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
private val onboardingRepository: OnboardingRepository,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -92,6 +96,7 @@ internal class TangemPayDetailsModel @Inject constructor(
onOpenMenu = ::onOpenMenu,
intents = this,
isRedesignEnabled = isRedesignEnabled(),
isRemoveAccountEnabled = tangemPayFeatureToggles.isRemoveAccountEnabled,
)
val uiState: StateFlow<TangemPayDetailsUM>
@ -374,6 +379,36 @@ internal class TangemPayDetailsModel @Inject constructor(
}
}
override fun onRemoveAccount() {
uiMessageSender.send(
DialogMessage(
title = resourceReference(R.string.tangempay_remove_account_alert_title),
message = resourceReference(R.string.tangempay_remove_account_alert_description),
firstActionBuilder = {
EventMessageAction(
isWarning = true,
title = resourceReference(R.string.tangempay_remove_account),
onClick = ::removeAccount,
)
},
secondActionBuilder = { cancelAction() },
),
)
}
private fun removeAccount() {
modelScope.launch {
onboardingRepository.disableTangemPay(userWalletId)
.onRight {
paymentAccountStatusFetcher.invoke(userWalletId)
router.pop()
}
.onLeft {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_error)))
}
}
}
private fun showBottomSheetError(type: TangemPayDetailsErrorType) {
uiMessageSender.send(message = TangemPayMessagesFactory.createErrorMessage(errorType = type))
}

View file

@ -34,8 +34,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.components.topFade
import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.message.TangemMessage
import com.tangem.core.ui.ds.message.TangemMessageEffect
@ -43,7 +47,10 @@ import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.shimmers.TextShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TangemPayTestTags
@ -202,22 +209,35 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
}
if (state.accountDeactivatedNotificationConfig != null) {
item("deactivationBannerBlock") {
TangemMessage(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens2.x4)
.clickableSingle(
onClick = { state.accountDeactivatedNotificationConfig.onClick?.invoke() },
),
title = state.accountDeactivatedNotificationConfig.title,
subtitle = state.accountDeactivatedNotificationConfig.subtitle,
messageEffect = TangemMessageEffect.Warning,
)
DeactivationBannerBlock(notificationConfig = state.accountDeactivatedNotificationConfig)
}
}
}
}
}
@Composable
private fun DeactivationBannerBlock(notificationConfig: NotificationConfig) {
val removeAccountButton = notificationConfig.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
TangemMessage(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
title = notificationConfig.title,
subtitle = notificationConfig.subtitle,
messageEffect = TangemMessageEffect.Warning,
buttons = removeAccountButton?.let { button ->
{
PrimaryInverseTangemButton(
text = button.text,
onClick = button.onClick,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
modifier = Modifier.weight(1f),
)
}
},
)
}
@Composable
private fun PayDetailsTopBar(
config: TangemPayDetailsTopBarConfig,

View file

@ -71,10 +71,10 @@ private fun PayContextMenuItem(item: TangemPayDropDownItemUM, onMenuDismiss: ()
Text(
text = item.title.resolveReference(),
style = TangemTheme.typography3.body.medium,
color = if (item.isEnabled) {
TangemTheme.colors3.text.primary
} else {
TangemTheme.colors3.text.tertiary
color = when {
!item.isEnabled -> TangemTheme.colors3.text.tertiary
item.titleColor != null -> item.titleColor.invoke()
else -> TangemTheme.colors3.text.primary
},
maxLines = 1,
)

View file

@ -11,4 +11,5 @@ internal interface TangemPayDetailIntents {
fun onClickTermsAndLimits()
fun onCardClick()
fun onAddCardClick()
fun onRemoveAccount()
}