Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 01:17:12 -07:00
parent 3b6eeb7535
commit 8b677a8e6b
25 changed files with 1149 additions and 17 deletions

View file

@ -1,13 +1,17 @@
package com.tangem.tap.data package com.tangem.tap.data
import android.content.Context
import android.os.Build import android.os.Build
import com.tangem.utils.info.AppInfoProvider import com.tangem.utils.info.AppInfoProvider
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import javax.inject.Inject import javax.inject.Inject
internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider { internal class DefaultAppInfoProvider @Inject constructor(
@ApplicationContext private val context: Context,
) : AppInfoProvider {
override val platform: String override val platform: String
get() = "Android" get() = "Android"
override val device: String override val device: String
@ -18,6 +22,8 @@ internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
get() = Build.VERSION.SDK_INT get() = Build.VERSION.SDK_INT
override val language: String override val language: String
get() = Locale.getDefault().toLanguageTag() get() = Locale.getDefault().toLanguageTag()
override val deviceScale: Float
get() = context.resources.displayMetrics.density
override val timezone: String override val timezone: String
get() = TimeZone.getDefault().id get() = TimeZone.getDefault().id
override val appVersion: String = BuildConfig.VERSION_NAME override val appVersion: String = BuildConfig.VERSION_NAME

View file

@ -55,6 +55,7 @@ internal sealed class TangemPay(
"version" to ProviderSuspend { appInfoProvider.appVersion }, "version" to ProviderSuspend { appInfoProvider.appVersion },
"platform" to ProviderSuspend { "Android" }, "platform" to ProviderSuspend { "Android" },
"X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) }, "X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) },
"X-Device-Scale" to ProviderSuspend { appInfoProvider.deviceScale.toString() },
) )
private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String { private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String {

View file

@ -29,6 +29,11 @@ interface TangemPayApi {
@GET("v1/customer/me") @GET("v1/customer/me")
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse> suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
@GET("v1/customer/tariff-plan/transitions")
suspend fun getTariffPlanTransitions(
@Header("Authorization") authHeader: String,
): ApiResponse<TariffPlanTransitionsResponse>
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */ /** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
@GET("v1/account/bank-credentials/{product_instance_id}") @GET("v1/account/bank-credentials/{product_instance_id}")
suspend fun getBankCredentials( suspend fun getBankCredentials(

View file

@ -39,6 +39,13 @@ data class CustomerMeResponse(
@Json(name = "type") val type: String?, @Json(name = "type") val type: String?,
@Json(name = "name") val name: String?, @Json(name = "name") val name: String?,
@Json(name = "description_items") val descriptionItems: List<DescriptionItem>?, @Json(name = "description_items") val descriptionItems: List<DescriptionItem>?,
@Json(name = "images") val images: List<Image>? = null,
)
@JsonClass(generateAdapter = true)
data class Image(
@Json(name = "type") val type: String?,
@Json(name = "url") val url: String?,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TariffPlanTransitionsResponse(
@Json(name = "result") val result: List<TariffPlanTransitionResponse>?,
)
@JsonClass(generateAdapter = true)
data class TariffPlanTransitionResponse(
@Json(name = "type") val type: String?,
@Json(name = "tariff_plan") val tariffPlan: CustomerMeResponse.TariffPlan?,
)

View file

@ -70,6 +70,7 @@ internal class ProdApiConfigsManagerTest {
every { appInfoProvider.osVersion } returns "Android 16" every { appInfoProvider.osVersion } returns "Android 16"
every { appInfoProvider.language } returns Locale.getDefault().toLanguageTag() every { appInfoProvider.language } returns Locale.getDefault().toLanguageTag()
every { appInfoProvider.device } returns "${Build.MANUFACTURER} ${Build.MODEL}" every { appInfoProvider.device } returns "${Build.MANUFACTURER} ${Build.MODEL}"
every { appInfoProvider.deviceScale } returns DEVICE_SCALE
manager = ProdApiConfigsManager(apiConfigs = createApiConfigs()) manager = ProdApiConfigsManager(apiConfigs = createApiConfigs())
} }
@ -298,6 +299,7 @@ internal class ProdApiConfigsManagerTest {
"version" to ProviderSuspend { VERSION_NAME }, "version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" }, "platform" to ProviderSuspend { "Android" },
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV }, "X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
), ),
), ),
) )
@ -313,6 +315,7 @@ internal class ProdApiConfigsManagerTest {
"version" to ProviderSuspend { VERSION_NAME }, "version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" }, "platform" to ProviderSuspend { "Android" },
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV }, "X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
), ),
), ),
) )
@ -457,6 +460,7 @@ internal class ProdApiConfigsManagerTest {
private companion object { private companion object {
const val VERSION_NAME = "debug" const val VERSION_NAME = "debug"
const val DEVICE_SCALE = 3f
const val EXPRESS_SESSION_ID = "express_session_id" const val EXPRESS_SESSION_ID = "express_session_id"
const val STAKE_KIT_API_KEY = "stake_kit_api_key" const val STAKE_KIT_API_KEY = "stake_kit_api_key"
const val P2P_API_KEY = "p2p_api_key" const val P2P_API_KEY = "p2p_api_key"

View file

@ -1978,6 +1978,13 @@
<string name="tangempay_pay_support">Pay Support</string> <string name="tangempay_pay_support">Pay Support</string>
<string name="tangempay_payment_account">Payment account</string> <string name="tangempay_payment_account">Payment account</string>
<string name="tangempay_payment_account_sync_needed">Session expired</string> <string name="tangempay_payment_account_sync_needed">Session expired</string>
<string name="tangempay_select_plan_title">Select plan</string>
<string name="tangempay_select_plan_confirm_title">Confirm selection</string>
<string name="tangempay_select_plan_compare">Compare plans</string>
<string name="tangempay_select_plan_btn_select">Select</string>
<string name="tangempay_select_plan_btn_cancel">Cancel</string>
<string name="tangempay_select_plan_btn_upgrade">Upgrade plan</string>
<string name="tangempay_select_plan_btn_downgrade">Downgrade plan</string>
<string name="tangempay_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</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_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> <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>

View file

@ -27,6 +27,11 @@ interface AppInfoProvider {
/** Current locale as a BCP 47 language tag (e.g. `"en-US"`, `"zh-CN"`). */ /** Current locale as a BCP 47 language tag (e.g. `"en-US"`, `"zh-CN"`). */
val language: String val language: String
/**
* Display density as a float (e.g. `"2.0"`, `"2.75"`, `"3.0"`).
*/
val deviceScale: Float
/** IANA time-zone id of the device's current time zone, e.g. `"Europe/Moscow"`, `"UTC"`. */ /** IANA time-zone id of the device's current time zone, e.g. `"Europe/Moscow"`, `"UTC"`. */
val timezone: String val timezone: String

View file

@ -0,0 +1,37 @@
package com.tangem.data.pay.converter
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.domain.models.account.TangemPayTariffPlan
internal object TangemPayTariffPlanConverter {
fun convert(value: CustomerMeResponse.TariffPlan?): TangemPayTariffPlan? {
val id = value?.id ?: return null
val name = value.name ?: return null
return TangemPayTariffPlan(
id = id,
type = TangemPayTariffPlan.Type.fromString(value.type),
name = name,
descriptionItems = value.descriptionItems.orEmpty().mapNotNull(::convertDescriptionItem),
images = value.images.orEmpty().mapNotNull(::convertImage),
)
}
private fun convertDescriptionItem(item: CustomerMeResponse.DescriptionItem): TangemPayTariffPlan.DescriptionItem? {
val title = item.title ?: return null
return TangemPayTariffPlan.DescriptionItem(
section = TangemPayTariffPlan.Section.fromString(item.type),
order = item.order ?: 0,
title = title,
body = item.body.orEmpty(),
)
}
private fun convertImage(image: CustomerMeResponse.Image): TangemPayTariffPlan.Image? {
val url = image.url ?: return null
return TangemPayTariffPlan.Image(
type = TangemPayTariffPlan.Image.Type.fromString(image.type),
url = url,
)
}
}

View file

@ -124,8 +124,21 @@ internal interface TangemPayDataModule {
@IntoSet @IntoSet
fun bindTangemPayUserWalletDataCleaner(impl: TangemPayUserWalletDataCleaner): UserWalletDataCleaner fun bindTangemPayUserWalletDataCleaner(impl: TangemPayUserWalletDataCleaner): UserWalletDataCleaner
@Binds
@Singleton
fun bindTariffPlanTransitionsRepository(
repository: DefaultTariffPlanTransitionsRepository,
): TangemPayTariffPlanTransitionsRepository
companion object { companion object {
@Provides
fun provideGetTangemPayTariffPlanTransitionsUseCase(
repository: TangemPayTariffPlanTransitionsRepository,
): GetTangemPayTariffPlanTransitionsUseCase {
return GetTangemPayTariffPlanTransitionsUseCase(repository)
}
@Provides @Provides
@Singleton @Singleton
fun providePaymentAccountStatusesStore( fun providePaymentAccountStatusesStore(

View file

@ -0,0 +1,36 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.data.pay.converter.TangemPayTariffPlanConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
import com.tangem.domain.visa.error.VisaApiError
import javax.inject.Inject
internal class DefaultTariffPlanTransitionsRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
) : TangemPayTariffPlanTransitionsRepository {
override suspend fun getTransitions(
userWalletId: UserWalletId,
): Either<VisaApiError, List<TangemPayTariffPlanTransition>> = either {
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getTariffPlanTransitions(authHeader = authHeader)
}.bind()
response.result.orEmpty().mapNotNull { it.toDomain() }
}
private fun TariffPlanTransitionResponse.toDomain(): TangemPayTariffPlanTransition? {
val plan = TangemPayTariffPlanConverter.convert(tariffPlan) ?: return null
return TangemPayTariffPlanTransition(
type = TangemPayTariffPlanTransition.Type.fromString(type),
plan = plan,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.data.pay.util package com.tangem.data.pay.util
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.data.pay.converter.TangemPayTariffPlanConverter
import com.tangem.datasource.api.pay.models.response.CryptoBalance import com.tangem.datasource.api.pay.models.response.CryptoBalance
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.datasource.api.pay.models.response.FiatBalance import com.tangem.datasource.api.pay.models.response.FiatBalance
@ -63,21 +64,8 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
) )
} }
private fun CustomerMeResponse.TariffPlan.toDomain(): TangemPayTariffPlan? { private fun CustomerMeResponse.TariffPlan.toDomain(): TangemPayTariffPlan? =
val name = name ?: return null TangemPayTariffPlanConverter.convert(this)
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 { private fun CustomerMeResponse.ProductInstance.toDomain(): ProductInstance {
val status = status.toDomain() val status = status.toDomain()

View file

@ -6,9 +6,11 @@ import java.util.Locale
@Serializable @Serializable
data class TangemPayTariffPlan( data class TangemPayTariffPlan(
@SerialName("id") val id: String,
@SerialName("type") val type: Type, @SerialName("type") val type: Type,
@SerialName("name") val name: String, @SerialName("name") val name: String,
@SerialName("description_items") val descriptionItems: List<DescriptionItem>, @SerialName("description_items") val descriptionItems: List<DescriptionItem>,
@SerialName("images") val images: List<Image> = emptyList(),
) { ) {
@Serializable @Serializable
data class DescriptionItem( data class DescriptionItem(
@ -18,6 +20,37 @@ data class TangemPayTariffPlan(
@SerialName("body") val body: String, @SerialName("body") val body: String,
) )
@Serializable
data class Image(
@SerialName("type") val type: Type,
@SerialName("url") val url: String,
) {
@Serializable
enum class Type {
@SerialName("THUMBNAIL")
THUMBNAIL,
@SerialName("MAIN")
MAIN,
@SerialName("BANNER")
BANNER,
@SerialName("UNKNOWN")
UNKNOWN,
;
companion object {
fun fromString(value: String?) = when (value?.uppercase(Locale.US)) {
"THUMBNAIL" -> THUMBNAIL
"MAIN" -> MAIN
"BANNER" -> BANNER
else -> UNKNOWN
}
}
}
}
@Serializable @Serializable
enum class Type { enum class Type {
@SerialName("BASIC") @SerialName("BASIC")

View file

@ -0,0 +1,41 @@
package com.tangem.domain.models.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Locale
@Serializable
data class TangemPayTariffPlanTransition(
@SerialName("type") val type: Type,
@SerialName("tariff_plan") val plan: TangemPayTariffPlan,
) {
@Serializable
enum class Type {
@SerialName("UPGRADE")
UPGRADE,
@SerialName("DOWNGRADE")
DOWNGRADE,
@SerialName("SYSTEM_DOWNGRADE")
SYSTEM_DOWNGRADE,
@SerialName("ACTIVATION")
ACTIVATION,
@SerialName("UNKNOWN")
UNKNOWN,
;
companion object {
fun fromString(value: String?) = when (value?.uppercase(Locale.US)) {
"UPGRADE" -> UPGRADE
"DOWNGRADE" -> DOWNGRADE
"SYSTEM_DOWNGRADE" -> SYSTEM_DOWNGRADE
"ACTIVATION" -> ACTIVATION
else -> UNKNOWN
}
}
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.error.VisaApiError
interface TangemPayTariffPlanTransitionsRepository {
suspend fun getTransitions(userWalletId: UserWalletId): Either<VisaApiError, List<TangemPayTariffPlanTransition>>
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
import com.tangem.domain.visa.error.VisaApiError
class GetTangemPayTariffPlanTransitionsUseCase(
private val repository: TangemPayTariffPlanTransitionsRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
): Either<VisaApiError, List<TangemPayTariffPlanTransition>> = repository.getTransitions(userWalletId)
}

View file

@ -18,6 +18,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.promobanners.api.PromoBannersBlockComponent import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanComponent import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanComponent
import com.tangem.features.tangempay.tiers.select.TangemPaySelectPlanComponent
import com.tangem.features.tangempay.utils.userWalletId import com.tangem.features.tangempay.utils.userWalletId
import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.tokenreceive.TokenReceiveComponent
@ -94,6 +95,12 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
tariffPlan = config.tariffPlan, tariffPlan = config.tariffPlan,
), ),
) )
TangemPayAccountDetailsInnerRoute.SelectPlan -> TangemPaySelectPlanComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
params = TangemPaySelectPlanComponent.Params(
userWalletId = params.initialStatus.userWalletId,
),
)
} }
private fun onChildBack() { private fun onChildBack() {

View file

@ -6,6 +6,7 @@ import com.tangem.features.tangempay.closure.TangemPayCloseCardModel
import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupModel import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupModel
import com.tangem.features.tangempay.model.* import com.tangem.features.tangempay.model.*
import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanModel import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanModel
import com.tangem.features.tangempay.tiers.select.TangemPaySelectPlanModel
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -90,4 +91,9 @@ internal interface TangemPayModelModule {
@IntoMap @IntoMap
@ClassKey(TangemPayCurrentPlanModel::class) @ClassKey(TangemPayCurrentPlanModel::class)
fun bindTangemPayCurrentPlanModel(model: TangemPayCurrentPlanModel): Model fun bindTangemPayCurrentPlanModel(model: TangemPayCurrentPlanModel): Model
@Binds
@IntoMap
@ClassKey(TangemPaySelectPlanModel::class)
fun bindTangemPaySelectPlanModel(model: TangemPaySelectPlanModel): Model
} }

View file

@ -20,4 +20,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
data class CurrentPlan( data class CurrentPlan(
val tariffPlan: TangemPayCustomerTariffPlan, val tariffPlan: TangemPayCustomerTariffPlan,
) : TangemPayAccountDetailsInnerRoute() ) : TangemPayAccountDetailsInnerRoute()
@Serializable
data object SelectPlan : TangemPayAccountDetailsInnerRoute()
} }

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.TangemPayTariffPlan import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -34,7 +35,7 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
notification = null, notification = null,
sections = buildSections(plan), sections = buildSections(plan),
onBackClick = router::pop, onBackClick = router::pop,
onChangePlanClick = {}, onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan) },
) )
private fun buildSections(plan: TangemPayTariffPlan) = persistentListOf( private fun buildSections(plan: TangemPayTariffPlan) = persistentListOf(

View file

@ -0,0 +1,255 @@
package com.tangem.features.tangempay.tiers.select
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.TextReference
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.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
@Composable
internal fun ComparePlansBottomSheet(compare: TangemPaySelectPlanUM.ComparePlans?) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = compare != null,
onDismissRequest = compare?.onDismiss ?: {},
content = TangemBottomSheetConfigContent.Empty,
),
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors3.bg.secondary,
title = {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = resourceReference(R.string.tangempay_select_plan_compare).resolveReference(),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
)
TangemButton.Close(
modifier = Modifier.align(Alignment.CenterEnd),
onClick = compare?.onDismiss ?: {},
)
}
},
content = {
if (compare != null) {
ComparePlansContent(compare)
}
},
)
}
@Composable
private fun ComparePlansContent(compare: TangemPaySelectPlanUM.ComparePlans) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val cardWidth = maxWidth - CARD_PEEK
val cardWidthPx = with(LocalDensity.current) { cardWidth.toPx() }
val scope = rememberCoroutineScope()
val rowStates = remember(compare.plans.size) { List(compare.plans.size) { LazyListState() } }
val tabState = rememberLazyListState()
rowStates.forEach { source ->
LaunchedEffect(source) {
snapshotFlow { source.firstVisibleItemIndex to source.firstVisibleItemScrollOffset }
.collect { (index, offset) ->
if (source.isScrollInProgress) {
rowStates.forEach { target -> if (target !== source) target.scrollToItem(index, offset) }
}
}
}
}
val selectedIndex by remember(rowStates, cardWidthPx) {
derivedStateOf {
val primary = rowStates.firstOrNull() ?: return@derivedStateOf 0
val extra = if (primary.firstVisibleItemScrollOffset > cardWidthPx / 2f) 1 else 0
(primary.firstVisibleItemIndex + extra).coerceIn(0, compare.attributes.lastIndex.coerceAtLeast(0))
}
}
LaunchedEffect(selectedIndex) { tabState.animateScrollToItem(selectedIndex) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
compare.plans.fastForEachIndexed { index, plan ->
PlanValuesSection(plan = plan, listState = rowStates[index], cardWidth = cardWidth)
}
AttributeTabs(
attributes = compare.attributes,
selectedIndex = selectedIndex,
listState = tabState,
onTabClick = { index -> scope.launch { rowStates.firstOrNull()?.animateScrollToItem(index) } },
)
}
}
}
@Composable
private fun PlanValuesSection(plan: TangemPaySelectPlanUM.ComparePlans.Plan, listState: LazyListState, cardWidth: Dp) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier.padding(horizontal = 32.dp, vertical = 12.dp),
text = plan.name.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
)
LazyRow(
state = listState,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
flingBehavior = rememberSnapFlingBehavior(lazyListState = listState),
) {
items(count = plan.values.size) { index ->
ValueCard(value = plan.values[index], width = cardWidth)
}
}
}
}
@Composable
private fun ValueCard(value: TextReference, width: Dp) {
Box(
modifier = Modifier
.width(width)
.heightIn(112.dp)
.clip(RoundedCornerShape(24.dp))
.background(TangemTheme.colors3.bg.tertiary)
.padding(16.dp),
) {
Text(
text = value.resolveReference(),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
)
}
}
@Composable
private fun AttributeTabs(
attributes: ImmutableList<TextReference>,
selectedIndex: Int,
listState: LazyListState,
onTabClick: (Int) -> Unit,
) {
LazyRow(
state = listState,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
itemsIndexed(attributes) { index, title ->
AttributeTab(title = title, selected = index == selectedIndex, onClick = { onTabClick(index) })
}
}
}
@Composable
private fun AttributeTab(title: TextReference, selected: Boolean, onClick: () -> Unit) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(100.dp))
.background(if (selected) TangemTheme.colors3.bg.tertiary else TangemTheme.colors3.bg.secondary)
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography3.body.medium,
color = if (selected) TangemTheme.colors3.text.primary else TangemTheme.colors3.text.secondary,
maxLines = 1,
)
}
}
private val CARD_PEEK = 72.dp
@Preview(showBackground = true, widthDp = 402)
@Preview(showBackground = true, widthDp = 402, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ComparePlansContentPreview() {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
) {
ComparePlansContent(compare = previewCompare())
}
}
}
private fun previewCompare() = TangemPaySelectPlanUM.ComparePlans(
attributes = persistentListOf(
stringReference("Visa Programme"),
stringReference("Plan fee"),
stringReference("FX fee"),
stringReference("Daily spending limit"),
stringReference("Max cards issued"),
stringReference("Additional benefits"),
),
plans = persistentListOf(
TangemPaySelectPlanUM.ComparePlans.Plan(
name = stringReference("Basic"),
values = persistentListOf(
stringReference("Platinum"),
stringReference("$0.00"),
stringReference("1%"),
stringReference("$10.000"),
stringReference("3"),
stringReference("No"),
),
),
TangemPaySelectPlanUM.ComparePlans.Plan(
name = stringReference("Plus"),
values = persistentListOf(
stringReference("Signature"),
stringReference("$29.99/month"),
stringReference("1%"),
stringReference("$50.000"),
stringReference("5"),
stringReference("Benefit 1, Benefit 2, Benefit 3"),
),
),
),
onDismiss = {},
)

