Updated on 2026-08-14
This commit is contained in:
parent
3b6eeb7535
commit
8b677a8e6b
25 changed files with 1149 additions and 17 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
|
)
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>>
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue