diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 0e7b71c7fb..f84c6e5813 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -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" diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index 17de6c4cd0..9f911e63f2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -21,6 +21,32 @@ data class CustomerMeResponse( @Json(name = "balance") val balance: BalanceResponse?, @Json(name = "product_instances") val productInstances: List, @Json(name = "cards") val cards: List, + @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?, + ) + + @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) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 0f66cd5146..e315735ecf 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1951,6 +1951,10 @@ Pay Support Payment account Session expired + Current plan + Card related + Plan related + Change plan Invalid PIN: avoid sequences or repeats Replace card This generates a new set of card details. Your old details will stop working. You can\'t undo this. diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index 55b9d892dd..95ee815e71 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -120,6 +120,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( }, error = null, virtualAccount = VirtualAccountOnramp.None, + tariffPlan = null, ) is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( source = StatusSource.CACHE, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index e9df89acad..83c1acd7c6 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -395,6 +395,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ), error = null, virtualAccount = virtualAccount, + tariffPlan = tariffPlan, ) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt index 2fd76d8480..f88311fc8b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt @@ -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 { override fun convert(value: CustomerMeResponse.Result): CustomerInfo { @@ -45,9 +48,37 @@ internal object CustomerInfoConverter : Converter SpecificationDataType.ACCOUNT CustomerMeResponse.ProductInstance.SpecificationDataType.CARD -> SpecificationDataType.CARD } + + private fun String?.toDateTimeOrNull(): DateTime? = this?.let { runCatching { DateTime.parse(it) }.getOrNull() } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index e9699e8118..83267d850c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -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, 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 new file mode 100644 index 0000000000..d7f218caaa --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayCustomerTariffPlan.kt @@ -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 + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..47b30f630c --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/TangemPayTariffPlan.kt @@ -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, +) { + @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 + } + } + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 752773bb5f..8701ea5e53 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -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. */ diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt index 709f0b2246..8e09e1ac9d 100644 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -5,4 +5,5 @@ interface TangemPayFeatureToggles { val isCloseCardEnabled: Boolean val isRemoveAccountEnabled: Boolean val isMultipleCardsEnabled: Boolean + val isTiersPlusPlanEnabled: Boolean } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt index b4eb4b2152..958b410264 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -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) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt index de4ad91bc2..1958cf537d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt @@ -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() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index 0cc4f2aa1d..0815d4c5ca 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -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 } \ 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 f2c556b185..143aa1524c 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 @@ -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 { - 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 { + 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( 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 270a42f551..5c22c9c463 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 @@ -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 @@ -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)) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt index 4e7fdfe512..c49b26761c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt @@ -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() } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanComponent.kt new file mode 100644 index 0000000000..24cbb4cbae --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanComponent.kt @@ -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) +} \ 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 new file mode 100644 index 0000000000..e3bdc1e072 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt @@ -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() + + val state: StateFlow + 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(), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanScreen.kt new file mode 100644 index 0000000000..d9c058cd85 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanScreen.kt @@ -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 = {}, +) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanUM.kt new file mode 100644 index 0000000000..43b54b452f --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanUM.kt @@ -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
, + 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, + ) + + @Immutable + data class InfoItem( + val label: TextReference, + val value: TextReference, + ) +} \ No newline at end of file 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 b1f9098f80..50bba1b5b5 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 @@ -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() diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt index 04609c579d..b284e0e22e 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactoryTest.kt @@ -32,6 +32,7 @@ internal class TangemPayDetailsStateFactoryTest { isRedesignEnabled = true, isRemoveAccountEnabled = true, isMultipleCardsEnabled = true, + isTiersPlusPlanEnabled = true, ) @BeforeEach