Updated on 2026-08-14
This commit is contained in:
parent
a0e5656330
commit
d82dbdd1e6
12 changed files with 256 additions and 129 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
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),
|
||||
)
|
||||
} 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue