Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 15:25:28 +03:00
parent 819ab8c161
commit ddba5fe3cf
19 changed files with 919 additions and 68 deletions

View file

@ -0,0 +1,21 @@
package com.tangem.features.tangempay.cashback.impl
import androidx.compose.runtime.Composable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
internal class CashbackBottomSheetComponent(
appComponentContext: AppComponentContext,
private val onDismiss: () -> Unit,
private val content: @Composable () -> Unit,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
override fun dismiss() {
onDismiss()
}
@Composable
override fun BottomSheet() {
content()
}
}

View file

@ -4,10 +4,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.tangempay.cashback.api.TangemPayCashbackComponent
import com.tangem.features.tangempay.cashback.impl.model.TangemPayCashbackModel
import com.tangem.features.tangempay.cashback.impl.model.TangemPayCashbackNavigation
import com.tangem.features.tangempay.cashback.impl.ui.TangemPayCashbackAccrualsBottomSheet
import com.tangem.features.tangempay.cashback.impl.ui.TangemPayCashbackDetailsBottomSheet
import com.tangem.features.tangempay.cashback.impl.ui.TangemPayCashbackScreen
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -20,10 +29,50 @@ internal class DefaultTangemPayCashbackComponent @AssistedInject constructor(
private val model: TangemPayCashbackModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = TangemPayCashbackNavigation.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
TangemPayCashbackScreen(state = state, modifier = modifier)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
navigation: TangemPayCashbackNavigation,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val context = childByContext(componentContext)
return when (navigation) {
TangemPayCashbackNavigation.Details -> CashbackBottomSheetComponent(
appComponentContext = context,
onDismiss = model.bottomSheetNavigation::dismiss,
content = {
val state by model.detailsSheet.collectAsStateWithLifecycle()
TangemPayCashbackDetailsBottomSheet(
state = state,
onDismiss = model.bottomSheetNavigation::dismiss,
)
},
)
TangemPayCashbackNavigation.Accruals -> CashbackBottomSheetComponent(
appComponentContext = context,
onDismiss = model.bottomSheetNavigation::dismiss,
content = {
val state by model.accrualsSheet.collectAsStateWithLifecycle()
TangemPayCashbackAccrualsBottomSheet(
state = state,
onDismiss = model.bottomSheetNavigation::dismiss,
)
},
)
}
}
@AssistedFactory

View file