View file

@ -0,0 +1,26 @@
package com.tangem.features.tangempay.tiers.select
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.wallet.UserWalletId
internal class TangemPaySelectPlanComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: TangemPaySelectPlanModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TangemPaySelectPlanScreen(state = state, modifier = modifier)
}
data class Params(val userWalletId: UserWalletId)
}

View file

@ -0,0 +1,193 @@
package com.tangem.features.tangempay.tiers.select
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.domain.models.account.TangemPayTariffPlanTransition
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanTransitionsUseCase
import com.tangem.features.tangempay.details.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPaySelectPlanModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getTransitions: GetTangemPayTariffPlanTransitionsUseCase,
) : Model() {
private val params = paramsContainer.require<TangemPaySelectPlanComponent.Params>()
private var transitions: List<TangemPayTariffPlanTransition> = emptyList()
private var selectedIndex: Int = 0
private var isConfirm: Boolean = false
val state: StateFlow<TangemPaySelectPlanUM>
field = MutableStateFlow(buildState())
init {
loadTransitions()
}
private fun loadTransitions() {
modelScope.launch {
getTransitions(params.userWalletId).onRight { result ->
transitions = result.filter { it.type in ALLOWED_TYPES }
state.update { buildState() }
}
}
}
private fun onPlanSelected(index: Int) {
if (index == selectedIndex) return
selectedIndex = index
state.update { buildState() }
}
private fun onSelectClick() {
if (transitions.isEmpty()) return
isConfirm = true
state.update { buildState() }
}
private fun onComparePlansClick() {
if (transitions.isEmpty()) return
state.update { buildState(showPlanCompare = true) }
}
private fun onCompareDismiss() {
state.update { buildState(showPlanCompare = false) }
}
private fun onBackClick() {
if (isConfirm) {
isConfirm = false
state.update { buildState() }
} else {
router.pop()
}
}
private fun buildState(showPlanCompare: Boolean = false): TangemPaySelectPlanUM = TangemPaySelectPlanUM(
topBarTitle = if (isConfirm) {
resourceReference(R.string.tangempay_select_plan_confirm_title)
} else {
resourceReference(R.string.tangempay_select_plan_title)
},
plans = transitions.map { it.plan.toPlanUM() }.toImmutableList(),
selectedIndex = selectedIndex,
onPlanSelected = ::onPlanSelected,
onBackClick = ::onBackClick,
onCloseClick = router::pop,
content = if (isConfirm) buildConfirmContent() else buildSelectContent(),
compare = if (showPlanCompare) buildCompare() else null,
)
private fun buildSelectContent() = TangemPaySelectPlanUM.Content.Select(
onComparePlansClick = ::onComparePlansClick,
onSelectClick = ::onSelectClick,
)
private fun buildCompare(): TangemPaySelectPlanUM.ComparePlans {
val plans = transitions.map { it.plan }
val orderedTitles = plans
.flatMap { it.descriptionItems }
.sortedWith(compareBy({ it.section.ordinal }, { it.order }))
.map { it.title }
.distinct()
return TangemPaySelectPlanUM.ComparePlans(
attributes = orderedTitles.map(::stringReference).toImmutableList(),
plans = plans.map { plan ->
val valueByTitle = plan.descriptionItems.associate { it.title to it.body }
TangemPaySelectPlanUM.ComparePlans.Plan(
name = stringReference(plan.name),
values = orderedTitles
.map { title -> stringReference(valueByTitle[title].orEmpty()) }
.toImmutableList(),
)
}.toImmutableList(),
onDismiss = ::onCompareDismiss,
)
}
private fun buildConfirmContent(): TangemPaySelectPlanUM.Content.Confirm {
val transition = transitions[selectedIndex]
val planName = transition.plan.name
val isUpgrade = transition.type == TangemPayTariffPlanTransition.Type.UPGRADE
return TangemPaySelectPlanUM.Content.Confirm(
// TODO v_rodionov: strings hardcoded for now - wait for documentation update
title = stringReference("We will issue Visa $planName for you"),
points = buildConfirmPoints(transition, planName, isUpgrade),
confirmButtonText = resourceReference(
if (isUpgrade) {
R.string.tangempay_select_plan_btn_upgrade
} else {
R.string.tangempay_select_plan_btn_downgrade
},
),
onCancelClick = ::onBackClick,
onConfirmClick = {},
)
}
// TODO v_rodionov: strings hardcoded for now - wait for documentation update
private fun buildConfirmPoints(
transition: TangemPayTariffPlanTransition,
planName: String,
isUpgrade: Boolean,
): ImmutableList<TangemPaySelectPlanUM.PointUM> = if (isUpgrade) {
val feeText = transition.plan.descriptionItems
.firstOrNull { it.section == TangemPayTariffPlan.Section.PLAN_RELATED }
?.title
listOf(
stringReference("You will get your virtual Visa $planName in minutes"),
if (feeText != null) {
stringReference("$feeText monthly fee will be taken from your account")
} else {
stringReference("No fee applied")
},
)
} else {
listOf(
stringReference("Your current Visa cards will be closed"),
stringReference("No fee applied"),
)
}
.map { TangemPaySelectPlanUM.PointUM(title = it, body = null) }
.toImmutableList()
private fun TangemPayTariffPlan.toPlanUM() = TangemPaySelectPlanUM.PlanUM(
name = stringReference(name),
imageUrl = images.firstOrNull { it.type == TangemPayTariffPlan.Image.Type.MAIN }?.url,
points = descriptionItems
.sortedWith(compareBy({ it.section.ordinal }, { it.order }))
.map { item ->
TangemPaySelectPlanUM.PointUM(
title = stringReference(item.title),
body = item.body.takeIf(String::isNotBlank)?.let(::stringReference),
)
}
.toImmutableList(),
)
companion object {
private val ALLOWED_TYPES = setOf(
TangemPayTariffPlanTransition.Type.UPGRADE,
TangemPayTariffPlanTransition.Type.DOWNGRADE,
TangemPayTariffPlanTransition.Type.ACTIVATION, // TODO v_rodionov: Only for test, must be removed in future
)
}
}

