From ae9eb162f67823ba7a9a16bb1be60eeff361dd4a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Jul 2026 02:57:52 -0700 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../core/ui/utils/DateTimeFormatters.kt | 10 ++ .../account/TangemPayCustomerTariffPlan.kt | 8 ++ .../models/account/TangemPayTariffPlan.kt | 4 + .../features/tangempay/TangemPayConstants.kt | 16 +++ .../TangemPayDetailsNotificationFactory.kt | 98 +++++++++++++++++++ .../entity/TangemPayDetailsStateFactory.kt | 98 ++++++------------- .../tangempay/entity/TangemPayDetailsUM.kt | 1 + .../tangempay/model/TangemPayDetailsModel.kt | 12 ++- .../transformers/DetailsBalanceTransformer.kt | 1 + ... TangemPayErrorNotificationTransformer.kt} | 2 +- .../features/tangempay/tiers/TiersUtils.kt | 22 +++++ .../current/TangemPayCurrentPlanModel.kt | 39 +++++++- .../tiers/select/TangemPaySelectPlanModel.kt | 39 +++++--- .../tangempay/ui/TangemPayDetailsScreen.kt | 1 + .../tangempay/ui/TangemPayDetailsScreenV2.kt | 9 +- .../tangempay/utils/TangemPayDetailIntents.kt | 1 + .../TangemPayActionButtonsTransformerTest.kt | 1 + 18 files changed, 269 insertions(+), 94 deletions(-) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt rename features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/{TangemPayRenewSessionTransformer.kt => TangemPayErrorNotificationTransformer.kt} (93%) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/TiersUtils.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f0a128eed2..b237dfe401 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -2045,6 +2045,7 @@ Use crypto from your wallet to top up your payment account From your Tangem Wallet USDC on Polygon network + Visa Benefits Funds from refunded purchases won’t be returned your on-chain Polygon balance or be available for withdrawal, but will stay on your card balance for purchases Please note Your PIN code diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 1b575284be..5a45b12535 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -84,6 +84,16 @@ object DateTimeFormatters { getBestFormatterBySkeleton("MMM d, yyyy") } + + val dateMMMd: DateTimeFormatter by lazy { + getBestFormatterBySkeleton("MMM d") + } + + + val dateMMMMdYYYY: DateTimeFormatter by lazy { + getBestFormatterBySkeleton("MMMM d, yyyy") + } + /** * Example: "2020" */ diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayCustomerTariffPlan.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayCustomerTariffPlan.kt index d7f218caaa..83915ff9f2 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayCustomerTariffPlan.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayCustomerTariffPlan.kt @@ -34,6 +34,12 @@ data class TangemPayCustomerTariffPlan( @SerialName("CANCELED") CANCELED, + @SerialName("SYSTEM_DOWNGRADE_PENDING") + SYSTEM_DOWNGRADE_PENDING, + + @SerialName("DOWNGRADE_PENDING") + DOWNGRADE_PENDING, + @SerialName("UNKNOWN") UNKNOWN, ; @@ -43,6 +49,8 @@ data class TangemPayCustomerTariffPlan( "ACTIVE" -> ACTIVE "TRANSITIONING" -> TRANSITIONING "CANCELED" -> CANCELED + "SYSTEM_DOWNGRADE_PENDING" -> SYSTEM_DOWNGRADE_PENDING + "DOWNGRADE_PENDING" -> DOWNGRADE_PENDING else -> UNKNOWN } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayTariffPlan.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayTariffPlan.kt index 094d113ea0..912bba0988 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayTariffPlan.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayTariffPlan.kt @@ -133,6 +133,9 @@ data class TangemPayTariffPlan( @SerialName("PLAN_RELATED") PLAN_RELATED, + @SerialName("ONBOARDING_RELATED") + ONBOARDING_RELATED, + @SerialName("UNKNOWN") UNKNOWN, ; @@ -141,6 +144,7 @@ data class TangemPayTariffPlan( fun fromString(value: String?) = when (value?.uppercase(Locale.US)) { "CARD_RELATED" -> CARD_RELATED "PLAN_RELATED" -> PLAN_RELATED + "ONBOARDING_RELATED" -> ONBOARDING_RELATED else -> UNKNOWN } } diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayConstants.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayConstants.kt index 5a9903bfd6..30c6606e2a 100644 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayConstants.kt +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayConstants.kt @@ -1,5 +1,21 @@ package com.tangem.features.tangempay +import com.tangem.utils.SupportedLanguages + object TangemPayConstants { const val TERMS_AND_LIMITS_LINK = "https://tangem.com/docs/en/tangem-visa-tariffs.pdf" + + fun visaBenefitsLink(): String { + val lang = SupportedLanguages.getCurrentSupportedLanguageCode() + val segment = when (lang) { + "es" -> "es" + "pt" -> "pt" + "ja" -> "ja" + "zh" -> "zh-Hans" + "fr" -> "fr" + "de" -> "de" + else -> "en" + } + return "https://tangem.com/$segment/tangem-pay/visa-benefits/" + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt new file mode 100644 index 0000000000..3d522360b7 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt @@ -0,0 +1,98 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.account.TangemPayCustomerTariffPlan +import com.tangem.domain.models.account.TangemPayTariffPlanState +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.tiers.formatNextBillingDateOrNull +import com.tangem.features.tangempay.tiers.formatRecurringFeeOrNull +import com.tangem.features.tangempay.utils.TangemPayDetailIntents + +internal class TangemPayDetailsNotificationFactory( + private val intents: TangemPayDetailIntents, + private val isRemoveAccountEnabled: Boolean, + private val isTiersPlusPlanEnabled: Boolean, + private val isRedesignEnabled: Boolean, +) { + fun createErrorConfig(error: PaymentAccountStatusValue.Error?): NotificationConfig? = when (error) { + null -> null + PaymentAccountStatusValue.Error.NotSynced -> createRenewSessionNotificationConfig(isRedesignEnabled) + else -> createAccountUnavailableConfig(isRedesignEnabled) + } + + fun createAccountDeactivatedConfig(isRedesignEnabled: Boolean) = NotificationConfig( + title = resourceReference(R.string.tangempay_account_deactivated_message_title), + subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle), + iconResId = if (isRedesignEnabled) R.drawable.ic_alert_circle_24 else R.drawable.img_attention_20, + buttonsState = if (isRemoveAccountEnabled) { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.tangempay_remove_account), + onClick = intents::onRemoveAccount, + ) + } else { + null + }, + ) + + // TODO v_rodionov: strings hardcoded for now - wait for localization + fun createAwaitingDepositConfig(tariffPlan: TangemPayTariffPlanState?): NotificationConfig? { + if (!isTiersPlusPlanEnabled) return null + if (tariffPlan == null) return null + + if (tariffPlan.tariff.status == TangemPayCustomerTariffPlan.Status.SYSTEM_DOWNGRADE_PENDING) { + return createTariffSystemDownGradePendingConfig(tariffPlan) + } + + val order = tariffPlan.order ?: return null + val orderStep = order.step + if (orderStep !is TangemPayTariffPlanState.OrderStep.AwaitingDeposit) return null + + val feeText = orderStep.toPlan.formatRecurringFeeOrNull() ?: return null + + val title = "Top-up your account on $feeText" + return NotificationConfig( + title = stringReference(title), + subtitle = stringReference("To pay monthly fee for plan and start use card"), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Cancel ${orderStep.toPlan.name}, move to ${orderStep.fromPlan.name}"), + onClick = { intents.onCancelPlusTransition(order.orderId) }, + ), + ) + } + + private fun createRenewSessionNotificationConfig(isRedesignEnabled: Boolean) = NotificationConfig( + title = resourceReference(R.string.tangempay_sync_needed_title), + subtitle = resourceReference(R.string.tangempay_sync_needed_body), + iconResId = if (isRedesignEnabled) 0 else R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.tangempay_sync_needed_button), + onClick = intents::onRenewSession, + iconResId = R.drawable.ic_tangem_24, + ), + ) + + private fun createAccountUnavailableConfig(isRedesignEnabled: Boolean) = NotificationConfig( + title = resourceReference(R.string.tangempay_temporarily_unavailable), + subtitle = resourceReference(R.string.tangempay_service_unreachable_try_later), + iconResId = if (isRedesignEnabled) R.drawable.ic_alert_circle_24 else R.drawable.img_attention_20, + ) + + // TODO v_rodionov: strings hardcoded for now - wait for localization + private fun createTariffSystemDownGradePendingConfig(tariffPlan: TangemPayTariffPlanState): NotificationConfig? { + val date = tariffPlan.tariff.formatNextBillingDateOrNull() ?: return null + val planName = tariffPlan.tariff.plan.name + return NotificationConfig( + title = stringReference("Top up your account shortly"), + subtitle = stringReference("If it will remain below zero your $planName cards will be closed on $date"), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Add funds"), + onClick = intents::onClickAddFunds, + ), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index b3c3ba3882..dfda075cd1 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -3,19 +3,15 @@ package com.tangem.features.tangempay.entity import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.themedColor -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode -import com.tangem.core.ui.format.bigdecimal.optionalDecimals import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.ic_document_20 import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.account.TangemPayCustomerTariffPlan import com.tangem.domain.models.account.TangemPayTariffPlan import com.tangem.domain.models.account.TangemPayTariffPlanState import com.tangem.domain.models.pay.TangemPayCard @@ -41,6 +37,13 @@ internal class TangemPayDetailsStateFactory( private val isMultipleCardsEnabled: Boolean, private val isTiersPlusPlanEnabled: Boolean, ) { + private val notificationFactory = TangemPayDetailsNotificationFactory( + intents = intents, + isRemoveAccountEnabled = isRemoveAccountEnabled, + isTiersPlusPlanEnabled = isTiersPlusPlanEnabled, + isRedesignEnabled = isRedesignEnabled, + ) + fun getLoadingState(): TangemPayDetailsUM { return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( @@ -75,6 +78,8 @@ internal class TangemPayDetailsStateFactory( val isAddCardEnabled = isFresh && !hasIssuingCard val areActionButtonsEnabled = isFresh && hasUnfrozenCard val hasWithdrawableBalance = status.balance.hasWithdrawableAmount + val errorNotification = notificationFactory.createErrorConfig(status.error) + val awaitingDepositNotification = notificationFactory.createAwaitingDepositConfig(status.tariffPlan) return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( onBackClick = onBack, @@ -111,17 +116,11 @@ internal class TangemPayDetailsStateFactory( ), isBalanceHidden = false, addToWalletBlockState = null, - errorNotificationConfig = createErrorConfig(status.error) ?: createAwaitingDepositConfig(status.tariffPlan), + errorNotificationConfig = errorNotification ?: awaitingDepositNotification, accountDeactivatedNotificationConfig = null, ) } - private fun createErrorConfig(error: PaymentAccountStatusValue.Error?): NotificationConfig? = when (error) { - null -> null - PaymentAccountStatusValue.Error.NotSynced -> createRenewSessionNotificationConfig(isRedesignEnabled) - else -> createAccountUnavailableConfig(isRedesignEnabled) - } - private fun List.resolveProgressBanner(): CardsProgressBannerUM? = when { any { it.state == TangemPayCardState.Reissuing } -> CardsProgressBannerUM.Reissuing any { it.state == TangemPayCardState.Issuing } -> CardsProgressBannerUM.Issuing @@ -129,6 +128,7 @@ internal class TangemPayDetailsStateFactory( } fun getDeactivatedState(hasWithdrawableBalance: Boolean): TangemPayDetailsUM { + val accountDeactivatedNotification = notificationFactory.createAccountDeactivatedConfig(isRedesignEnabled) return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( onBackClick = onBack, @@ -150,65 +150,10 @@ internal class TangemPayDetailsStateFactory( isBalanceHidden = false, addToWalletBlockState = null, errorNotificationConfig = null, - accountDeactivatedNotificationConfig = createAccountDeactivatedConfig(isRedesignEnabled), + accountDeactivatedNotificationConfig = accountDeactivatedNotification, ) } - private fun createAccountUnavailableConfig(isRedesignEnabled: Boolean) = NotificationConfig( - title = resourceReference(R.string.tangempay_temporarily_unavailable), - subtitle = resourceReference(R.string.tangempay_service_unreachable_try_later), - iconResId = if (isRedesignEnabled) R.drawable.ic_alert_circle_24 else R.drawable.img_attention_20, - ) - - private fun createAccountDeactivatedConfig(isRedesignEnabled: Boolean) = NotificationConfig( - title = resourceReference(R.string.tangempay_account_deactivated_message_title), - subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle), - iconResId = if (isRedesignEnabled) R.drawable.ic_alert_circle_24 else R.drawable.img_attention_20, - buttonsState = if (isRemoveAccountEnabled) { - NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.tangempay_remove_account), - onClick = intents::onRemoveAccount, - ) - } else { - null - }, - ) - - // TODO v_rodionov: strings hardcoded for now - wait for localization - private fun createAwaitingDepositConfig(tariffPlan: TangemPayTariffPlanState?): NotificationConfig? { - val order = tariffPlan?.order ?: return null - - val orderStep = order.step - if (orderStep !is TangemPayTariffPlanState.OrderStep.AwaitingDeposit) return null - - val recurringFee = orderStep.toPlan.fees.find { it.type == TangemPayTariffPlan.Fee.Type.RECURRING } - val feeText = recurringFee?.let { fee -> - val currency = getJavaCurrencyByCode(fee.currency) - fee.amount.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() } - } - val title = if (feeText != null) "Top-up your account on $feeText" else "Top-up your account" - return NotificationConfig( - title = stringReference(title), - subtitle = stringReference("To pay monthly fee for plan and start use card"), - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = stringReference("Cancel ${orderStep.toPlan.name}, move to ${orderStep.fromPlan.name}"), - onClick = { intents.onCancelPlusTransition(order.orderId) }, - ), - ) - } - - private fun createRenewSessionNotificationConfig(isRedesignEnabled: Boolean) = NotificationConfig( - title = resourceReference(R.string.tangempay_sync_needed_title), - subtitle = resourceReference(R.string.tangempay_sync_needed_body), - iconResId = if (isRedesignEnabled) 0 else R.drawable.img_attention_20, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.tangempay_sync_needed_button), - onClick = intents::onRenewSession, - iconResId = R.drawable.ic_tangem_24, - ), - ) - private fun getTopBarMenuItems(): ImmutableList { return persistentListOf( TangemDropdownMenuItem( @@ -285,6 +230,23 @@ internal class TangemPayDetailsStateFactory( tintReference = { TangemTheme.colors3.icon.primary }, ), subtitle = stringReference(tariffPlan.tariff.plan.name), + isEnabled = tariffPlan.order?.step !is TangemPayTariffPlanState.OrderStep.AwaitingDeposit && + tariffPlan.tariff.status != TangemPayCustomerTariffPlan.Status.TRANSITIONING, + ), + ) + } + if (isTiersPlusPlanEnabled && + tariffPlan != null && + tariffPlan.tariff.plan.type != TangemPayTariffPlan.Type.BASIC + ) { + add( + TangemPayDropDownItemUM( + title = resourceReference(R.string.tangempay_visa_benefits), + onClick = intents::onClickVisaBenefits, + icon = TangemIconUM.Icon( + iconRes = CoreUiR.drawable.ic_heart_20, + tintReference = { TangemTheme.colors3.icon.primary }, + ), ), ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index 53e147e2b9..5363e0b205 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -93,6 +93,7 @@ internal sealed class TangemPayDetailsBalanceBlockState { override val cardsBlockState: CardsBlockState?, val fiatBalance: TextReference, val isBalanceFlickering: Boolean, + val isNegative: Boolean, val isMuted: Boolean = false, ) : TangemPayDetailsBalanceBlockState() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 21294660a1..2b5f382a09 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -173,11 +173,13 @@ internal class TangemPayDetailsModel @Inject constructor( } override fun onCancelPlusTransition(orderId: String) { + uiState.update(TangemPayErrorNotificationTransformer(shouldShowProgress = true)) modelScope.launch { cancelTangemPayOrderUseCase(userWalletId = userWalletId, orderId = orderId) .onLeft { uiMessageSender.send(TangemPayMessagesFactory.createGenericError()) } + uiState.update(TangemPayErrorNotificationTransformer(shouldShowProgress = false)) } } @@ -444,6 +446,10 @@ internal class TangemPayDetailsModel @Inject constructor( urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) } + override fun onClickVisaBenefits() { + urlOpener.openUrl(TangemPayConstants.visaBenefitsLink()) + } + override fun onClickCurrentPlan(tariffPlan: TangemPayCustomerTariffPlan) { router.push(TangemPayAccountDetailsInnerRoute.CurrentPlan(tariffPlan)) } @@ -504,16 +510,16 @@ internal class TangemPayDetailsModel @Inject constructor( } override fun onRenewSession() { - uiState.update(TangemPayRenewSessionTransformer(shouldShowProgress = true)) + uiState.update(TangemPayErrorNotificationTransformer(shouldShowProgress = true)) modelScope.launch { produceTangemPayInitialDataUseCase(userWalletId) .onRight { paymentAccountStatusFetcher.invoke(userWalletId) - uiState.update(TangemPayRenewSessionTransformer(shouldShowProgress = false)) + uiState.update(TangemPayErrorNotificationTransformer(shouldShowProgress = false)) } .onLeft { uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_error))) - uiState.update(TangemPayRenewSessionTransformer(shouldShowProgress = false)) + uiState.update(TangemPayErrorNotificationTransformer(shouldShowProgress = false)) } } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt index 97b8c574ec..f5a0fba8be 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt @@ -22,6 +22,7 @@ internal class DetailsBalanceTransformer( actionButtons = prevState.balanceBlockState.actionButtons, cardsBlockState = prevState.balanceBlockState.cardsBlockState, isMuted = isMuted, + isNegative = fiatBalance.availableBalance.signum() < 0, ) return prevState.copy(balanceBlockState = balance) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayRenewSessionTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayErrorNotificationTransformer.kt similarity index 93% rename from features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayRenewSessionTransformer.kt rename to features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayErrorNotificationTransformer.kt index f7efa293a6..46c67040dd 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayRenewSessionTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayErrorNotificationTransformer.kt @@ -4,7 +4,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.utils.transformer.Transformer -internal class TangemPayRenewSessionTransformer( +internal class TangemPayErrorNotificationTransformer( private val shouldShowProgress: Boolean, ) : Transformer { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/TiersUtils.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/TiersUtils.kt new file mode 100644 index 0000000000..a82b5f1ab4 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/TiersUtils.kt @@ -0,0 +1,22 @@ +package com.tangem.features.tangempay.tiers + +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode +import com.tangem.core.ui.format.bigdecimal.optionalDecimals +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.models.account.TangemPayCustomerTariffPlan +import com.tangem.domain.models.account.TangemPayTariffPlan +import org.joda.time.format.DateTimeFormatter + +fun TangemPayTariffPlan.formatRecurringFeeOrNull(): String? { + val fee = fees.find { it.type == TangemPayTariffPlan.Fee.Type.RECURRING } ?: return null + val currency = getJavaCurrencyByCode(fee.currency) + return fee.amount.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() } +} + +fun TangemPayCustomerTariffPlan.formatNextBillingDateOrNull( + formatter: DateTimeFormatter = DateTimeFormatters.dateMMMMdYYYY, +): String? { + return nextBillingAt?.let { DateTimeFormatters.formatDate(it, formatter) } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt index 5cba793686..a7ad0f5ca4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt @@ -7,9 +7,13 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.models.account.TangemPayCustomerTariffPlan import com.tangem.domain.models.account.TangemPayTariffPlan import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute +import com.tangem.features.tangempay.tiers.formatNextBillingDateOrNull +import com.tangem.features.tangempay.tiers.formatRecurringFeeOrNull import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -28,16 +32,41 @@ internal class TangemPayCurrentPlanModel @Inject constructor( private val params = paramsContainer.require() val state: StateFlow - field = MutableStateFlow(createState(params.tariffPlan.plan)) + field = MutableStateFlow(createState(params.tariffPlan)) - private fun createState(plan: TangemPayTariffPlan): TangemPayCurrentPlanUM = TangemPayCurrentPlanUM( - planName = stringReference(plan.name), - notification = null, - sections = buildSections(plan), + private fun createState(customerPlan: TangemPayCustomerTariffPlan): TangemPayCurrentPlanUM = TangemPayCurrentPlanUM( + planName = stringReference(customerPlan.plan.name), + notification = createNotification(customerPlan), + sections = buildSections(customerPlan.plan), onBackClick = router::pop, onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan) }, ) + // TODO v_rodionov: strings hardcoded for now - wait for localization + private fun createNotification(customerPlan: TangemPayCustomerTariffPlan): TangemPayCurrentPlanUM.Notification? { + val date = customerPlan.formatNextBillingDateOrNull(formatter = DateTimeFormatters.dateMMMd) ?: return null + val feeText = customerPlan.plan.formatRecurringFeeOrNull() ?: return null + return when (customerPlan.status) { + TangemPayCustomerTariffPlan.Status.DOWNGRADE_PENDING -> { + val targetPlan = customerPlan.pendingPlan ?: return null + TangemPayCurrentPlanUM.Notification( + text = stringReference( + "Your ${customerPlan.plan.name} plan is active till $date, then we will move you to " + + "${targetPlan.name}. $feeText won't be charged.", + ), + button = TangemPayCurrentPlanUM.Notification.Button( + text = stringReference("Stay on ${customerPlan.plan.name}"), + onClick = {}, // TODO v_rodionov: #[REDACTED_TASK_KEY] - Downgrade + ), + ) + } + TangemPayCustomerTariffPlan.Status.ACTIVE -> TangemPayCurrentPlanUM.Notification( + text = stringReference("$feeText monthly fee will be charged on $date"), + ) + else -> null + } + } + private fun buildSections(plan: TangemPayTariffPlan) = persistentListOf( sectionOf(plan, TangemPayTariffPlan.Section.CARD_RELATED, R.string.tangempay_current_plan_section_card), sectionOf(plan, TangemPayTariffPlan.Section.PLAN_RELATED, R.string.tangempay_current_plan_section_plan), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt index ed80558a98..f62f83147c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt @@ -14,6 +14,7 @@ import com.tangem.domain.pay.usecase.CreateTariffPlanTransitionOrderUseCase import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanTransitionsUseCase import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute +import com.tangem.features.tangempay.tiers.formatRecurringFeeOrNull import com.tangem.features.tangempay.utils.TangemPayMessagesFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList @@ -38,6 +39,10 @@ internal class TangemPaySelectPlanModel @Inject constructor( private val params = paramsContainer.require() private var transitions: List = emptyList() + + private val allowedTransitions: List + get() = transitions.filter { it.type in ALLOWED_TYPES } + private var selectedIndex: Int = 0 private var isConfirm: Boolean = false private var isProcessing: Boolean = false @@ -52,7 +57,7 @@ internal class TangemPaySelectPlanModel @Inject constructor( private fun loadTransitions() { modelScope.launch { getTransitions(params.userWalletId).onRight { result -> - transitions = result.filter { it.type in ALLOWED_TYPES } + transitions = result state.update { buildState() } } } @@ -65,13 +70,13 @@ internal class TangemPaySelectPlanModel @Inject constructor( } private fun onSelectClick() { - if (transitions.isEmpty()) return + if (allowedTransitions.isEmpty()) return isConfirm = true state.update { buildState() } } private fun onComparePlansClick() { - if (transitions.isEmpty()) return + if (allowedTransitions.isEmpty()) return state.update { buildState(showPlanCompare = true) } } @@ -90,8 +95,11 @@ internal class TangemPaySelectPlanModel @Inject constructor( } private fun onConfirmClick() { - val transition = transitions.getOrNull(selectedIndex) ?: return + val transition = allowedTransitions.getOrNull(selectedIndex) ?: return + + // TODO v_rodionov: #[REDACTED_TASK_KEY] - Downgrade if (transition.type != TangemPayTariffPlanTransition.Type.UPGRADE) return + if (isProcessing) return isProcessing = true @@ -118,7 +126,7 @@ internal class TangemPaySelectPlanModel @Inject constructor( } else { resourceReference(R.string.tangempay_select_plan_title) }, - plans = transitions.map { it.plan.toPlanUM() }.toImmutableList(), + plans = allowedTransitions.map { it.plan.toPlanUM() }.toImmutableList(), selectedIndex = selectedIndex, onPlanSelected = ::onPlanSelected, onBackClick = ::onBackClick, @@ -135,14 +143,16 @@ internal class TangemPaySelectPlanModel @Inject constructor( private fun buildCompare(): TangemPaySelectPlanUM.ComparePlans { val plans = transitions.map { it.plan } val orderedTitles = plans - .flatMap { it.descriptionItems } + .flatMap { plan -> plan.descriptionItems.filter { it.section in COMPARE_SECTIONS } } .sortedWith(compareBy({ it.section.ordinal }, { it.order })) .map { it.title } .distinct() return TangemPaySelectPlanUM.ComparePlans( attributes = orderedTitles.map(::stringReference).toImmutableList(), plans = plans.map { plan -> - val valueByTitle = plan.descriptionItems.associate { it.title to it.body } + val valueByTitle = plan.descriptionItems + .filter { it.section in COMPARE_SECTIONS } + .associate { it.title to it.body } TangemPaySelectPlanUM.ComparePlans.Plan( name = stringReference(plan.name), values = orderedTitles @@ -154,8 +164,8 @@ internal class TangemPaySelectPlanModel @Inject constructor( ) } - private fun buildConfirmContent(): TangemPaySelectPlanUM.Content.Confirm { - val transition = transitions[selectedIndex] + private fun buildConfirmContent(): TangemPaySelectPlanUM.Content { + val transition = allowedTransitions.getOrNull(selectedIndex) ?: return buildSelectContent() val planName = transition.plan.name val isUpgrade = transition.type == TangemPayTariffPlanTransition.Type.UPGRADE return TangemPaySelectPlanUM.Content.Confirm( @@ -181,9 +191,7 @@ internal class TangemPaySelectPlanModel @Inject constructor( planName: String, isUpgrade: Boolean, ): ImmutableList = if (isUpgrade) { - val feeText = transition.plan.descriptionItems - .firstOrNull { it.section == TangemPayTariffPlan.Section.PLAN_RELATED } - ?.title + val feeText = transition.plan.formatRecurringFeeOrNull() listOf( stringReference("You will get your virtual Visa $planName in minutes"), if (feeText != null) { @@ -205,7 +213,8 @@ internal class TangemPaySelectPlanModel @Inject constructor( name = stringReference(name), imageUrl = images.firstOrNull { it.type == TangemPayTariffPlan.Image.Type.MAIN }?.url, points = descriptionItems - .sortedWith(compareBy({ it.section.ordinal }, { it.order })) + .filter { it.section == TangemPayTariffPlan.Section.ONBOARDING_RELATED } + .sortedBy { it.order } .map { item -> TangemPaySelectPlanUM.PointUM( title = stringReference(item.title), @@ -220,5 +229,9 @@ internal class TangemPaySelectPlanModel @Inject constructor( TangemPayTariffPlanTransition.Type.UPGRADE, TangemPayTariffPlanTransition.Type.DOWNGRADE, ) + private val COMPARE_SECTIONS = setOf( + TangemPayTariffPlan.Section.CARD_RELATED, + TangemPayTariffPlan.Section.PLAN_RELATED, + ) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index ea3be706fd..c2ebc93549 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -507,6 +507,7 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider { - val balanceColor = if (animatedState.isMuted) { - TangemTheme.colors3.text.secondary - } else { - TangemTheme.colors3.text.primary + val balanceColor = when { + animatedState.isMuted -> TangemTheme.colors3.text.secondary + animatedState.isNegative -> TangemTheme.colors3.text.status.error + else -> TangemTheme.colors3.text.primary } Text( modifier = Modifier.testTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt index f3b98c9141..dd7d0208ec 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt @@ -10,6 +10,7 @@ internal interface TangemPayDetailIntents { fun onClickAddFunds() fun onClickWithdraw() fun onClickTermsAndLimits() + fun onClickVisaBenefits() fun onClickCurrentPlan(tariffPlan: TangemPayCustomerTariffPlan) fun onCancelPlusTransition(orderId: String) fun onCardClick(cardId: String) diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayActionButtonsTransformerTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayActionButtonsTransformerTest.kt index 8e4494200a..1aa9be44ee 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayActionButtonsTransformerTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayActionButtonsTransformerTest.kt @@ -60,6 +60,7 @@ internal class TangemPayActionButtonsTransformerTest { cardsBlockState = null, fiatBalance = TextReference.EMPTY, isBalanceFlickering = false, + isNegative = false, ), addToWalletBlockState = null, isBalanceHidden = false,