@ -0,0 +1,13 @@
package com.tangem.features.tangempay.cashback.impl.model
import java.util.Locale
internal object CashbackRates {
private val byTier = mapOf(
"basic" to 1,
"plus" to 2,
)
fun forTier(tier: String): Int? = byTier[tier.lowercase(Locale.ROOT)]
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.tangem.domain.models.account.TangemPayTariffPlan
/** Cashback program tier, mapped once from the domain and shared by the rate tile and the details sheet. */
internal data class CashbackTier(
val planType: TangemPayTariffPlan.Type,
val rate: Int?,
val label: String,
val scope: String,
val minPurchase: String?,
val monthlyCap: String?,
)

View file

@ -0,0 +1,50 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.pay.model.CashbackDocument
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackAccrualsUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class TangemPayCashbackAccrualsConverter(
private val onDocClick: (url: String) -> Unit,
) : Converter<List<CashbackDocument>, TangemPayCashbackAccrualsUM> {
// TODO([REDACTED_TASK_KEY]): move hardcoded strings to string resources
override fun convert(value: List<CashbackDocument>): TangemPayCashbackAccrualsUM {
return TangemPayCashbackAccrualsUM(
title = stringReference("Accruals"),
infoRows = INFO_ROWS,
docRows = value.map { doc ->
TangemPayCashbackAccrualsUM.DocRow(
title = stringReference(doc.title),
onClick = { onDocClick(doc.url) },
)
}.toImmutableList(),
)
}
private companion object {
val INFO_ROWS = persistentListOf(
TangemPayCashbackAccrualsUM.InfoRow(
title = stringReference("How we calculate cashback?"),
description = stringReference(
"We process purchases within 5 days after the operation and count only completed transactions",
),
),
TangemPayCashbackAccrualsUM.InfoRow(
title = stringReference("How we pay cashback?"),
description = stringReference("From the 2nd and the 5th of the next month"),
),
TangemPayCashbackAccrualsUM.InfoRow(
title = stringReference("Exceptions"),
description = stringReference(
"No cashback will be awarded for in-person/in-store purchases at EU merchants; also for " +
"withdrawals, transfers, quasi-cash, mobile phone bills, government services and certain " +
"other categories",
),
),
)
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackDetailsUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
internal class TangemPayCashbackDetailsConverter : Converter<List<CashbackTier>, TangemPayCashbackDetailsUM> {
// TODO([REDACTED_TASK_KEY]): move hardcoded strings to string resources
override fun convert(value: List<CashbackTier>): TangemPayCashbackDetailsUM {
return TangemPayCashbackDetailsUM(
title = title(value),
rows = value.map(::tierRow).toImmutableList(),
)
}
private fun title(tiers: List<CashbackTier>): TextReference {
val rates = tiers.mapNotNull { it.rate }
return when {
rates.isEmpty() -> stringReference("Cashback")
rates.size == 1 -> stringReference("Cashback ${rates.single()}%")
else -> stringReference("Cashback up to ${rates.max()}%")
}
}
private fun tierRow(tier: CashbackTier): TextReference {
val rate = tier.rate?.let { "$it% for " }.orEmpty()
val min = tier.minPurchase?.let { ", min purchase $it" }.orEmpty()
val cap = tier.monthlyCap?.let { ", up to $it per month" }.orEmpty()
return stringReference("$rate${tier.scope} with your ${tier.label}$min$cap")
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackInfoTilesUM
internal class TangemPayCashbackInfoTilesConverter(
private val onRateClick: () -> Unit,
private val onAccrualsClick: () -> Unit,
) {
// TODO([REDACTED_TASK_KEY]): move hardcoded strings to string resources
fun convert(
tiers: List<CashbackTier>,
currentPlanType: TangemPayTariffPlan.Type,
currentPlanName: String?,
): TangemPayCashbackInfoTilesUM {
val rate = tiers.selectTier(currentPlanType)?.rate
return TangemPayCashbackInfoTilesUM(
rate = TangemPayCashbackInfoTilesUM.Tile(
iconRes = R.drawable.ic_percent_24,
title = stringReference(if (rate != null) "Cashback $rate%" else "Cashback"),
subtitle = currentPlanName?.let { stringReference("With your $it plan") } ?: TextReference.EMPTY,
onClick = onRateClick,
),
accruals = TangemPayCashbackInfoTilesUM.Tile(
iconRes = R.drawable.ic_information_24,
title = stringReference("Accruals"),
subtitle = stringReference("Limits and exceptions"),
onClick = onAccrualsClick,
),
)
}
private fun List<CashbackTier>.selectTier(planType: TangemPayTariffPlan.Type): CashbackTier? {
return firstOrNull {
planType != TangemPayTariffPlan.Type.UNKNOWN && it.planType == planType
} ?: firstOrNull()
}
}

View file

@ -1,44 +1,114 @@
package com.tangem.features.tangempay.cashback.impl.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
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.domain.pay.model.TangemPayCashback
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackUM
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.pay.model.CashbackDocument
import com.tangem.domain.pay.model.CashbackPromotions
import com.tangem.domain.pay.model.CashbackSummary
import com.tangem.domain.pay.repository.CashbackRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.features.tangempay.cashback.api.TangemPayCashbackComponent
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackAccrualsUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackDetailsUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackScreenUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.joda.time.DateTime
import java.math.BigDecimal
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayCashbackModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val router: Router,
private val urlOpener: UrlOpener,
private val cashbackRepository: CashbackRepository,
private val onboardingRepository: OnboardingRepository,
) : Model() {
private val params: TangemPayCashbackComponent.Params = paramsContainer.require()
private val userWalletId get() = params.userWalletId
val bottomSheetNavigation: SlotNavigation<TangemPayCashbackNavigation> = SlotNavigation()
private val cashbackConverter = TangemPayCashbackUmConverter(onCloseClick = router::pop)
private val tiersConverter = TangemPayCashbackTiersConverter()
private val infoTilesConverter = TangemPayCashbackInfoTilesConverter(
onRateClick = { bottomSheetNavigation.activate(TangemPayCashbackNavigation.Details) },
onAccrualsClick = { bottomSheetNavigation.activate(TangemPayCashbackNavigation.Accruals) },
)
private val detailsConverter = TangemPayCashbackDetailsConverter()
private val accrualsConverter = TangemPayCashbackAccrualsConverter(onDocClick = urlOpener::openUrl)
val uiState: StateFlow<TangemPayCashbackUM>
field = MutableStateFlow(cashbackConverter.convert(STUB_CASHBACK))
val detailsSheet: StateFlow<TangemPayCashbackDetailsUM>
field = MutableStateFlow(detailsConverter.convert(emptyList()))
private companion object {
// TODO([REDACTED_TASK_KEY]): replace stub with repository load
val STUB_CASHBACK = TangemPayCashback(
confirmedAmount = BigDecimal("22.54"),
pendingAmount = BigDecimal("13.65"),
currency = "USD",
payoutCurrency = "USDC",
payoutNetwork = "Polygon",
period = TangemPayCashback.Period(
year = 2026,
month = 6,
payoutStart = DateTime.parse("2026-07-01"),
payoutEnd = DateTime.parse("2026-07-05"),
val accrualsSheet: StateFlow<TangemPayCashbackAccrualsUM>
field = MutableStateFlow(accrualsConverter.convert(emptyList()))
val uiState: StateFlow<TangemPayCashbackScreenUM>
field = MutableStateFlow(
TangemPayCashbackScreenUM(
cashback = cashbackConverter.convert(value = null),
infoTiles = null,
),
)
init {
loadCashback()
}
private fun loadCashback() {
modelScope.launch {
val summaryDeferred = async { loadSummary() }
val promotionsDeferred = async { loadPromotions() }
val docsDeferred = async { loadDocs() }
val planDeferred = async { loadPlan() }
val summary = summaryDeferred.await()
val promotions = promotionsDeferred.await()
val plan = planDeferred.await()
val tiers = promotions?.let(tiersConverter::convert).orEmpty()
uiState.value = TangemPayCashbackScreenUM(
cashback = cashbackConverter.convert((summary as? CashbackSummary.Enabled)?.cashback),
infoTiles = promotions?.let {
infoTilesConverter.convert(
tiers = tiers,
currentPlanType = plan?.type ?: TangemPayTariffPlan.Type.UNKNOWN,
currentPlanName = plan?.name,
)
},
)
detailsSheet.value = detailsConverter.convert(tiers)
accrualsSheet.value = accrualsConverter.convert(docsDeferred.await())
}
}
private suspend fun loadSummary(): CashbackSummary? =
runSuspendCatching { cashbackRepository.getCashbackSummary(userWalletId).getOrNull() }.getOrNull()
private suspend fun loadPromotions(): CashbackPromotions? =
runSuspendCatching { cashbackRepository.getCashbackPromotions(userWalletId).getOrNull() }.getOrNull()
private suspend fun loadDocs(): List<CashbackDocument> =
runSuspendCatching { cashbackRepository.getCashbackAccrualDocs(userWalletId).getOrNull() }
.getOrNull().orEmpty()
private suspend fun loadPlan(): TangemPayTariffPlan? {
return runSuspendCatching {
onboardingRepository.getCustomerInfo(userWalletId).getOrNull()
}.getOrNull()?.tariffPlan?.plan
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.tangempay.cashback.impl.model
import kotlinx.serialization.Serializable
@Serializable
internal sealed class TangemPayCashbackNavigation {
@Serializable
data object Details : TangemPayCashbackNavigation()
@Serializable
data object Accruals : TangemPayCashbackNavigation()
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.format.bigdecimal.optionalDecimals
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.pay.model.CashbackPromotions
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class TangemPayCashbackTiersConverter : Converter<CashbackPromotions, List<CashbackTier>> {
override fun convert(value: CashbackPromotions): List<CashbackTier> {
return value.cardTiers.map { tier ->
CashbackTier(
planType = TangemPayTariffPlan.Type.fromString(tier.tier),
rate = CashbackRates.forTier(tier.tier),
label = tier.label,
scope = tier.scope,
minPurchase = tier.minTransactionAmount?.formatUsd(),
monthlyCap = tier.monthlyCapAmount?.formatUsd(),
)
}
}
private fun BigDecimal.formatUsd(): String {
val currency = getJavaCurrencyByCode(AMOUNT_CURRENCY_CODE)
return format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() }
}
private companion object {
const val AMOUNT_CURRENCY_CODE = "USD"
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.tangempay.cashback.impl.ui
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.runtime.Composable
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.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun CashbackBottomSheet(title: TextReference, onDismiss: () -> Unit, content: @Composable () -> Unit) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors3.bg.secondary,
title = {
TangemTopNavigation(
title = title,
contentAlign = TangemTopNavigation.ContentAlign.Center,
windowInsets = WindowInsets(0),
onClose = onDismiss,
)
},
content = { content() },
)
}

View file

@ -0,0 +1,148 @@
package com.tangem.features.tangempay.cashback.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowText
import com.tangem.core.ui.ds2.row.TangemRowTextRole
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
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.cashback.impl.ui.state.TangemPayCashbackAccrualsUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun TangemPayCashbackAccrualsBottomSheet(state: TangemPayCashbackAccrualsUM, onDismiss: () -> Unit) {
CashbackBottomSheet(title = state.title, onDismiss = onDismiss) {
AccrualsContent(state)
}
}
@Composable
private fun AccrualsContent(state: TangemPayCashbackAccrualsUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
) {
state.infoRows.forEach { row ->
TangemRow(
verticalAlignment = TangemRowVerticalAlignment.Top,
startSlot = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = R.drawable.ic_information_24),
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
},
titleSlot = { TangemRowText(text = row.title, role = TangemRowTextRole.Title) },
subtitleSlot = {
TangemRowText(text = row.description, role = TangemRowTextRole.Subtitle, maxLines = Int.MAX_VALUE)
},
divider = true,
)
}
state.docRows.forEachIndexed { index, row ->
TangemRow(
verticalAlignment = TangemRowVerticalAlignment.Center,
startSlot = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = R.drawable.ic_doc_24),
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
},
titleSlot = { TangemRowText(text = row.title, role = TangemRowTextRole.Title) },
endSlot = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors3.icon.secondary,
)
},
onClick = row.onClick,
divider = index < state.docRows.lastIndex,
)
}
}
}
@Preview(showBackground = true, widthDp = 402)
@Preview(showBackground = true, widthDp = 402, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCashbackAccrualsBottomSheetPreview(
@PreviewParameter(TangemPayCashbackAccrualsPreviewProvider::class) state: TangemPayCashbackAccrualsUM,
) {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
) {
AccrualsContent(state = state)
}
}
}
private class TangemPayCashbackAccrualsPreviewProvider :
CollectionPreviewParameterProvider<TangemPayCashbackAccrualsUM>(
listOf(
TangemPayCashbackAccrualsUM(
title = stringReference("Accruals"),
infoRows = previewInfoRows(),
docRows = persistentListOf(
TangemPayCashbackAccrualsUM.DocRow(
title = stringReference("All categories without cashback"),
onClick = {},
),
TangemPayCashbackAccrualsUM.DocRow(
title = stringReference("Full terms of cashback program"),
onClick = {},
),
),
),
TangemPayCashbackAccrualsUM(
title = stringReference("Accruals"),
infoRows = previewInfoRows(),
docRows = persistentListOf(),
),
),
)
private fun previewInfoRows() = persistentListOf(
TangemPayCashbackAccrualsUM.InfoRow(
title = stringReference("How we calculate cashback?"),
description = stringReference(
"We process purchases within 5 days after the operation and count only completed transactions",
),
),
TangemPayCashbackAccrualsUM.InfoRow(
title = stringReference("How we pay cashback?"),
description = stringReference("From the 2nd and the 5th of the next month"),
),
TangemPayCashbackAccrualsUM.InfoRow(
title = stringReference("Exceptions"),
description = stringReference(
"No cashback will be awarded for in-person/in-store purchases at EU merchants; also for " +
"withdrawals, transfers, quasi-cash, mobile phone bills, government services and certain " +
"other categories",
),
),
)

View file

@ -0,0 +1,88 @@
package com.tangem.features.tangempay.cashback.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowText
import com.tangem.core.ui.ds2.row.TangemRowTextRole
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
import com.tangem.core.ui.extensions.TextReference
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.cashback.impl.ui.state.TangemPayCashbackDetailsUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun TangemPayCashbackDetailsBottomSheet(state: TangemPayCashbackDetailsUM, onDismiss: () -> Unit) {
CashbackBottomSheet(title = state.title, onDismiss = onDismiss) {
DetailsContent(state)
}
}
@Composable
private fun DetailsContent(state: TangemPayCashbackDetailsUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
) {
state.rows.forEachIndexed { index, row ->
TangemRow(
verticalAlignment = TangemRowVerticalAlignment.Top,
startSlot = {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = R.drawable.ic_information_24),
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
},
titleSlot = { TangemRowText(text = row, role = TangemRowTextRole.Title, maxLines = Int.MAX_VALUE) },
divider = index < state.rows.lastIndex,
)
}
}
}
@Preview(showBackground = true, widthDp = 402)
@Preview(showBackground = true, widthDp = 402, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCashbackDetailsBottomSheetPreview(
@PreviewParameter(TangemPayCashbackDetailsPreviewProvider::class) state: TangemPayCashbackDetailsUM,
) {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
) {
DetailsContent(state = state)
}
}
}
private class TangemPayCashbackDetailsPreviewProvider : CollectionPreviewParameterProvider<TangemPayCashbackDetailsUM>(
listOf(
TangemPayCashbackDetailsUM(
title = stringReference("Cashback up to 2%"),
rows = persistentListOf<TextReference>(
stringReference("1% for All purchases with your Basic cards, min purchase $30, up to $100 per month"),
stringReference("2% for All purchases with your Plus cards, min purchase $30, up to $300 per month"),
),
),
),
)