View file

@ -0,0 +1,356 @@
package com.tangem.features.tangempay.tiers.select
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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 coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.ds.TangemPagerIndicator
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.Back
import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.TextReference
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.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.distinctUntilChanged
import com.tangem.core.ui.R as CoreUiR
@Composable
internal fun TangemPaySelectPlanScreen(state: TangemPaySelectPlanUM, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors3.bg.primary),
) {
Column(modifier = Modifier.fillMaxSize()) {
SelectPlanTopBar(state = state)
AnimatedContent(
targetState = state.content,
modifier = Modifier.weight(1f),
contentKey = { it::class },
label = "SelectPlanContent",
) { content ->
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
when (content) {
is TangemPaySelectPlanUM.Content.Select -> SelectContent(state = state)
is TangemPaySelectPlanUM.Content.Confirm -> ConfirmContent(state = state, content = content)
}
}
}
AnimatedContent(
targetState = state.content,
contentKey = { it::class },
label = "SelectPlanFooter",
) { content ->
when (content) {
is TangemPaySelectPlanUM.Content.Select -> SelectFooter(content = content)
is TangemPaySelectPlanUM.Content.Confirm -> ConfirmFooter(content = content)
}
}
}
ComparePlansBottomSheet(compare = state.compare)
}
}
@Composable
private fun SelectPlanTopBar(state: TangemPaySelectPlanUM) {
TangemTopBar(
modifier = Modifier.statusBarsPadding(),
title = state.topBarTitle,
startContent = { TangemButton.Back(onClick = state.onBackClick) },
endContent = { TangemButton.Close(onClick = state.onCloseClick) },
)
}
@Composable
private fun ColumnScope.SelectContent(state: TangemPaySelectPlanUM) {
val plans = state.plans.takeIf { it.isNotEmpty() } ?: return
val pagerState = rememberPagerState(
initialPage = state.selectedIndex.coerceIn(0, plans.lastIndex),
pageCount = { plans.size },
)
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }
.distinctUntilChanged()
.collect(state.onPlanSelected)
}
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
contentPadding = PaddingValues(horizontal = 24.dp),
pageSpacing = 8.dp,
beyondViewportPageCount = 1,
) { page ->
PlanCard(imageUrl = plans[page].imageUrl)
}
if (plans.size > 1) {
TangemPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.Start)
.padding(start = 24.dp, top = 16.dp),
)
}
val plan = plans[state.selectedIndex.coerceIn(0, plans.lastIndex)]
PlanDetails(
title = plan.name,
points = plan.points,
)
}
@Composable
private fun ColumnScope.PlanDetails(title: TextReference, points: ImmutableList<TangemPaySelectPlanUM.PointUM>) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 24.dp, bottom = 12.dp),
text = title.resolveReference(),
style = TangemTheme.typography3.heading.medium,
color = TangemTheme.colors3.text.primary,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 12.dp, bottom = 12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
points.fastForEach { point -> PlanPoint(point = point) }
}
}
@Composable
private fun PlanPoint(point: TangemPaySelectPlanUM.PointUM, modifier: Modifier = Modifier) {
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.secondary,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = point.title.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
point.body?.let { body ->
Text(
text = body.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
)
}
}
}
}
@Composable
private fun ColumnScope.ConfirmContent(state: TangemPaySelectPlanUM, content: TangemPaySelectPlanUM.Content.Confirm) {
val selectedPlan = state.plans.getOrNull(state.selectedIndex) ?: return
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 12.dp),
contentAlignment = Alignment.Center,
) {
PlanCard(imageUrl = selectedPlan.imageUrl)
}
Spacer(modifier = Modifier.weight(1f))
PlanDetails(
title = content.title,
points = content.points,
)
}
@Composable
private fun SelectFooter(content: TangemPaySelectPlanUM.Content.Select, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.navigationBarsPadding(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
TangemButton(
modifier = Modifier.fillMaxWidth(),
variant = TangemButton.Variant.Secondary,
size = TangemButton.Size.X12,
text = resourceReference(R.string.tangempay_select_plan_compare),
onClick = content.onComparePlansClick,
)
TangemButton(
modifier = Modifier.fillMaxWidth(),
variant = TangemButton.Variant.Primary,
size = TangemButton.Size.X12,
text = resourceReference(R.string.tangempay_select_plan_btn_select),
onClick = content.onSelectClick,
)
}
}
@Composable
private fun ConfirmFooter(content: TangemPaySelectPlanUM.Content.Confirm, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.navigationBarsPadding(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
TangemButton(
modifier = Modifier.fillMaxWidth(),
variant = TangemButton.Variant.Secondary,
size = TangemButton.Size.X12,
text = resourceReference(R.string.tangempay_select_plan_btn_cancel),
onClick = content.onCancelClick,
)
TangemButton(
modifier = Modifier.fillMaxWidth(),
variant = TangemButton.Variant.Primary,
size = TangemButton.Size.X12,
text = content.confirmButtonText,
onClick = content.onConfirmClick,
)
}
}
@Suppress("MagicNumber")
@Composable
private fun PlanCard(imageUrl: String?, modifier: Modifier = Modifier) {
SubcomposeAsyncImage(
modifier = modifier
.fillMaxWidth()
.aspectRatio(ratio = 266f / 172f)
.clip(RoundedCornerShape(12.dp)),
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.crossfade(true)
.build(),
loading = {
RectangleShimmer(
modifier = Modifier.fillMaxSize(),
radius = 12.dp,
)
},
error = {
Box(
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors3.bg.secondary),
)
},
contentScale = ContentScale.Crop,
contentDescription = null,
)
}
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO)
@Composable
private fun TangemPaySelectPlanScreenPreview() {
TangemThemePreviewRedesign {
TangemPaySelectPlanScreen(state = previewState(isConfirm = false))
}
}
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Composable
private fun TangemPaySelectPlanConfirmPreview() {
TangemThemePreviewRedesign {
TangemPaySelectPlanScreen(state = previewState(isConfirm = true))
}
}
private fun previewState(isConfirm: Boolean) = TangemPaySelectPlanUM(
topBarTitle = stringReference(if (isConfirm) "Confirm selection" else "Select plan"),
plans = persistentListOf(
TangemPaySelectPlanUM.PlanUM(
name = stringReference("Plus"),
imageUrl = null,
points = persistentListOf(
TangemPaySelectPlanUM.PointUM(
title = stringReference("2 airport lounge pass a year"),
body = stringReference("Travel insurance and other benefits"),
),
TangemPaySelectPlanUM.PointUM(stringReference("$50.000 daily spending limit"), null),
TangemPaySelectPlanUM.PointUM(stringReference("$29.99 / month"), null),
),
),
),
selectedIndex = 0,
onPlanSelected = {},
onBackClick = {},
onCloseClick = {},
content = if (isConfirm) {
TangemPaySelectPlanUM.Content.Confirm(
title = stringReference("We will issue Visa Plus for you"),
points = persistentListOf(
TangemPaySelectPlanUM.PointUM(
title = stringReference("You will get your virtual Visa Plus in minutes"),
body = null,
),
TangemPaySelectPlanUM.PointUM(
title = stringReference("$29.99 / month will be charged from your account"),
body = null,
),
),
confirmButtonText = stringReference("Upgrade plan"),
onCancelClick = {},
onConfirmClick = {},
)
} else {
TangemPaySelectPlanUM.Content.Select(
onComparePlansClick = {},
onSelectClick = {},
)
},
)

