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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
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
|
||||
get() = "Android"
|
||||
override val device: String
|
||||
|
|
@ -18,6 +22,8 @@ internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
|
|||
get() = Build.VERSION.SDK_INT
|
||||
override val language: String
|
||||
get() = Locale.getDefault().toLanguageTag()
|
||||
override val deviceScale: Float
|
||||
get() = context.resources.displayMetrics.density
|
||||
override val timezone: String
|
||||
get() = TimeZone.getDefault().id
|
||||
override val appVersion: String = BuildConfig.VERSION_NAME
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ internal sealed class TangemPay(
|
|||
"version" to ProviderSuspend { appInfoProvider.appVersion },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) },
|
||||
"X-Device-Scale" to ProviderSuspend { appInfoProvider.deviceScale.toString() },
|
||||
)
|
||||
|
||||
private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ interface TangemPayApi {
|
|||
@GET("v1/customer/me")
|
||||
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). */
|
||||
@GET("v1/account/bank-credentials/{product_instance_id}")
|
||||
suspend fun getBankCredentials(
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ data class CustomerMeResponse(
|
|||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "name") val name: String?,
|
||||
@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)
|
||||
|
|
|
|||
|
|
@ -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.language } returns Locale.getDefault().toLanguageTag()
|
||||
every { appInfoProvider.device } returns "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
every { appInfoProvider.deviceScale } returns DEVICE_SCALE
|
||||
|
||||
manager = ProdApiConfigsManager(apiConfigs = createApiConfigs())
|
||||
}
|
||||
|
|
@ -298,6 +299,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"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 },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"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 {
|
||||
|
||||
const val VERSION_NAME = "debug"
|
||||
const val DEVICE_SCALE = 3f
|
||||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_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_payment_account">Payment account</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_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>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ interface AppInfoProvider {
|
|||
/** Current locale as a BCP 47 language tag (e.g. `"en-US"`, `"zh-CN"`). */
|
||||
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"`. */
|
||||
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
|
||||
fun bindTangemPayUserWalletDataCleaner(impl: TangemPayUserWalletDataCleaner): UserWalletDataCleaner
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTariffPlanTransitionsRepository(
|
||||
repository: DefaultTariffPlanTransitionsRepository,
|
||||
): TangemPayTariffPlanTransitionsRepository
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
fun provideGetTangemPayTariffPlanTransitionsUseCase(
|
||||
repository: TangemPayTariffPlanTransitionsRepository,
|
||||
): GetTangemPayTariffPlanTransitionsUseCase {
|
||||
return GetTangemPayTariffPlanTransitionsUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
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
|
||||
|
||||
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.CustomerMeResponse
|
||||
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? {
|
||||
val name = name ?: return null
|
||||
return TangemPayTariffPlan(
|
||||
type = TangemPayTariffPlan.Type.fromString(type),
|
||||
name = name,
|
||||
descriptionItems = descriptionItems.orEmpty().map { it.toDomain() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.DescriptionItem.toDomain() = TangemPayTariffPlan.DescriptionItem(
|
||||
section = TangemPayTariffPlan.Section.fromString(type),
|
||||
order = order ?: 0,
|
||||
title = title.orEmpty(),
|
||||
body = body.orEmpty(),
|
||||
)
|
||||
private fun CustomerMeResponse.TariffPlan.toDomain(): TangemPayTariffPlan? =
|
||||
TangemPayTariffPlanConverter.convert(this)
|
||||
|
||||
private fun CustomerMeResponse.ProductInstance.toDomain(): ProductInstance {
|
||||
val status = status.toDomain()
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import java.util.Locale
|
|||
|
||||
@Serializable
|
||||
data class TangemPayTariffPlan(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("type") val type: Type,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("description_items") val descriptionItems: List<DescriptionItem>,
|
||||
@SerialName("images") val images: List<Image> = emptyList(),
|
||||
) {
|
||||
@Serializable
|
||||
data class DescriptionItem(
|
||||
|
|
@ -18,6 +20,37 @@ data class TangemPayTariffPlan(
|
|||
@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
|
||||
enum class Type {
|
||||
@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.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
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.tokendetails.ExpressTransactionsComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
|
|
@ -94,6 +95,12 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
tariffPlan = config.tariffPlan,
|
||||
),
|
||||
)
|
||||
TangemPayAccountDetailsInnerRoute.SelectPlan -> TangemPaySelectPlanComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPaySelectPlanComponent.Params(
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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.model.*
|
||||
import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanModel
|
||||
import com.tangem.features.tangempay.tiers.select.TangemPaySelectPlanModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -90,4 +91,9 @@ internal interface TangemPayModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(TangemPayCurrentPlanModel::class)
|
||||
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(
|
||||
val tariffPlan: TangemPayCustomerTariffPlan,
|
||||
) : 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.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -34,7 +35,7 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
|
|||
notification = null,
|
||||
sections = buildSections(plan),
|
||||
onBackClick = router::pop,
|
||||
onChangePlanClick = {},
|
||||
onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan) },
|
||||
)
|
||||
|
||||
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