View file

@ -0,0 +1,156 @@
package com.tangem.features.tangempay.cashback.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.ds2.surface.TangemSurface
import com.tangem.core.ui.extensions.resolveReference
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.cashback.impl.ui.state.TangemPayCashbackInfoTilesUM
@Composable
internal fun TangemPayCashbackInfoTiles(state: TangemPayCashbackInfoTilesUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Tile(
tile = state.rate,
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
)
Tile(
tile = state.accruals,
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
)
}
}
@Composable
private fun Tile(tile: TangemPayCashbackInfoTilesUM.Tile, modifier: Modifier = Modifier) {
TangemSurface(
modifier = modifier,
color = TangemTheme.colors3.bg.secondary,
shape = RoundedCornerShape(16.dp),
onClick = tile.onClick,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.tertiary),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = tile.iconRes),
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
}
Spacer(modifier = Modifier.weight(1f))
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = tile.title.resolveReference(),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
)
Text(
text = tile.subtitle.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
}
}
}
@Preview(showBackground = true, widthDp = 402)
@Preview(showBackground = true, widthDp = 402, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCashbackInfoTilesPreview(
@PreviewParameter(TangemPayCashbackInfoTilesPreviewProvider::class) state: TangemPayCashbackInfoTilesUM,
) {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.background(TangemTheme.colors3.bg.primary)
.padding(vertical = 16.dp),
) {
TangemPayCashbackInfoTiles(state = state)
}
}
}
private class TangemPayCashbackInfoTilesPreviewProvider :
CollectionPreviewParameterProvider<TangemPayCashbackInfoTilesUM>(
listOf(
TangemPayCashbackInfoTilesUM(
rate = TangemPayCashbackInfoTilesUM.Tile(
iconRes = R.drawable.ic_percent_24,
title = stringReference("Cashback 1%"),
subtitle = stringReference("With your Basic plan"),
onClick = {},
),
accruals = TangemPayCashbackInfoTilesUM.Tile(
iconRes = R.drawable.ic_information_24,
title = stringReference("Accruals"),
subtitle = stringReference("Limits and exceptions"),
onClick = {},
),
),
TangemPayCashbackInfoTilesUM(
rate = TangemPayCashbackInfoTilesUM.Tile(
iconRes = R.drawable.ic_percent_24,
title = stringReference("Cashback up to 2%"),
subtitle = stringReference("With your Plus plan"),
onClick = {},
),
accruals = TangemPayCashbackInfoTilesUM.Tile(
iconRes = R.drawable.ic_information_24,
title = stringReference("Accruals"),
subtitle = stringReference("Limits and exceptions"),
onClick = {},
),
),
),
)