View file

@ -0,0 +1,61 @@
package com.tangem.features.tangempay.tiers.select
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class TangemPaySelectPlanUM(
val topBarTitle: TextReference,
val plans: ImmutableList<PlanUM>,
val selectedIndex: Int,
val onPlanSelected: (Int) -> Unit,
val onBackClick: () -> Unit,
val onCloseClick: () -> Unit,
val content: Content,
val compare: ComparePlans? = null,
) {
@Immutable
data class PlanUM(
val name: TextReference,
val imageUrl: String?,
val points: ImmutableList<PointUM>,
)
@Immutable
data class ComparePlans(
val attributes: ImmutableList<TextReference>,
val plans: ImmutableList<Plan>,
val onDismiss: () -> Unit,
) {
@Immutable
data class Plan(
val name: TextReference,
val values: ImmutableList<TextReference>,
)
}
@Immutable
data class PointUM(
val title: TextReference,
val body: TextReference?,
)
@Immutable
sealed interface Content {
data class Select(
val onComparePlansClick: () -> Unit,
val onSelectClick: () -> Unit,
) : Content
data class Confirm(
val title: TextReference,
val points: ImmutableList<PointUM>,
val confirmButtonText: TextReference,
val onCancelClick: () -> Unit,
val onConfirmClick: () -> Unit,
) : Content
}
}