From d82dbdd1e674ed473ac711a73b4fa4bed0ec2113 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Jun 2026 16:07:08 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultPaymentAccountStatusFetcher.kt | 34 +++- .../tangem/data/pay/flow/StableOrderTest.kt | 80 ++++++++ .../domain/pay/model/OrderConflictRules.kt | 4 +- .../com/tangem/domain/pay/model/OrderType.kt | 2 +- .../pay/usecase/IssueAdditionalCardUseCase.kt | 10 +- .../pay/model/OrderConflictRulesTest.kt | 8 +- .../usecase/IssueAdditionalCardUseCaseTest.kt | 18 +- .../usecase/RestoreActiveOrdersUseCaseTest.kt | 2 +- .../domain/pay/util/OrderResolverTest.kt | 22 +- .../entity/TangemPayDetailsStateFactory.kt | 4 +- .../tangempay/ui/TangemPayDetailsScreenV2.kt | 10 + .../ui/TangemPayIssueAdditionalCardContent.kt | 191 ++++++++---------- 12 files changed, 256 insertions(+), 129 deletions(-) create mode 100644 data/visa/src/test/kotlin/com/tangem/data/pay/flow/StableOrderTest.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 68cc4759a6..0c90bc8ef9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -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.stableOrder(previousOrder: List): List { + 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 { + 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() diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/flow/StableOrderTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/flow/StableOrderTest.kt new file mode 100644 index 0000000000..814491f08f --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/flow/StableOrderTest.kt @@ -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, + val previousOrder: List, + val expectedIds: List, + ) { + 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, + ) + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt index 9e8e69e94a..4c576e6bd3 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt @@ -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 } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt index 979f004e79..006a67088f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt @@ -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"), diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt index 5850737efd..e33223eab4 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt @@ -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) }, diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt index 5df9fc677d..a288112d52 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt @@ -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) diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt index 474ac3d59d..8e6b1926a4 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt @@ -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().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().right() diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt index 824337d9e4..03fa34daf2 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt @@ -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() diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt index d348afc362..0681644652 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt @@ -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") } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 9efe87d846..b1a1c7fb6d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -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, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreenV2.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreenV2.kt index 79e79331e9..9aa75168c8 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreenV2.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreenV2.kt @@ -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, + ) + }, ) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt index 38da0cd2bd..dcf9185741 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt @@ -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) } }