View file

@ -10,15 +10,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
@ -31,16 +28,17 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackInfoTilesUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackScreenUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackUM
import com.tangem.features.tangempay.details.impl.R
import dev.chrisbanes.haze.HazeStyle
import com.tangem.core.ui.R as CoreUiR
private const val GLOW_RADIUS_FACTOR = 0.585f
@ -48,31 +46,24 @@ private const val GLOW_BLUE_ALPHA = 0.20f
private const val GLOW_WARM_ALPHA = 0.15f
@Composable
internal fun TangemPayCashbackScreen(state: TangemPayCashbackUM, modifier: Modifier = Modifier) {
internal fun TangemPayCashbackScreen(state: TangemPayCashbackScreenUM, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors3.bg.primary),
) {
if (state.isEmpty) {
if (state.cashback.isEmpty) {
EmptyStateGlow(modifier = Modifier.fillMaxSize())
}
Column(modifier = Modifier.fillMaxSize()) {
TangemTopBar(
modifier = Modifier.statusBarsPadding(),
TangemTopNavigation(
// TODO([REDACTED_TASK_KEY]): move to string resources
title = stringReference("Cashback"),
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = state.onCloseClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
contentAlign = TangemTopNavigation.ContentAlign.Center,
onClose = state.cashback.onCloseClick,
)
HeroBlock(state = state)
state.banner?.let { banner ->
HeroBlock(state = state.cashback)
state.cashback.banner?.let { banner ->
CashbackBanner(
banner = banner,
modifier = Modifier
@ -80,6 +71,12 @@ internal fun TangemPayCashbackScreen(state: TangemPayCashbackUM, modifier: Modif
.padding(horizontal = 16.dp),
)
}
state.infoTiles?.let { infoTiles ->
TangemPayCashbackInfoTiles(
state = infoTiles,
modifier = Modifier.padding(top = 24.dp),
)
}
}
}
}
@ -92,7 +89,7 @@ private fun EmptyStateGlow(modifier: Modifier = Modifier) {
val blue = if (isDark) Color(0xFF0090F9) else Color(0xFF0092FC)
Box(
modifier = modifier
.blur(56.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
.hazeForegroundEffectTangem(style = HazeStyle(blurRadius = 56.dp, tint = null))
.drawBehind {
val radius = size.width * GLOW_RADIUS_FACTOR
val center = Offset(x = size.width / 2f, y = 0f)
@ -175,43 +172,67 @@ private fun CashbackBanner(banner: TangemPayCashbackUM.Banner, modifier: Modifie
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO)
@Composable
private fun TangemPayCashbackScreenPreview(
@PreviewParameter(TangemPayCashbackUMProvider::class) state: TangemPayCashbackUM,
@PreviewParameter(TangemPayCashbackScreenUMProvider::class) state: TangemPayCashbackScreenUM,
) {
TangemThemePreviewRedesign {
TangemPayCashbackScreen(state = state)
}
}
private class TangemPayCashbackUMProvider : CollectionPreviewParameterProvider<TangemPayCashbackUM>(
private class TangemPayCashbackScreenUMProvider : CollectionPreviewParameterProvider<TangemPayCashbackScreenUM>(
collection = listOf(
TangemPayCashbackUM(
title = stringReference("$22.54 earned in June"),
subtitle = stringReference("Will be deposited on July 15"),
isEmpty = false,
banner = TangemPayCashbackUM.Banner(
text = stringReference("Cashback $22.54 for June will be deposited till July 5"),
type = TangemPayCashbackUM.Banner.Type.Info,
),
onCloseClick = {},
),
TangemPayCashbackUM(
title = stringReference("$22.54 earned in June"),
subtitle = stringReference("Will be deposited on July 15"),
isEmpty = false,
banner = TangemPayCashbackUM.Banner(
text = stringReference(
"We received a refund for a purchase for which cashback had previously been awarded",
TangemPayCashbackScreenUM(
cashback = TangemPayCashbackUM(
title = stringReference("$22.54 earned in June"),
subtitle = stringReference("Will be deposited on July 15"),
isEmpty = false,
banner = TangemPayCashbackUM.Banner(
text = stringReference("Cashback $22.54 for June will be deposited till July 5"),
type = TangemPayCashbackUM.Banner.Type.Info,
),
type = TangemPayCashbackUM.Banner.Type.Error,
onCloseClick = {},
),
onCloseClick = {},
infoTiles = previewInfoTiles(),
),
TangemPayCashbackUM(
title = stringReference("Start spending and earn cashback"),
subtitle = stringReference("Collected amount will be shown here"),
isEmpty = true,
banner = null,
onCloseClick = {},
TangemPayCashbackScreenUM(
cashback = TangemPayCashbackUM(
title = stringReference("$22.54 earned in June"),
subtitle = stringReference("Will be deposited on July 15"),
isEmpty = false,
banner = TangemPayCashbackUM.Banner(
text = stringReference(
"We received a refund for a purchase for which cashback had previously been awarded",
),
type = TangemPayCashbackUM.Banner.Type.Error,
),
onCloseClick = {},
),
infoTiles = null,
),
TangemPayCashbackScreenUM(
cashback = TangemPayCashbackUM(
title = stringReference("Start spending and earn cashback"),
subtitle = stringReference("Collected amount will be shown here"),
isEmpty = true,
banner = null,
onCloseClick = {},
),
infoTiles = null,
),
),
)
private fun previewInfoTiles() = TangemPayCashbackInfoTilesUM(
rate = TangemPayCashbackInfoTilesUM.Tile(
iconRes = CoreUiR.drawable.ic_percent_24,
title = stringReference("Cashback 1%"),
subtitle = stringReference("With your Basic plan"),
onClick = {},
),
accruals = TangemPayCashbackInfoTilesUM.Tile(
iconRes = CoreUiR.drawable.ic_information_24,
title = stringReference("Accruals"),
subtitle = stringReference("Limits and exceptions"),
onClick = {},
),
)

View file

@ -0,0 +1,25 @@
package com.tangem.features.tangempay.cashback.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class TangemPayCashbackAccrualsUM(
val title: TextReference,
val infoRows: ImmutableList<InfoRow>,
val docRows: ImmutableList<DocRow>,
) {
@Immutable
data class InfoRow(
val title: TextReference,
val description: TextReference,
)
@Immutable
data class DocRow(
val title: TextReference,
val onClick: () -> Unit,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.tangempay.cashback.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class TangemPayCashbackDetailsUM(
val title: TextReference,
val rows: ImmutableList<TextReference>,
)

View file

@ -0,0 +1,20 @@
package com.tangem.features.tangempay.cashback.impl.ui.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
data class TangemPayCashbackInfoTilesUM(
val rate: Tile,
val accruals: Tile,
) {
@Immutable
data class Tile(
@DrawableRes val iconRes: Int,
val title: TextReference,
val subtitle: TextReference,
val onClick: () -> Unit,
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.tangempay.cashback.impl.ui.state
import androidx.compose.runtime.Immutable
@Immutable
internal data class TangemPayCashbackScreenUM(
val cashback: TangemPayCashbackUM,
val infoTiles: TangemPayCashbackInfoTilesUM?,
)