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,