Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-15 04:50:41 -07:00
parent 5c293e71ec
commit 6caeea3576
22 changed files with 243 additions and 135 deletions

View file

@ -38,6 +38,7 @@ data class CustomerMeResponse(
@Json(name = "id") val id: String?,
@Json(name = "type") val type: String?,
@Json(name = "name") val name: String?,
@Json(name = "program_name") val programName: String?,
@Json(name = "description_items") val descriptionItems: List<DescriptionItem>?,
@Json(name = "images") val images: List<Image>? = null,
@Json(name = "fees") val fees: List<Fee>? = null,

View file

@ -98,15 +98,22 @@
<string name="address_book_add_address">Agregar dirección</string>
<string name="address_book_add_address_description">Agregue la dirección y seleccione la red</string>
<string name="address_book_add_contact">Añadir contacto</string>
<string name="address_book_address_copied">Dirección copiada</string>
<string name="address_book_address_taken_error">Esta dirección ya está guardada como %1$s</string>
<plurals name="address_book_addresses">
<item quantity="one">%d dirección</item>
<item quantity="other">%d direcciones</item>
</plurals>
<string name="address_book_choose_address">Seleccione una dirección</string>
<string name="address_book_clear_all_networks">Borrar todo</string>
<string name="address_book_contact">Contacto</string>
<string name="address_book_contact_name">Nombre de contacto</string>
<string name="address_book_copy_address">Copiar dirección</string>
<string name="address_book_create_success_message">Contacto guardado</string>
<string name="address_book_creating_error">No se pudo crear el contacto. Inténtelo de nuevo más tarde.</string>
<string name="address_book_delete_contact">Eliminar contacto</string>
<string name="address_book_delete_contact_description">Este contacto se eliminará de todas sus libretas de direcciones</string>
<string name="address_book_delete_last_address">"%1$s\" tiene solo una dirección. Si la eliminas, también se eliminará el contacto. ¿Continuar?"</string>
<string name="address_book_deleting_error">No se pudo eliminar el contacto. Inténtelo de nuevo más tarde.</string>
<string name="address_book_description">Gestionar contactos y direcciones</string>
<string name="address_book_discard">Descartar</string>
@ -114,11 +121,22 @@
<string name="address_book_enter_address">Introduzca la dirección</string>
<string name="address_book_invalid_address_error">Dirección no válida</string>
<string name="address_book_keep_editing">Seguir editando</string>
<string name="address_book_max_networks_alert_description">No puede crear más de 20 direcciones. Elimine una para añadir una nueva.</string>
<string name="address_book_max_networks_alert_title">No se puede agregar una nueva dirección</string>
<string name="address_book_name_empty_error">Se requiere el nombre del contacto.</string>
<string name="address_book_name_invalid_chars_error">El nombre del contacto contiene caracteres no válidos.</string>
<string name="address_book_name_max_chars_error">El nombre de contacto no debe superar los 50 caracteres</string>
<string name="address_book_name_taken_error">Ese nombre ya está en uso en esta billetera</string>
<string name="address_book_new_contact">Nuevo contacto</string>
<string name="address_book_no_contacts">Aún no hay contactos</string>
<string name="address_book_no_contacts_description">Los contactos añadidos aparecerán aquí</string>
<string name="address_book_remove_address">Eliminar dirección</string>
<string name="address_book_save_address">Guardar dirección</string>
<string name="address_book_save_contact">Guardar contacto</string>
<string name="address_book_save_to_wallet_title">Guardar en la billetera</string>
<string name="address_book_save_wallet_to_description">Este contacto se añadirá a la agenda de esta billetera.</string>
<string name="address_book_search_no_results">No se encontraron resultados.\nPruebe con otro nombre.</string>
<string name="address_book_select_all_networks">Seleccionar todo</string>
<string name="address_book_select_network">Seleccionar red</string>
<string name="address_book_title">Libreta de direcciones</string>
<string name="address_book_unsaved_changes">Cambios no guardados</string>

View file

@ -1864,6 +1864,9 @@
<string name="tangempay_card_details_add_funds">Add funds</string>
<string name="tangempay_card_details_add_funds_subtitle">Top-up options</string>
<string name="tangempay_card_details_add_to_wallet_button_text">Add to Google Wallet</string>
<string name="tangempay_card_details_awaiting_deposit_cancel_button">Cancel %1$s, move to %2$s</string>
<string name="tangempay_card_details_awaiting_deposit_subtitle">To pay monthly fee for plan and start use card</string>
<string name="tangempay_card_details_awaiting_deposit_title">Top-up your account on %1$s</string>
<string name="tangempay_card_details_card_number">Card Number</string>
<string name="tangempay_card_details_change_pin">PIN code</string>
<string name="tangempay_card_details_change_pin_success_description">The card is fully ready for payments.</string>
@ -1898,6 +1901,8 @@
<string name="tangempay_card_details_rename_card_placeholder">Card name</string>
<string name="tangempay_card_details_reveal_text">Reveal</string>
<string name="tangempay_card_details_show_details">Details</string>
<string name="tangempay_card_details_system_downgrade_subtitle">If it will remain below zero your %1$s cards will be closed on %2$s</string>
<string name="tangempay_card_details_system_downgrade_title">Top up your account shortly</string>
<string name="tangempay_card_details_title">Details</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Please try again later</string>
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
@ -1933,9 +1938,14 @@
<string name="tangempay_come_back_if_forget_pin">Come back to the app if you forget it.</string>
<string name="tangempay_common_card">Card</string>
<string name="tangempay_common_error_loading">Error loading</string>
<string name="tangempay_current_plan_active_till_notification">Your %1$s plan is active till %2$s, then we will move you to %3$s. %4$s won\'t be charged.</string>
<string name="tangempay_current_plan_change">Change plan</string>
<string name="tangempay_current_plan_fee_charged_notification">%1$s monthly fee will be charged on %2$s</string>
<string name="tangempay_current_plan_section_card">Card related</string>
<string name="tangempay_current_plan_section_plan">Plan related</string>
<string name="tangempay_current_plan_stay_button">Stay on %1$s</string>
<string name="tangempay_current_plan_stay_sheet_body">Your transition on %1$s will be canceled</string>
<string name="tangempay_current_plan_stay_sheet_title">Do you want to stay on %1$s?</string>
<string name="tangempay_current_plan_title">Current plan</string>
<string name="tangempay_daily_limit_hint" formatted="false">Set a limit from %s to %s</string>
<string name="tangempay_daily_limit_set_button">Set limits</string>
@ -2047,7 +2057,16 @@
<string name="tangempay_select_plan_btn_select">Select</string>
<string name="tangempay_select_plan_btn_upgrade">Upgrade plan</string>
<string name="tangempay_select_plan_compare">Compare plans</string>
<string name="tangempay_select_plan_confirm_downgrade_title">Your %1$s plan and %2$s cards will be active till %3$s</string>
<string name="tangempay_select_plan_confirm_point_cancel_till">You can cancel this transition till %1$s</string>
<string name="tangempay_select_plan_confirm_point_cards_closed">Your %1$s cards will be closed</string>
<string name="tangempay_select_plan_confirm_point_monthly_fee">%1$s monthly fee will be taken from your account</string>
<string name="tangempay_select_plan_confirm_point_move_on_date">On %1$s we will move you to %2$s plan</string>
<string name="tangempay_select_plan_confirm_point_no_fee">No fee applied</string>
<string name="tangempay_select_plan_confirm_point_virtual_card">You will get your virtual %1$s in minutes</string>
<string name="tangempay_select_plan_confirm_switch_title">You are switching to %1$s</string>
<string name="tangempay_select_plan_confirm_title">Confirm selection</string>
<string name="tangempay_select_plan_confirm_upgrade_title">We will issue %1$s for you</string>
<string name="tangempay_select_plan_title">Select plan</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>

View file

@ -2,16 +2,29 @@ package com.tangem.data.pay.converter
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.domain.models.account.TangemPayTariffPlan
import java.util.Locale
internal object TangemPayTariffPlanConverter {
fun convert(value: CustomerMeResponse.TariffPlan?): TangemPayTariffPlan? {
val id = value?.id ?: return null
val name = value.name ?: return null
val programName = value.programName ?: return null
// We cannot base our logic on knowledge what exact tier type is it.
// Use it only as identifier to get data from other responses
val tierId = value.type ?: return null
// Basic tier is a default tier. We can base some features on it.
// Other tiers are adjusted from admin panel. It is not guaranteed to have it in future
val isBasicTier = tierId.uppercase(Locale.US) == "BASIC"
return TangemPayTariffPlan(
id = id,
type = TangemPayTariffPlan.Type.fromString(value.type),
tierId = tierId,
isBasicTier = isBasicTier,
name = name,
programName = programName,
descriptionItems = value.descriptionItems.orEmpty().mapNotNull(::convertDescriptionItem),
images = value.images.orEmpty().mapNotNull(::convertImage),
fees = value.fees.orEmpty().mapNotNull(::convertFee),

View file

@ -27,6 +27,30 @@ internal class TangemPayTariffPlanConverterTest {
assertThat(TangemPayTariffPlanConverter.convert(value)).isNull()
}
@Test
fun `GIVEN missing type WHEN convert THEN returns null`() {
val value = tariffPlan(type = null)
assertThat(TangemPayTariffPlanConverter.convert(value)).isNull()
}
@Test
fun `GIVEN missing programName WHEN convert THEN returns null`() {
val value = tariffPlan(programName = null)
assertThat(TangemPayTariffPlanConverter.convert(value)).isNull()
}
@Test
fun `GIVEN basic tier in mixed case WHEN convert THEN isBasicTier is true`() {
val value = tariffPlan(type = "Basic")
val result = TangemPayTariffPlanConverter.convert(value)
assertThat(result?.tierId).isEqualTo("Basic")
assertThat(result?.isBasicTier).isTrue()
}
@Test
fun `GIVEN full valid plan WHEN convert THEN maps all fields`() {
// GIVEN
@ -58,8 +82,10 @@ internal class TangemPayTariffPlanConverterTest {
// THEN
val expected = TangemPayTariffPlan(
id = PLAN_ID,
type = TangemPayTariffPlan.Type.PLUS,
tierId = "PLUS",
isBasicTier = false,
name = PLAN_NAME,
programName = PROGRAM_NAME,
descriptionItems = listOf(
TangemPayTariffPlan.DescriptionItem(
section = TangemPayTariffPlan.Section.PLAN_RELATED,
@ -110,8 +136,10 @@ internal class TangemPayTariffPlanConverterTest {
// THEN
val expected = TangemPayTariffPlan(
id = PLAN_ID,
type = TangemPayTariffPlan.Type.UNKNOWN,
tierId = "SOMETHING_NEW",
isBasicTier = false,
name = PLAN_NAME,
programName = PROGRAM_NAME,
descriptionItems = listOf(
TangemPayTariffPlan.DescriptionItem(
section = TangemPayTariffPlan.Section.UNKNOWN,
@ -168,6 +196,7 @@ internal class TangemPayTariffPlanConverterTest {
id: String? = PLAN_ID,
type: String? = "BASIC",
name: String? = PLAN_NAME,
programName: String? = PROGRAM_NAME,
descriptionItems: List<CustomerMeResponse.DescriptionItem>? = null,
images: List<CustomerMeResponse.Image>? = null,
fees: List<CustomerMeResponse.Fee>? = null,
@ -175,6 +204,7 @@ internal class TangemPayTariffPlanConverterTest {
id = id,
type = type,
name = name,
programName = programName,
descriptionItems = descriptionItems,
images = images,
fees = fees,
@ -183,5 +213,6 @@ internal class TangemPayTariffPlanConverterTest {
private companion object {
const val PLAN_ID = "plan-1"
const val PLAN_NAME = "Plus"
const val PROGRAM_NAME = "program-1"
}
}

View file

@ -120,8 +120,10 @@ internal class DefaultPaymentAccountStatusFetcherTest {
private val basicPlan = TangemPayTariffPlan(
id = "plan_basic",
type = TangemPayTariffPlan.Type.BASIC,
tierId = "BASIC",
isBasicTier = true,
name = "Basic",
programName = "program_basic",
descriptionItems = emptyList(),
images = emptyList(),
fees = emptyList(),

View file

@ -91,8 +91,10 @@ internal class DefaultTariffPlanTransitionsRepositoryTest {
type = TangemPayTariffPlanTransition.Type.UPGRADE,
plan = TangemPayTariffPlan(
id = PLAN_ID,
type = TangemPayTariffPlan.Type.PLUS,
tierId = "PLUS",
isBasicTier = false,
name = PLAN_NAME,
programName = PROGRAM_NAME,
descriptionItems = emptyList(),
images = emptyList(),
fees = emptyList(),
@ -187,6 +189,7 @@ internal class DefaultTariffPlanTransitionsRepositoryTest {
id = id,
type = "PLUS",
name = PLAN_NAME,
programName = PROGRAM_NAME,
descriptionItems = null,
images = null,
fees = null,
@ -197,6 +200,7 @@ internal class DefaultTariffPlanTransitionsRepositoryTest {
const val AUTH_HEADER = "auth-header"
const val PLAN_ID = "plan-plus"
const val PLAN_NAME = "Plus"
const val PROGRAM_NAME = "program-plus"
const val PENDING_PLAN_ID = "plan-basic"
}
}

View file

@ -8,8 +8,10 @@ import java.util.Locale
@Serializable
data class TangemPayTariffPlan(
@SerialName("id") val id: String,
@SerialName("type") val type: Type,
@SerialName("tier_id") val tierId: String,
@SerialName("is_basic_tier") val isBasicTier: Boolean,
@SerialName("name") val name: String,
@SerialName("program_name") val programName: String,
@SerialName("description_items") val descriptionItems: List<DescriptionItem>,
@SerialName("images") val images: List<Image> = emptyList(),
@SerialName("fees") val fees: List<Fee> = emptyList(),
@ -100,31 +102,6 @@ data class TangemPayTariffPlan(
}
}
@Serializable
enum class Type {
@SerialName("BASIC")
BASIC,
@SerialName("PLUS")
PLUS,
@SerialName("PLUS_FF")
PLUS_FF,
@SerialName("UNKNOWN")
UNKNOWN,
;
companion object {
fun fromString(value: String?) = when (value?.uppercase(Locale.US)) {
"BASIC" -> BASIC
"PLUS" -> PLUS
"PLUS_FF" -> PLUS_FF
else -> UNKNOWN
}
}
}
@Serializable
enum class Section {
@SerialName("CARD_RELATED")

View file

@ -153,14 +153,18 @@ internal class GetTangemPayTariffPlanStateUseCaseTest {
val CURRENT_PLAN = TangemPayTariffPlan(
id = "plan-basic",
type = TangemPayTariffPlan.Type.BASIC,
tierId = "BASIC",
isBasicTier = true,
name = "Basic",
programName = "program-basic",
descriptionItems = emptyList(),
)
val TARGET_PLAN = TangemPayTariffPlan(
id = TARGET_PLAN_ID,
type = TangemPayTariffPlan.Type.PLUS,
tierId = "PLUS",
isBasicTier = false,
name = "Plus",
programName = "program-plus",
descriptionItems = emptyList(),
)
val CUSTOMER_TARIFF = TangemPayCustomerTariffPlan(

View file

@ -1,10 +1,8 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.tangem.domain.models.account.TangemPayTariffPlan
/** Cashback program tier, mapped once from the domain and shared by the rate tile and the details sheet. */
internal data class CashbackTier(
val planType: TangemPayTariffPlan.Type,
val tierId: String,
val rate: Int?,
val label: String,
val scope: String,

View file

@ -12,17 +12,13 @@ internal class TangemPayCashbackInfoTilesConverter(
) {
// TODO([REDACTED_TASK_KEY]): move hardcoded strings to string resources
fun convert(
tiers: List<CashbackTier>,
currentPlanType: TangemPayTariffPlan.Type,
currentPlanName: String?,
): TangemPayCashbackInfoTilesUM {
val rate = tiers.selectTier(currentPlanType)?.rate
fun convert(tiers: List<CashbackTier>, currentPlan: TangemPayTariffPlan?): TangemPayCashbackInfoTilesUM {
val rate = tiers.selectTier(currentPlan?.tierId)?.rate
return TangemPayCashbackInfoTilesUM(
rate = TangemPayCashbackInfoTilesUM.Tile(
iconRes = R.drawable.ic_percent_24,
title = stringReference(if (rate != null) "Cashback $rate%" else "Cashback"),
subtitle = currentPlanName?.let { stringReference("With your $it plan") } ?: TextReference.EMPTY,
subtitle = currentPlan?.name?.let { stringReference("With your $it plan") } ?: TextReference.EMPTY,
onClick = onRateClick,
),
accruals = TangemPayCashbackInfoTilesUM.Tile(
@ -34,9 +30,7 @@ internal class TangemPayCashbackInfoTilesConverter(
)
}
private fun List<CashbackTier>.selectTier(planType: TangemPayTariffPlan.Type): CashbackTier? {
return firstOrNull {
planType != TangemPayTariffPlan.Type.UNKNOWN && it.planType == planType
} ?: firstOrNull()
private fun List<CashbackTier>.selectTier(tierId: String?): CashbackTier? {
return firstOrNull { it.tierId == tierId } ?: firstOrNull()
}
}

View file

@ -86,8 +86,7 @@ internal class TangemPayCashbackModel @Inject constructor(
infoTiles = promotions?.let {
infoTilesConverter.convert(
tiers = tiers,
currentPlanType = plan?.type ?: TangemPayTariffPlan.Type.UNKNOWN,
currentPlanName = plan?.name,
currentPlan = plan,
)
},
)

View file

@ -4,7 +4,6 @@ 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.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.pay.model.CashbackPromotions
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
@ -14,7 +13,7 @@ internal class TangemPayCashbackTiersConverter : Converter<CashbackPromotions, L
override fun convert(value: CashbackPromotions): List<CashbackTier> {
return value.cardTiers.map { tier ->
CashbackTier(
planType = TangemPayTariffPlan.Type.fromString(tier.tier),
tierId = tier.tier,
rate = CashbackRates.forTier(tier.tier),
label = tier.label,
scope = tier.scope,

View file

@ -2,7 +2,7 @@ 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.core.ui.extensions.wrappedList
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
import com.tangem.domain.models.account.TangemPayTariffPlanState
@ -37,7 +37,6 @@ internal class TangemPayDetailsNotificationFactory(
},
)
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
fun createAwaitingDepositConfig(tariffPlan: TangemPayTariffPlanState?): NotificationConfig? {
if (!isTiersPlusPlanEnabled) return null
if (tariffPlan == null) return null
@ -52,13 +51,15 @@ internal class TangemPayDetailsNotificationFactory(
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"),
title = resourceReference(R.string.tangempay_card_details_awaiting_deposit_title, wrappedList(feeText)),
subtitle = resourceReference(R.string.tangempay_card_details_awaiting_deposit_subtitle),
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = stringReference("Cancel ${orderStep.toPlan.name}, move to ${orderStep.fromPlan.name}"),
text = resourceReference(
R.string.tangempay_card_details_awaiting_deposit_cancel_button,
wrappedList(orderStep.toPlan.name, orderStep.fromPlan.name),
),
onClick = { intents.onCancelPlusTransition(order.orderId) },
),
)
@ -81,16 +82,18 @@ internal class TangemPayDetailsNotificationFactory(
iconResId = if (isRedesignEnabled) R.drawable.ic_alert_circle_24 else R.drawable.img_attention_20,
)
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
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"),
title = resourceReference(R.string.tangempay_card_details_system_downgrade_title),
subtitle = resourceReference(
R.string.tangempay_card_details_system_downgrade_subtitle,
wrappedList(planName, date),
),
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = stringReference("Add funds"),
text = resourceReference(R.string.tangempay_card_details_add_funds),
onClick = intents::onClickAddFunds,
),
)

View file

@ -12,7 +12,6 @@ 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
import com.tangem.domain.models.pay.TangemPayCardFrozenState
@ -286,10 +285,7 @@ internal class TangemPayDetailsStateFactory(
),
)
}
if (isTiersPlusPlanEnabled &&
tariffPlan != null &&
tariffPlan.tariff.plan.type != TangemPayTariffPlan.Type.BASIC
) {
if (isTiersPlusPlanEnabled && tariffPlan != null && !tariffPlan.tariff.plan.isBasicTier) {
add(
TangemPayDropDownItemUM(
title = resourceReference(R.string.tangempay_visa_benefits),

View file

@ -7,6 +7,7 @@ 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.extensions.wrappedList
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
@ -50,7 +51,6 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan(params.tariffPlan)) },
)
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
private fun createNotification(customerPlan: TangemPayCustomerTariffPlan): TangemPayCurrentPlanUM.Notification? {
val date = customerPlan.formatNextBillingDateOrNull(formatter = DateTimeFormatters.dateMMMd) ?: return null
val feeText = customerPlan.plan.formatRecurringFeeOrNull() ?: return null
@ -58,19 +58,25 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
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.",
text = resourceReference(
R.string.tangempay_current_plan_active_till_notification,
wrappedList(customerPlan.plan.name, date, targetPlan.name, feeText),
),
button = TangemPayCurrentPlanUM.Notification.Button(
text = stringReference("Stay on ${customerPlan.plan.name}"),
text = resourceReference(
R.string.tangempay_current_plan_stay_button,
wrappedList(customerPlan.plan.name),
),
isProcessing = isProcessing,
onClick = ::onStayOnPlanClick,
),
)
}
TangemPayCustomerTariffPlan.Status.ACTIVE -> TangemPayCurrentPlanUM.Notification(
text = stringReference("$feeText monthly fee will be charged on $date"),
text = resourceReference(
R.string.tangempay_current_plan_fee_charged_notification,
wrappedList(feeText, date),
),
)
else -> null
}

View file

@ -9,6 +9,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
@ -189,28 +190,34 @@ internal class TangemPaySelectPlanModel @Inject constructor(
)
}
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
private fun buildConfirmContent(): TangemPaySelectPlanUM.Content {
val transition = allowedTransitions.getOrNull(selectedIndex) ?: return buildSelectContent()
val targetProgramme = transition.plan.name
val targetPlanName = transition.plan.name
val programName = transition.plan.programName
return TangemPaySelectPlanUM.Content.Confirm(
title = when (transition.type) {
TangemPayTariffPlanTransition.Type.UPGRADE -> stringReference(
"We will issue Visa $targetProgramme for you",
TangemPayTariffPlanTransition.Type.UPGRADE -> resourceReference(
R.string.tangempay_select_plan_confirm_upgrade_title,
wrappedList(programName),
)
TangemPayTariffPlanTransition.Type.DOWNGRADE -> {
val nextBillingDate = nextBillingDate()
if (nextBillingDate != null) {
val programName = "UNKNOWN" // TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
val planName = params.tariffPlan.plan.name
stringReference(
"Your $planName plan and $programName cards will be active till $nextBillingDate",
resourceReference(
R.string.tangempay_select_plan_confirm_downgrade_title,
wrappedList(params.tariffPlan.plan.name, programName, nextBillingDate),
)
} else {
stringReference("You are switching to $targetProgramme")
resourceReference(
R.string.tangempay_select_plan_confirm_switch_title,
wrappedList(targetPlanName),
)
}
}
else -> stringReference("You are switching to $targetProgramme")
else -> resourceReference(
R.string.tangempay_select_plan_confirm_switch_title,
wrappedList(targetPlanName),
)
},
points = buildConfirmPoints(transition),
confirmButtonText = resourceReference(
@ -226,18 +233,27 @@ internal class TangemPaySelectPlanModel @Inject constructor(
)
}
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
private fun buildConfirmPoints(
transition: TangemPayTariffPlanTransition,
): ImmutableList<TangemPaySelectPlanUM.PointUM> {
val programName = "UNKNOWN" // TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
val programName = transition.plan.programName
return when (transition.type) {
TangemPayTariffPlanTransition.Type.UPGRADE -> {
val feeText = transition.plan.formatRecurringFeeOrNull()
buildList {
add("You will get your virtual Visa $programName in minutes")
add(
resourceReference(
R.string.tangempay_select_plan_confirm_point_virtual_card,
wrappedList(programName),
),
)
if (feeText != null) {
add("$feeText monthly fee will be taken from your account")
add(
resourceReference(
R.string.tangempay_select_plan_confirm_point_monthly_fee,
wrappedList(feeText),
),
)
}
}
}
@ -245,18 +261,33 @@ internal class TangemPaySelectPlanModel @Inject constructor(
val date = nextBillingDate()
buildList {
if (date != null) {
add("On $date we will move you to ${transition.plan.name} plan")
add(
resourceReference(
R.string.tangempay_select_plan_confirm_point_move_on_date,
wrappedList(date, transition.plan.name),
),
)
}
add("Your Visa $programName cards will be closed")
add(
resourceReference(
R.string.tangempay_select_plan_confirm_point_cards_closed,
wrappedList(programName),
),
)
if (date != null) {
add("You can cancel this transition till $date")
add(
resourceReference(
R.string.tangempay_select_plan_confirm_point_cancel_till,
wrappedList(date),
),
)
}
add("No fee applied")
add(resourceReference(R.string.tangempay_select_plan_confirm_point_no_fee))
}
}
else -> listOf("No fee applied")
else -> listOf(resourceReference(R.string.tangempay_select_plan_confirm_point_no_fee))
}
.map { TangemPaySelectPlanUM.PointUM(title = stringReference(it), body = null) }
.map { TangemPaySelectPlanUM.PointUM(title = it, body = null) }
.toImmutableList()
}

View file

@ -4,7 +4,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.bottomSheetMessage
@ -149,7 +149,6 @@ internal object TangemPayMessagesFactory {
}
}
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
fun createStayOnPlanMessage(
planName: String,
targetPlanName: String,
@ -161,15 +160,18 @@ internal object TangemPayMessagesFactory {
type = MessageBottomSheetUM.Icon.Type.Informative
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Informative
}
title = stringReference("Do you want to stay on $planName?")
body = stringReference("Your transition on $targetPlanName will be canceled")
title = resourceReference(R.string.tangempay_current_plan_stay_sheet_title, wrappedList(planName))
body = resourceReference(
R.string.tangempay_current_plan_stay_sheet_body,
wrappedList(targetPlanName),
)
}
secondaryButton {
text = resourceReference(R.string.common_cancel)
onClick { closeBs() }
}
primaryButton {
text = stringReference("Stay on $planName")
text = resourceReference(R.string.tangempay_current_plan_stay_button, wrappedList(planName))
onClick {
onStayClick()
closeBs()

View file

@ -2,7 +2,6 @@ package com.tangem.features.tangempay.cashback.impl.model
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.TangemPayTariffPlan
import org.junit.jupiter.api.Test
internal class TangemPayCashbackDetailsConverterTest {
@ -81,7 +80,7 @@ internal class TangemPayCashbackDetailsConverterTest {
min: String? = null,
cap: String? = null,
) = CashbackTier(
planType = TangemPayTariffPlan.Type.UNKNOWN,
tierId = "basic",
rate = rate,
label = label,
scope = scope,

View file

@ -21,7 +21,7 @@ internal class TangemPayCashbackInfoTilesConverterTest {
@MethodSource("rateSelectionModels")
fun `GIVEN plan WHEN convert THEN rate tile shows the tier rate and the plan subtitle`(model: RateSelectionModel) {
// Act
val result = converter.convert(twoTiers(), model.plan, model.planName)
val result = converter.convert(twoTiers(), model.plan)
// Assert
assertThat(result.rate.title).isEqualTo(stringReference(model.expectedTitle))
@ -29,9 +29,9 @@ internal class TangemPayCashbackInfoTilesConverterTest {
}
@Test
fun `GIVEN empty tiers and unknown plan WHEN convert THEN rate tile has no percent and empty subtitle`() {
fun `GIVEN empty tiers and no plan WHEN convert THEN rate tile has no percent and empty subtitle`() {
// Act
val result = converter.convert(emptyList(), TangemPayTariffPlan.Type.UNKNOWN, currentPlanName = null)
val result = converter.convert(emptyList(), currentPlan = null)
// Assert
assertThat(result.rate.title).isEqualTo(stringReference("Cashback"))
@ -41,7 +41,7 @@ internal class TangemPayCashbackInfoTilesConverterTest {
@Test
fun `GIVEN any tiers WHEN convert THEN accruals tile is static`() {
// Act
val result = converter.convert(twoTiers(), TangemPayTariffPlan.Type.BASIC, currentPlanName = "Basic")
val result = converter.convert(twoTiers(), plan(tierId = "basic", name = "Basic"))
// Assert
assertThat(result.accruals.title).isEqualTo(stringReference("Accruals"))
@ -50,43 +50,52 @@ internal class TangemPayCashbackInfoTilesConverterTest {
private fun rateSelectionModels() = listOf(
RateSelectionModel(
plan = TangemPayTariffPlan.Type.PLUS,
planName = "Plus",
plan = plan(tierId = "plus", name = "Plus"),
expectedTitle = "Cashback 2%",
expectedSubtitle = stringReference("With your Plus plan"),
),
RateSelectionModel(
plan = TangemPayTariffPlan.Type.BASIC,
planName = "Basic",
plan = plan(tierId = "basic", name = "Basic"),
expectedTitle = "Cashback 1%",
expectedSubtitle = stringReference("With your Basic plan"),
),
RateSelectionModel(
plan = TangemPayTariffPlan.Type.UNKNOWN,
planName = null,
plan = plan(tierId = "gold", name = "Gold"),
expectedTitle = "Cashback 1%",
expectedSubtitle = stringReference("With your Gold plan"),
),
RateSelectionModel(
plan = null,
expectedTitle = "Cashback 1%",
expectedSubtitle = TextReference.EMPTY,
),
)
private fun twoTiers() = listOf(
tier(TangemPayTariffPlan.Type.BASIC, rate = 1),
tier(TangemPayTariffPlan.Type.PLUS, rate = 2),
tier(tierId = "basic", rate = 1),
tier(tierId = "plus", rate = 2),
)
private fun tier(planType: TangemPayTariffPlan.Type = TangemPayTariffPlan.Type.UNKNOWN, rate: Int? = null) =
CashbackTier(
planType = planType,
rate = rate,
label = "label",
scope = "scope",
minPurchase = null,
monthlyCap = null,
)
private fun tier(tierId: String, rate: Int? = null) = CashbackTier(
tierId = tierId,
rate = rate,
label = "label",
scope = "scope",
minPurchase = null,
monthlyCap = null,
)
private fun plan(tierId: String, name: String) = TangemPayTariffPlan(
id = "plan-$tierId",
tierId = tierId,
isBasicTier = tierId == "basic",
name = name,
programName = "program",
descriptionItems = emptyList(),
)
data class RateSelectionModel(
val plan: TangemPayTariffPlan.Type,
val planName: String?,
val plan: TangemPayTariffPlan?,
val expectedTitle: String,
val expectedSubtitle: TextReference,
)

View file

@ -45,7 +45,7 @@ internal class TangemPayCashbackModelTest {
coEvery { cashbackRepository.getCashbackPromotions(any()) } returns promotions().right()
coEvery { cashbackRepository.getCashbackAccrualDocs(any()) } returns docs().right()
coEvery { onboardingRepository.getCustomerInfo(any()) } returns
customerInfo(TangemPayTariffPlan.Type.BASIC).right()
customerInfo(tierId = "basic", planName = "Basic").right()
}
@AfterEach
@ -69,7 +69,7 @@ internal class TangemPayCashbackModelTest {
fun `GIVEN PLUS plan WHEN model created THEN rate tile shows the Plus tier rate`() {
// Arrange
coEvery { onboardingRepository.getCustomerInfo(any()) } returns
customerInfo(TangemPayTariffPlan.Type.PLUS).right()
customerInfo(tierId = "plus", planName = "Plus").right()
// Act
val model = createModel()
@ -162,12 +162,16 @@ internal class TangemPayCashbackModelTest {
CashbackDocument(id = "terms", title = "Full terms of cashback program", url = "https://x/terms.pdf"),
)
private fun customerInfo(planType: TangemPayTariffPlan.Type): CustomerInfo {
val tariffPlanMock = mockk<TangemPayTariffPlan> {
every { type } returns planType
every { name } returns planType.name
}
val customerTariffPlanMock = mockk<TangemPayCustomerTariffPlan> { every { plan } returns tariffPlanMock }
private fun customerInfo(tierId: String, planName: String): CustomerInfo {
val currentPlan = TangemPayTariffPlan(
id = "plan-$tierId",
tierId = tierId,
isBasicTier = tierId == "basic",
name = planName,
programName = "program",
descriptionItems = emptyList(),
)
val customerTariffPlanMock = mockk<TangemPayCustomerTariffPlan> { every { plan } returns currentPlan }
return mockk { every { tariffPlan } returns customerTariffPlanMock }
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.pay.model.CashbackPromotions
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@ -38,7 +37,7 @@ internal class TangemPayCashbackTiersConverterTest {
// Assert
assertThat(result).containsExactly(
CashbackTier(
planType = TangemPayTariffPlan.Type.BASIC,
tierId = "basic",
rate = 1,
label = "Basic cards",
scope = "All purchases",
@ -46,7 +45,7 @@ internal class TangemPayCashbackTiersConverterTest {
monthlyCap = "$100",
),
CashbackTier(
planType = TangemPayTariffPlan.Type.PLUS,
tierId = "plus",
rate = 2,
label = "Plus cards",
scope = "Everywhere",
@ -57,7 +56,7 @@ internal class TangemPayCashbackTiersConverterTest {
}
@Test
fun `GIVEN unknown tier WHEN convert THEN rate null and plan type UNKNOWN`() {
fun `GIVEN unknown tier WHEN convert THEN rate is null and tier id kept`() {
// Arrange
val promotions = promotions(tier(id = "gold", label = "Gold", scope = "All", min = null, cap = null))
@ -67,7 +66,7 @@ internal class TangemPayCashbackTiersConverterTest {
// Assert
assertThat(result).containsExactly(
CashbackTier(
planType = TangemPayTariffPlan.Type.UNKNOWN,
tierId = "gold",
rate = null,
label = "Gold",
scope = "All",