Updated on 2026-08-14
This commit is contained in:
parent
3fefb7bf52
commit
ff958f6bba
23 changed files with 631 additions and 24 deletions
|
|
@ -131,6 +131,10 @@
|
|||
"name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_16041_VISA_TIERS_PLUS_PLAN",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_83_ADDRESS_BOOK_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,32 @@ data class CustomerMeResponse(
|
|||
@Json(name = "balance") val balance: BalanceResponse?,
|
||||
@Json(name = "product_instances") val productInstances: List<ProductInstance>,
|
||||
@Json(name = "cards") val cards: List<Card>,
|
||||
@Json(name = "customer_tariff_plan") val customerTariffPlan: CustomerTariffPlan? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerTariffPlan(
|
||||
@Json(name = "status") val status: String?,
|
||||
@Json(name = "next_billing_at") val nextBillingAt: String?,
|
||||
@Json(name = "pending_transition_at") val pendingTransitionAt: String?,
|
||||
@Json(name = "tariff_plan") val tariffPlan: TariffPlan?,
|
||||
@Json(name = "pending_tariff_plan") val pendingTariffPlan: TariffPlan?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlan(
|
||||
@Json(name = "id") val id: String?,
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "name") val name: String?,
|
||||
@Json(name = "description_items") val descriptionItems: List<DescriptionItem>?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DescriptionItem(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "order") val order: Int?,
|
||||
@Json(name = "title") val title: String?,
|
||||
@Json(name = "body") val body: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -1951,6 +1951,10 @@
|
|||
<string name="tangempay_pay_support">Pay Support</string>
|
||||
<string name="tangempay_payment_account">Payment account</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Session expired</string>
|
||||
<string name="tangempay_current_plan_title">Current plan</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_change">Change plan</string>
|
||||
<string name="tangempay_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</string>
|
||||
<string name="tangempay_reissue_card_confirm">Replace card</string>
|
||||
<string name="tangempay_reissue_card_description">This generates a new set of card details. Your old details will stop working. You can\'t undo this.</string>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
},
|
||||
error = null,
|
||||
virtualAccount = VirtualAccountOnramp.None,
|
||||
tariffPlan = null,
|
||||
)
|
||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.CACHE,
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
),
|
||||
error = null,
|
||||
virtualAccount = virtualAccount,
|
||||
tariffPlan = tariffPlan,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
|||
import com.tangem.datasource.api.pay.models.response.FiatBalance
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
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.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
|
|
@ -18,6 +20,7 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.SpecificationDat
|
|||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, CustomerInfo> {
|
||||
override fun convert(value: CustomerMeResponse.Result): CustomerInfo {
|
||||
|
|
@ -45,9 +48,37 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
fiatBalance = fiatBalance?.toDomain(),
|
||||
cryptoBalance = cryptoBalance?.toDomain(),
|
||||
availableForWithdrawal = value.balance?.availableForWithdrawal?.amount.orZero(),
|
||||
tariffPlan = value.customerTariffPlan?.toDomain(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.CustomerTariffPlan.toDomain(): TangemPayCustomerTariffPlan? {
|
||||
val plan = tariffPlan?.toDomain() ?: return null
|
||||
return TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.fromString(status),
|
||||
plan = plan,
|
||||
nextBillingAt = nextBillingAt.toDateTimeOrNull(),
|
||||
pendingPlan = pendingTariffPlan?.toDomain(),
|
||||
pendingTransitionAt = pendingTransitionAt.toDateTimeOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.TariffPlan.toDomain(): TangemPayTariffPlan? {
|
||||
val name = name ?: return null
|
||||
return TangemPayTariffPlan(
|
||||
type = TangemPayTariffPlan.Type.fromString(type),
|
||||
name = name,
|
||||
descriptionItems = descriptionItems.orEmpty().map { it.toDomain() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.DescriptionItem.toDomain() = TangemPayTariffPlan.DescriptionItem(
|
||||
section = TangemPayTariffPlan.Section.fromString(type),
|
||||
order = order ?: 0,
|
||||
title = title.orEmpty(),
|
||||
body = body.orEmpty(),
|
||||
)
|
||||
|
||||
private fun CustomerMeResponse.ProductInstance.toDomain(): ProductInstance {
|
||||
val status = status.toDomain()
|
||||
val cardFrozenState = when (status) {
|
||||
|
|
@ -116,4 +147,6 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
CustomerMeResponse.ProductInstance.SpecificationDataType.ACCOUNT -> SpecificationDataType.ACCOUNT
|
||||
CustomerMeResponse.ProductInstance.SpecificationDataType.CARD -> SpecificationDataType.CARD
|
||||
}
|
||||
|
||||
private fun String?.toDateTimeOrNull(): DateTime? = this?.let { runCatching { DateTime.parse(it) }.getOrNull() }
|
||||
}
|
||||
|
|
@ -151,6 +151,8 @@ sealed class PaymentAccountStatusValue {
|
|||
* (see [copySealed]), or `null` when the status is up to date. Not persisted.
|
||||
* @property virtualAccount Virtual Account (Visa on-ramp) availability — VA MVP0 (TWI-1638).
|
||||
* Transient: not persisted in the local cache.
|
||||
* @property tariffPlan Current tariff plan with subscription data (Tiers).
|
||||
* Transient: not persisted in the local cache.
|
||||
*/
|
||||
@Serializable
|
||||
data class Loaded(
|
||||
|
|
@ -163,6 +165,7 @@ sealed class PaymentAccountStatusValue {
|
|||
val fiatRate: SerializedBigDecimal?,
|
||||
val error: Error?,
|
||||
val virtualAccount: VirtualAccountOnramp,
|
||||
val tariffPlan: TangemPayCustomerTariffPlan?,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Customer's current tariff plan.
|
||||
*
|
||||
* @property status Lifecycle status of the subscription.
|
||||
* @property plan The currently active plan ([TangemPayTariffPlan]).
|
||||
* @property nextBillingAt When the next plan fee is charged; `null` for free plans.
|
||||
* @property pendingPlan Plan the customer will be moved to (scheduled downgrade), or `null`.
|
||||
* @property pendingTransitionAt When [pendingPlan] is applied, or `null`.
|
||||
*/
|
||||
@Serializable
|
||||
data class TangemPayCustomerTariffPlan(
|
||||
@SerialName("status") val status: Status,
|
||||
@SerialName("plan") val plan: TangemPayTariffPlan,
|
||||
@SerialName("next_billing_at") val nextBillingAt: SerializedDateTime?,
|
||||
@SerialName("pending_plan") val pendingPlan: TangemPayTariffPlan?,
|
||||
@SerialName("pending_transition_at") val pendingTransitionAt: SerializedDateTime?,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
enum class Status {
|
||||
@SerialName("ACTIVE")
|
||||
ACTIVE,
|
||||
|
||||
@SerialName("TRANSITIONING")
|
||||
TRANSITIONING,
|
||||
|
||||
@SerialName("CANCELED")
|
||||
CANCELED,
|
||||
|
||||
@SerialName("UNKNOWN")
|
||||
UNKNOWN,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String?) = when (value?.uppercase(Locale.US)) {
|
||||
"ACTIVE" -> ACTIVE
|
||||
"TRANSITIONING" -> TRANSITIONING
|
||||
"CANCELED" -> CANCELED
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class TangemPayTariffPlan(
|
||||
@SerialName("type") val type: Type,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("description_items") val descriptionItems: List<DescriptionItem>,
|
||||
) {
|
||||
@Serializable
|
||||
data class DescriptionItem(
|
||||
@SerialName("section") val section: Section,
|
||||
@SerialName("order") val order: Int,
|
||||
@SerialName("title") val title: String,
|
||||
@SerialName("body") val body: String,
|
||||
)
|
||||
|
||||
@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")
|
||||
CARD_RELATED,
|
||||
|
||||
@SerialName("PLAN_RELATED")
|
||||
PLAN_RELATED,
|
||||
|
||||
@SerialName("UNKNOWN")
|
||||
UNKNOWN,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String?) = when (value?.uppercase(Locale.US)) {
|
||||
"CARD_RELATED" -> CARD_RELATED
|
||||
"PLAN_RELATED" -> PLAN_RELATED
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.pay.model
|
|||
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
|
|
@ -30,6 +31,7 @@ data class CustomerInfo(
|
|||
val fiatBalance: PaymentAccountStatusValue.FiatBalance?,
|
||||
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance?,
|
||||
val availableForWithdrawal: BigDecimal,
|
||||
val tariffPlan: TangemPayCustomerTariffPlan?,
|
||||
) {
|
||||
|
||||
/** Transitional single-card accessor — returns the first product instance, or null if none. */
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ interface TangemPayFeatureToggles {
|
|||
val isCloseCardEnabled: Boolean
|
||||
val isRemoveAccountEnabled: Boolean
|
||||
val isMultipleCardsEnabled: Boolean
|
||||
val isTiersPlusPlanEnabled: Boolean
|
||||
}
|
||||
|
|
@ -18,4 +18,7 @@ internal class DefaultTangemPayFeatureToggles(
|
|||
|
||||
override val isMultipleCardsEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15235_VISA_MULTIPLE_CARDS)
|
||||
|
||||
override val isTiersPlusPlanEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16041_VISA_TIERS_PLUS_PLAN)
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanComponent
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
|
|
@ -83,6 +84,12 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
userWalletId = params.initialStatus.userWalletId,
|
||||
),
|
||||
)
|
||||
is TangemPayAccountDetailsInnerRoute.CurrentPlan -> TangemPayCurrentPlanComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPayCurrentPlanComponent.Params(
|
||||
tariffPlan = config.tariffPlan,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.features.tangempay.closure.TangemPayCloseCardModel
|
||||
import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupModel
|
||||
import com.tangem.features.tangempay.model.*
|
||||
import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -79,4 +80,9 @@ internal interface TangemPayModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(TangemPayCardLimitSetupModel::class)
|
||||
fun bindTangemPayCardLimitSetupModel(model: TangemPayCardLimitSetupModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayCurrentPlanModel::class)
|
||||
fun bindTangemPayCurrentPlanModel(model: TangemPayCurrentPlanModel): Model
|
||||
}
|
||||
|
|
@ -6,11 +6,13 @@ 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.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.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
|
|
@ -32,6 +34,7 @@ internal class TangemPayDetailsStateFactory(
|
|||
private val isRedesignEnabled: Boolean,
|
||||
private val isRemoveAccountEnabled: Boolean,
|
||||
private val isMultipleCardsEnabled: Boolean,
|
||||
private val isTiersPlusPlanEnabled: Boolean,
|
||||
) {
|
||||
fun getLoadingState(): TangemPayDetailsUM {
|
||||
return TangemPayDetailsUM(
|
||||
|
|
@ -39,7 +42,7 @@ internal class TangemPayDetailsStateFactory(
|
|||
onBackClick = onBack,
|
||||
onOpenMenu = onOpenMenu,
|
||||
items = getTopBarMenuItems(),
|
||||
itemsV2 = getTopBarMenuItemsV2(),
|
||||
itemsV2 = getTopBarMenuItemsV2(tariffPlan = null),
|
||||
),
|
||||
pullToRefreshConfig = PullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
|
|
@ -72,7 +75,7 @@ internal class TangemPayDetailsStateFactory(
|
|||
onBackClick = onBack,
|
||||
onOpenMenu = onOpenMenu,
|
||||
items = getTopBarMenuItems(),
|
||||
itemsV2 = getTopBarMenuItemsV2(),
|
||||
itemsV2 = getTopBarMenuItemsV2(tariffPlan = status.tariffPlan),
|
||||
),
|
||||
pullToRefreshConfig = PullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
|
|
@ -239,29 +242,44 @@ internal class TangemPayDetailsStateFactory(
|
|||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun getTopBarMenuItemsV2(): ImmutableList<TangemPayDropDownItemUM> {
|
||||
return persistentListOf(
|
||||
TangemPayDropDownItemUM(
|
||||
title = resourceReference(R.string.tangem_pay_terms_limits),
|
||||
onClick = intents::onClickTermsAndLimits,
|
||||
icon = TangemIconUM.Icon(
|
||||
imageVector = Icons.ic_document_20,
|
||||
tintReference = {
|
||||
TangemTheme.colors3.icon.primary
|
||||
},
|
||||
private fun getTopBarMenuItemsV2(
|
||||
tariffPlan: TangemPayCustomerTariffPlan?,
|
||||
): ImmutableList<TangemPayDropDownItemUM> {
|
||||
return buildList {
|
||||
if (isTiersPlusPlanEnabled && tariffPlan != null) {
|
||||
add(
|
||||
TangemPayDropDownItemUM(
|
||||
title = resourceReference(R.string.tangempay_current_plan_title),
|
||||
onClick = { intents.onClickCurrentPlan(tariffPlan) },
|
||||
icon = TangemIconUM.Icon(
|
||||
iconRes = CoreUiR.drawable.ic_information_24,
|
||||
tintReference = { TangemTheme.colors3.icon.primary },
|
||||
),
|
||||
subtitle = stringReference(tariffPlan.plan.name),
|
||||
),
|
||||
)
|
||||
}
|
||||
add(
|
||||
TangemPayDropDownItemUM(
|
||||
title = resourceReference(R.string.tangem_pay_terms_limits),
|
||||
onClick = intents::onClickTermsAndLimits,
|
||||
icon = TangemIconUM.Icon(
|
||||
imageVector = Icons.ic_document_20,
|
||||
tintReference = { TangemTheme.colors3.icon.primary },
|
||||
),
|
||||
),
|
||||
),
|
||||
TangemPayDropDownItemUM(
|
||||
title = resourceReference(R.string.tangempay_pay_support),
|
||||
onClick = intents::onContactSupportClicked,
|
||||
icon = TangemIconUM.Icon(
|
||||
iconRes = R.drawable.ic_mail_20,
|
||||
tintReference = {
|
||||
TangemTheme.colors3.icon.primary
|
||||
},
|
||||
)
|
||||
add(
|
||||
TangemPayDropDownItemUM(
|
||||
title = resourceReference(R.string.tangempay_pay_support),
|
||||
onClick = intents::onContactSupportClicked,
|
||||
icon = TangemIconUM.Icon(
|
||||
iconRes = R.drawable.ic_mail_20,
|
||||
tintReference = { TangemTheme.colors3.icon.primary },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun getActionButtonsConfig(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
|
|||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
|
|
@ -62,7 +63,7 @@ import kotlinx.coroutines.launch
|
|||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayDetailsModel @Inject constructor(
|
||||
|
|
@ -109,6 +110,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
isRedesignEnabled = isRedesignEnabled(),
|
||||
isRemoveAccountEnabled = tangemPayFeatureToggles.isRemoveAccountEnabled,
|
||||
isMultipleCardsEnabled = isMultipleCardsEnabled,
|
||||
isTiersPlusPlanEnabled = tangemPayFeatureToggles.isTiersPlusPlanEnabled,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<TangemPayDetailsUM>
|
||||
|
|
@ -380,6 +382,10 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
|
||||
}
|
||||
|
||||
override fun onClickCurrentPlan(tariffPlan: TangemPayCustomerTariffPlan) {
|
||||
router.push(TangemPayAccountDetailsInnerRoute.CurrentPlan(tariffPlan))
|
||||
}
|
||||
|
||||
override fun onCardClick(cardId: String) {
|
||||
analytics.send(TangemPayAnalyticsEvents.CardIconClicked())
|
||||
router.push(TangemPayAccountDetailsInnerRoute.CardDetails(cardId = cardId))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.navigation
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -14,4 +15,9 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data class AddToWallet(val card: TangemPayCard) : TangemPayAccountDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data class CurrentPlan(
|
||||
val tariffPlan: TangemPayCustomerTariffPlan,
|
||||
) : TangemPayAccountDetailsInnerRoute()
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.tangempay.tiers.current
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
|
||||
internal class TangemPayCurrentPlanComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TangemPayCurrentPlanModel = getOrCreateModel(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
TangemPayCurrentPlanScreen(state = state, modifier = modifier)
|
||||
}
|
||||
|
||||
data class Params(val tariffPlan: TangemPayCustomerTariffPlan)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.features.tangempay.tiers.current
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
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.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayCurrentPlanModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayCurrentPlanComponent.Params>()
|
||||
|
||||
val state: StateFlow<TangemPayCurrentPlanUM>
|
||||
field = MutableStateFlow(createState(params.tariffPlan.plan))
|
||||
|
||||
private fun createState(plan: TangemPayTariffPlan): TangemPayCurrentPlanUM = TangemPayCurrentPlanUM(
|
||||
planName = stringReference(plan.name),
|
||||
notification = null,
|
||||
sections = buildSections(plan),
|
||||
onBackClick = router::pop,
|
||||
onChangePlanClick = {},
|
||||
)
|
||||
|
||||
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),
|
||||
)
|
||||
.filter { it.items.isNotEmpty() }
|
||||
.toImmutableList()
|
||||
|
||||
private fun sectionOf(
|
||||
plan: TangemPayTariffPlan,
|
||||
section: TangemPayTariffPlan.Section,
|
||||
headerStrRes: Int,
|
||||
): TangemPayCurrentPlanUM.Section {
|
||||
return TangemPayCurrentPlanUM.Section(
|
||||
header = resourceReference(headerStrRes),
|
||||
items = plan.descriptionItems
|
||||
.filter { it.section == section }
|
||||
.sortedBy { it.order }
|
||||
.map { item ->
|
||||
TangemPayCurrentPlanUM.InfoItem(
|
||||
label = stringReference(item.title),
|
||||
value = stringReference(item.body),
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
package com.tangem.features.tangempay.tiers.current
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayCurrentPlanScreen(state: TangemPayCurrentPlanUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors3.bg.primary),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
CurrentPlanTopBar(state = state)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 12.dp, bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
state.notification?.let { PlanNotification(notification = it) }
|
||||
state.sections.fastForEach { section -> PlanSection(section = section) }
|
||||
}
|
||||
|
||||
ChangePlanFooter(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CurrentPlanTopBar(state: TangemPayCurrentPlanUM) {
|
||||
TangemTopBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
title = resourceReference(R.string.tangempay_current_plan_title),
|
||||
subtitle = state.planName,
|
||||
startContent = {
|
||||
TangemButton(
|
||||
iconStart = TangemIconUM.Icon(iconRes = CoreUiR.drawable.ic_arrow_back_28),
|
||||
onClick = state.onBackClick,
|
||||
size = TangemButton.Size.X11,
|
||||
variant = TangemButton.Variant.Material,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlanNotification(notification: TangemPayCurrentPlanUM.Notification, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors3.bg.status.infoSubtle)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = CoreUiR.drawable.ic_information_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.status.info,
|
||||
)
|
||||
Text(
|
||||
text = notification.text.resolveReference(),
|
||||
style = TangemTheme.typography3.subheading.medium,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
)
|
||||
}
|
||||
notification.button?.let { button ->
|
||||
TangemButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
variant = TangemButton.Variant.Secondary,
|
||||
size = TangemButton.Size.X11,
|
||||
text = button.text,
|
||||
onClick = button.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlanSection(section: TangemPayCurrentPlanUM.Section, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 20.dp,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
text = section.header.resolveReference(),
|
||||
style = TangemTheme.typography3.subheading.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(TangemTheme.colors3.bg.secondary),
|
||||
) {
|
||||
section.items.fastForEachIndexed { index, item ->
|
||||
PlanInfoRow(item = item)
|
||||
if (index != section.items.lastIndex) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = TangemTheme.colors3.border.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlanInfoRow(item: TangemPayCurrentPlanUM.InfoItem, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = item.label.resolveReference(),
|
||||
style = TangemTheme.typography3.caption.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = item.value.resolveReference(),
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChangePlanFooter(state: TangemPayCurrentPlanUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
variant = TangemButton.Variant.Primary,
|
||||
size = TangemButton.Size.X12,
|
||||
text = resourceReference(R.string.tangempay_current_plan_change),
|
||||
onClick = state.onChangePlanClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO)
|
||||
@Composable
|
||||
private fun TangemPayCurrentPlanScreenPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
TangemPayCurrentPlanScreen(state = previewState())
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewState() = TangemPayCurrentPlanUM(
|
||||
planName = stringReference("Plus"),
|
||||
notification = TangemPayCurrentPlanUM.Notification(
|
||||
text = stringReference(
|
||||
"Your Plus plan is active till Mar 23, then we will move you to Basic. $29.99 won't be charged.",
|
||||
),
|
||||
button = TangemPayCurrentPlanUM.Notification.Button(
|
||||
text = stringReference("Stay on Plus"),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
sections = persistentListOf(
|
||||
TangemPayCurrentPlanUM.Section(
|
||||
header = stringReference("Card related"),
|
||||
items = persistentListOf(
|
||||
TangemPayCurrentPlanUM.InfoItem(stringReference("Visa Programme"), stringReference("Signature")),
|
||||
TangemPayCurrentPlanUM.InfoItem(
|
||||
label = stringReference("Max daily spending limit"),
|
||||
value = stringReference("$50.000"),
|
||||
),
|
||||
TangemPayCurrentPlanUM.InfoItem(stringReference("FX fee"), stringReference("1%")),
|
||||
),
|
||||
),
|
||||
TangemPayCurrentPlanUM.Section(
|
||||
header = stringReference("Plan related"),
|
||||
items = persistentListOf(
|
||||
TangemPayCurrentPlanUM.InfoItem(stringReference("Plan fee"), stringReference("\$29.99/month")),
|
||||
TangemPayCurrentPlanUM.InfoItem(stringReference("Max cards issued"), stringReference("5")),
|
||||
TangemPayCurrentPlanUM.InfoItem(
|
||||
label = stringReference("Additional benefits"),
|
||||
value = stringReference("Benefit 1, Benefit 2, Benefit 3"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onBackClick = {},
|
||||
onChangePlanClick = {},
|
||||
)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.tangempay.tiers.current
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class TangemPayCurrentPlanUM(
|
||||
val planName: TextReference,
|
||||
val notification: Notification?,
|
||||
val sections: ImmutableList<Section>,
|
||||
val onBackClick: () -> Unit,
|
||||
val onChangePlanClick: () -> Unit,
|
||||
) {
|
||||
@Immutable
|
||||
data class Notification(
|
||||
val text: TextReference,
|
||||
val button: Button? = null,
|
||||
) {
|
||||
@Immutable
|
||||
data class Button(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Section(
|
||||
val header: TextReference,
|
||||
val items: ImmutableList<InfoItem>,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class InfoItem(
|
||||
val label: TextReference,
|
||||
val value: TextReference,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.utils
|
||||
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
|
||||
internal interface TangemPayDetailIntents {
|
||||
fun onContactSupportClicked()
|
||||
|
|
@ -9,6 +10,7 @@ internal interface TangemPayDetailIntents {
|
|||
fun onClickAddFunds()
|
||||
fun onClickWithdraw()
|
||||
fun onClickTermsAndLimits()
|
||||
fun onClickCurrentPlan(tariffPlan: TangemPayCustomerTariffPlan)
|
||||
fun onCardClick(cardId: String)
|
||||
fun onAddCardClick()
|
||||
fun onRemoveAccount()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ internal class TangemPayDetailsStateFactoryTest {
|
|||
isRedesignEnabled = true,
|
||||
isRemoveAccountEnabled = true,
|
||||
isMultipleCardsEnabled = true,
|
||||
isTiersPlusPlanEnabled = true,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue