Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-19 16:07:08 +05:00
parent a0e5656330
commit d82dbdd1e6
12 changed files with 256 additions and 129 deletions

View file

@ -40,6 +40,18 @@ import kotlin.time.Duration.Companion.minutes
private const val TAG = "PaymentAccountStatusFetcher"
/**
* Reorders cards to match [previousOrder] (by [TangemPayCard.id]), appending any card absent from it at the
* end while preserving the relative order among the new ones. Keeps the card layout stable when the backend
* reorders `productInstances` (e.g. after a rename bumps `updated_at`). Returns the receiver unchanged when
* [previousOrder] is empty (first load backend order).
*/
internal fun List<TangemPayCard>.stableOrder(previousOrder: List<String>): List<TangemPayCard> {
if (previousOrder.isEmpty()) return this
val indexById = previousOrder.withIndex().associate { (index, id) -> id to index }
return sortedBy { indexById[it.id] ?: Int.MAX_VALUE }
}
@Suppress("LongParameterList", "LargeClass")
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
@ -365,13 +377,18 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
// placeholder for every locally tracked in-flight issuance order alongside the real cards.
val issuingCards = buildIssuingCards(userWalletId)
// Keep the card order stable across refetches: the backend orders `productInstances` by a mutable
// field (a rename bumps `updated_at`), which would otherwise make the renamed card jump. Anchor on
// the previously shown order and append newly seen cards at the end.
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
customerId = customerId,
depositAddress = cryptoBalance.depositAddress,
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
fiatRate = fiatRate,
cards = tangemPayCards + issuingCards,
cards = orderedCards + issuingCards,
balance = PaymentAccountStatusValue.Balance(
fiatBalance = fiatBalance,
cryptoBalance = cryptoBalance,
@ -381,6 +398,21 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
)
}
/**
* Order of real (product-instance-backed) cards from the previously stored status, used as the stable
* anchor for [stableOrder]. Issuing placeholders are excluded they carry synthetic order ids and are
* always appended last. Empty on the first load (no prior [PaymentAccountStatusValue.Loaded]), which makes
* [stableOrder] fall back to the backend order.
*/
private suspend fun previousRealCardOrder(userWalletId: UserWalletId): List<String> {
val previousValue = paymentAccountStatusesStore.getSyncOrNull(userWalletId)?.value
return (previousValue as? PaymentAccountStatusValue.Loaded)
?.cards
?.filterNot { it.state == TangemPayCardState.Issuing }
?.map { it.id }
.orEmpty()
}
private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState {
val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull()
val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull()

View file

@ -0,0 +1,80 @@
package com.tangem.data.pay.flow
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class StableOrderTest {
@ParameterizedTest
@ProvideTestModels
fun stableOrder(model: Model) {
// Act
val result = model.cards.stableOrder(model.previousOrder).map { it.id }
// Assert
assertThat(result).containsExactlyElementsIn(model.expectedIds).inOrder()
}
private fun provideTestModels() = listOf(
Model(
name = "GIVEN no previous order WHEN stableOrder THEN backend order is kept",
cards = listOf(card("a"), card("b"), card("c")),
previousOrder = emptyList(),
expectedIds = listOf("a", "b", "c"),
),
Model(
// The reported bug: a rename bumps updated_at, so the backend moves the renamed card.
name = "GIVEN backend reordered existing cards WHEN stableOrder THEN previous order is preserved",
cards = listOf(card("b"), card("c"), card("a")),
previousOrder = listOf("a", "b", "c"),
expectedIds = listOf("a", "b", "c"),
),
Model(
name = "GIVEN a newly seen card WHEN stableOrder THEN it is appended at the end",
cards = listOf(card("c"), card("a"), card("b")),
previousOrder = listOf("a", "b"),
expectedIds = listOf("a", "b", "c"),
),
Model(
name = "GIVEN several new cards WHEN stableOrder THEN they keep their backend order at the end",
cards = listOf(card("d"), card("b"), card("a"), card("c")),
previousOrder = listOf("a", "b"),
expectedIds = listOf("a", "b", "d", "c"),
),
Model(
name = "GIVEN previous order references a gone card WHEN stableOrder THEN the stale id is ignored",
cards = listOf(card("b"), card("a")),
previousOrder = listOf("a", "x", "b"),
expectedIds = listOf("a", "b"),
),
)
internal data class Model(
val name: String,
val cards: List<TangemPayCard>,
val previousOrder: List<String>,
val expectedIds: List<String>,
) {
override fun toString(): String = name
}
private companion object {
fun card(id: String): TangemPayCard = TangemPayCard(
id = id,
productInstanceId = id,
cardStatus = TangemPayCard.Status.ACTIVE,
hasPinCode = false,
displayName = null,
limit = null,
frozenState = TangemPayCardFrozenState.Unfrozen,
lastDigits = "0000",
state = TangemPayCardState.Active,
)
}
}

View file

@ -66,8 +66,8 @@ object OrderConflictRules {
}
private fun OrderType.isIssuing(): Boolean {
return this == OrderType.CARD_ISSUE ||
this == OrderType.CARD_ISSUE_ADDITIONAL ||
return this == OrderType.CARD_ISSUE_ADDITIONAL ||
this == OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC ||
this == OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2
}

View file

@ -9,9 +9,9 @@ import com.tangem.domain.pay.model.OrderType.Companion.fromString
* so the app never crashes on a new server-side type.
*/
enum class OrderType(val wireValue: String) {
CARD_ISSUE("CARD_ISSUE_VIRTUAL_RAIN_KYC"),
CARD_ISSUE_ADDITIONAL("CARD_ISSUE_ADDITIONAL"),
CARD_ISSUE_VIRTUAL_RAIN_KYC_V2("CARD_ISSUE_VIRTUAL_RAIN_KYC_V2"),
CARD_ISSUE_VIRTUAL_RAIN_KYC("CARD_ISSUE_VIRTUAL_RAIN_KYC"),
CARD_REISSUE("CARD_REISSUE"),
CARD_FREEZE("CARD_FREEZE"),
CARD_UNFREEZE("CARD_UNFREEZE"),

View file

@ -6,6 +6,7 @@ import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.Offer
import com.tangem.domain.pay.model.OrderType
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.CustomerOffersRepository
import com.tangem.domain.pay.repository.CustomerOrderRepository
@ -53,7 +54,14 @@ class IssueAdditionalCardUseCase(
val activeOrders = catch(
block = {
customerOrderRepository
.findOrders(userWalletId = userWalletId, types = setOf(offer.data.orderType))
.findOrders(
userWalletId = userWalletId,
types = setOf(
offer.data.orderType,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
)
.bind()
},
catch = { handleError(it) },

View file

@ -10,7 +10,7 @@ internal class OrderConflictRulesTest {
@Test
fun `IssueCard is blocked by an active issue order`() {
val active = listOf(order(type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING))
val active = listOf(order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING))
val resolution = OrderConflictRules.resolve(OrderIntent.IssueCard, active)
@ -104,7 +104,7 @@ internal class OrderConflictRulesTest {
val active = listOf(
order(type = OrderType.CARD_FREEZE, status = OrderStatus.PROCESSING, productInstanceId = cardA),
order(type = OrderType.WITHDRAW, status = OrderStatus.PROCESSING),
order(type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING),
order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING),
)
val resolution = OrderConflictRules.resolve(OrderIntent.Rename(cardA), active)
@ -115,8 +115,8 @@ internal class OrderConflictRulesTest {
@Test
fun `Terminal-status orders never block`() {
val terminal = listOf(
order(type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED),
order(type = OrderType.CARD_ISSUE, status = OrderStatus.CANCELED),
order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED),
order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.CANCELED),
)
val resolution = OrderConflictRules.resolve(OrderIntent.IssueCard, terminal)

View file

@ -65,7 +65,11 @@ internal class IssueAdditionalCardUseCaseTest {
coEvery {
orderRepository.findOrders(
userWalletId,
types = setOf(OrderType.CARD_ISSUE_ADDITIONAL),
types = setOf(
OrderType.CARD_ISSUE_ADDITIONAL,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
statuses = emptySet(),
)
} returns listOf(existing).right()
@ -83,7 +87,11 @@ internal class IssueAdditionalCardUseCaseTest {
coEvery {
orderRepository.findOrders(
userWalletId,
types = setOf(OrderType.CARD_ISSUE_ADDITIONAL),
types = setOf(
OrderType.CARD_ISSUE_ADDITIONAL,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
statuses = emptySet(),
)
} returns emptyList<Order>().right()
@ -107,7 +115,11 @@ internal class IssueAdditionalCardUseCaseTest {
coEvery {
orderRepository.findOrders(
userWalletId,
types = setOf(OrderType.CARD_ISSUE_ADDITIONAL),
types = setOf(
OrderType.CARD_ISSUE_ADDITIONAL,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
statuses = emptySet(),
)
} returns emptyList<Order>().right()

View file

@ -35,7 +35,7 @@ internal class RestoreActiveOrdersUseCaseTest {
@Test
fun `returns the orders found by the repository`() = runTest {
val orders = listOf(
order(id = "issue", type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING),
order(id = "issue", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING),
order(id = "withdraw", type = OrderType.WITHDRAW, status = OrderStatus.NEW),
)
coEvery { repository.findOrders(userWalletId, types = emptySet(), statuses = any()) } returns orders.right()

View file

@ -12,11 +12,11 @@ internal class OrderResolverTest {
fun `selectActive filters by type and active status`() {
val orders = listOf(
order(id = "1", type = OrderType.WITHDRAW, status = OrderStatus.PROCESSING, updatedAt = "2026-01-01"),
order(id = "2", type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING, updatedAt = "2026-01-02"),
order(id = "3", type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED, updatedAt = "2026-01-03"),
order(id = "2", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING, updatedAt = "2026-01-02"),
order(id = "3", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED, updatedAt = "2026-01-03"),
)
val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE)
val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC)
assertThat(result?.id).isEqualTo("2")
}
@ -24,11 +24,11 @@ internal class OrderResolverTest {
@Test
fun `selectActive picks the latest by updatedAt`() {
val orders = listOf(
order(id = "old", type = OrderType.CARD_ISSUE, status = OrderStatus.NEW, updatedAt = "2026-01-01"),
order(id = "new", type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING, updatedAt = "2026-06-05"),
order(id = "old", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.NEW, updatedAt = "2026-01-01"),
order(id = "new", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING, updatedAt = "2026-06-05"),
)
val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE)
val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC)
assertThat(result?.id).isEqualTo("new")
}
@ -36,11 +36,11 @@ internal class OrderResolverTest {
@Test
fun `selectActive returns null when no active order of the type exists`() {
val orders = listOf(
order(id = "1", type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED, updatedAt = "2026-01-01"),
order(id = "1", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED, updatedAt = "2026-01-01"),
order(id = "2", type = OrderType.WITHDRAW, status = OrderStatus.PROCESSING, updatedAt = "2026-01-02"),
)
val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE)
val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC)
assertThat(result).isNull()
}
@ -91,11 +91,11 @@ internal class OrderResolverTest {
@Test
fun `selectLatest includes terminal orders`() {
val orders = listOf(
order(id = "1", type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED, updatedAt = "2026-01-05"),
order(id = "2", type = OrderType.CARD_ISSUE, status = OrderStatus.NEW, updatedAt = "2026-01-01"),
order(id = "1", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED, updatedAt = "2026-01-05"),
order(id = "2", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.NEW, updatedAt = "2026-01-01"),
)
val result = OrderResolver.selectLatest(orders = orders, type = OrderType.CARD_ISSUE)
val result = OrderResolver.selectLatest(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC)
assertThat(result?.id).isEqualTo("1")
}

View file

@ -59,6 +59,8 @@ internal class TangemPayDetailsStateFactory(
fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM {
val hasUnfrozenCard = status.cards.any { it.frozenState == TangemPayCardFrozenState.Unfrozen }
val hasIssuingCard = status.cards.any { it.state == TangemPayCardState.Issuing }
val isAddCardEnabled = status.error == null && !hasIssuingCard
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
@ -88,7 +90,7 @@ internal class TangemPayDetailsStateFactory(
}
.toImmutableList(),
onAddCardClick = intents::onAddCardClick,
isAddCardEnabled = status.error == null,
isAddCardEnabled = isAddCardEnabled,
),
),
isBalanceHidden = false,

View file

@ -141,6 +141,7 @@ internal fun TangemPayDetailsScreenV2(
expressTransactionsBottomSheetState?.content(null)
}
@Suppress("LongMethod")
private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
item("balanceBlock") {
BalanceBlock(
@ -174,6 +175,15 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
title = resourceReference(R.string.tangempay_issuing_new_digital_card_title),
subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description),
contentColor = TangemTheme.colors3.bg.opaque.secondary,
leadingContent = {
Icon(
modifier = Modifier.size(20.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
},
)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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
@ -11,7 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -37,7 +39,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_card_plus_32
import com.tangem.core.ui.res.generated.icons.ic_error_28
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayIssueAdditionalCardUM
@ -72,47 +73,55 @@ internal fun TangemPayIssueAdditionalCardContent(state: TangemPayIssueAdditional
@Composable
private fun Content(state: TangemPayIssueAdditionalCardUM) {
val appearance = state.contentAppearance()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(16.dp)
StatusIcon(appearance = appearance)
SpacerH(32.dp)
CenteredMessageText(
textRes = appearance.titleRes,
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
)
CenteredMessageText(
textRes = appearance.subtitleRes,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
)
SpacerH(32.dp)
FeeBlock(modifier = Modifier.padding(top = 16.dp), state = state)
SpacerH(8.dp)
BottomButtonsBlock(state = state, appearance = appearance)
Column(modifier = Modifier.fillMaxWidth()) {
Header()
FeeBlock(state = state)
if (state.isBalanceInsufficient) {
InsufficientFundsNotification(state = state)
}
IssueButton(state = state)
}
}
@Composable
private fun StatusIcon(appearance: IssueAdditionalCardContentAppearance) {
private fun Header() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(32.dp)
StatusIcon()
SpacerH(32.dp)
CenteredMessageText(
textRes = R.string.tangempay_issue_additional_card_title,
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
)
SpacerH(8.dp)
CenteredMessageText(
textRes = R.string.tangempay_issue_additional_card_description,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
)
SpacerH(32.dp)
}
}
@Composable
private fun StatusIcon() {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(appearance.iconBackgroundColor),
.background(TangemTheme.colors3.bg.status.infoSubtle),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = appearance.icon,
imageVector = Icons.ic_card_plus_32,
contentDescription = null,
tint = appearance.iconColor,
tint = TangemTheme.colors3.icon.status.info,
modifier = Modifier.size(28.dp),
)
}
@ -152,68 +161,64 @@ private fun FeeBlock(state: TangemPayIssueAdditionalCardUM, modifier: Modifier =
}
@Composable
private fun BottomButtonsBlock(
state: TangemPayIssueAdditionalCardUM,
appearance: IssueAdditionalCardContentAppearance,
modifier: Modifier = Modifier,
) {
private fun InsufficientFundsNotification(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp)
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors3.bg.status.warningSubtle)
.padding(horizontal = 14.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Top,
) {
Image(
modifier = Modifier.size(20.dp),
painter = painterResource(R.drawable.img_usdc_16),
contentDescription = null,
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_title),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
Text(
text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_subtitle),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
}
TangemButton(
modifier = Modifier.fillMaxWidth(),
variant = TangemButton.Variant.Secondary,
size = TangemButton.Size.X12,
onClick = state.onDismiss,
text = resourceReference(R.string.common_cancel),
)
TangemButton(
modifier = Modifier.fillMaxWidth(),
size = TangemButton.Size.X12,
onClick = appearance.primaryAction(state),
isEnabled = !state.isLoading,
isLoading = state.isLoading,
text = resourceReference(appearance.primaryButtonTextRes),
size = TangemButton.Size.X8,
onClick = state.onAddFundsClick,
text = resourceReference(R.string.tangempay_card_details_add_funds),
)
}
}
private data class IssueAdditionalCardContentAppearance(
val titleRes: Int,
val subtitleRes: Int,
val icon: ImageVector,
val iconColor: Color,
val iconBackgroundColor: Color,
val primaryButtonTextRes: Int,
val primaryAction: (TangemPayIssueAdditionalCardUM) -> () -> Unit,
)
@Composable
private fun TangemPayIssueAdditionalCardUM.contentAppearance(): IssueAdditionalCardContentAppearance {
return if (isBalanceInsufficient) {
IssueAdditionalCardContentAppearance(
titleRes = R.string.tangempay_reissue_card_insufficient_funds_title,
subtitleRes = R.string.tangempay_reissue_card_insufficient_funds_subtitle,
icon = Icons.ic_error_28,
iconColor = TangemTheme.colors3.icon.status.warning,
iconBackgroundColor = TangemTheme.colors3.bg.status.warningSubtle,
primaryButtonTextRes = R.string.tangempay_card_details_add_funds,
primaryAction = { it.onAddFundsClick },
)
} else {
IssueAdditionalCardContentAppearance(
titleRes = R.string.tangempay_issue_additional_card_title,
subtitleRes = R.string.tangempay_issue_additional_card_description,
icon = Icons.ic_card_plus_32,
iconColor = TangemTheme.colors3.icon.status.info,
iconBackgroundColor = TangemTheme.colors3.bg.status.infoSubtle,
primaryButtonTextRes = R.string.tangempay_issue_card,
primaryAction = { it.onIssueClick },
)
}
private fun IssueButton(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
size = TangemButton.Size.X12,
onClick = state.onIssueClick,
isEnabled = !state.isLoading && !state.isBalanceInsufficient,
isLoading = state.isLoading,
text = resourceReference(R.string.tangempay_issue_card),
)
}
@Preview(showBackground = true, widthDp = 360)
@ -223,29 +228,7 @@ private fun TangemPayIssueAdditionalCardContentPreview(
@PreviewParameter(IssueAdditionalCardPreviewProvider::class) state: TangemPayIssueAdditionalCardUM,
) {
TangemThemePreviewRedesign {
IssueAdditionalCardSheetPreview(state = state)
}
}
@Composable
private fun IssueAdditionalCardSheetPreview(state: TangemPayIssueAdditionalCardUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
) {
TangemTopBar(
type = TangemTopBarType.BottomSheet,
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = state.onDismiss,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
)
Content(state)
TangemPayIssueAdditionalCardContent(state = state)
}
}