From 084bad17abed69d320376b7201e180543b5fcedd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jun 2026 14:44:06 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../datasource/di/TangemPayStoresModule.kt | 10 + .../visa/DefaultTangemPayIssueCardStore.kt | 33 ++ .../local/visa/TangemPayIssueCardStore.kt | 19 + .../tangem/data/pay/di/TangemPayDataModule.kt | 15 +- .../DefaultPaymentAccountStatusFetcher.kt | 38 +- .../repository/DefaultIssueCardRepository.kt | 25 ++ .../domain/models/pay/TangemPayCardState.kt | 6 + .../TangemPayIssueCardRepository.kt | 18 + .../pay/usecase/IssueAdditionalCardUseCase.kt | 28 +- .../usecase/IssueAdditionalCardUseCaseTest.kt | 20 +- .../entity/TangemPayDetailsStateFactory.kt | 11 +- .../tangempay/entity/TangemPayDetailsUM.kt | 3 +- .../tangempay/ui/TangemPayCardPageScreen.kt | 6 + .../tangempay/ui/TangemPayDetailsScreen.kt | 15 +- .../tangempay/ui/TangemPayDetailsScreenV2.kt | 13 +- .../ui/TangemPayIssueAdditionalCardContent.kt | 346 ++++++++++-------- .../ui/components/TangemPayCardView.kt | 32 +- .../utils/TangemPayMessagesFactory.kt | 6 +- 18 files changed, 452 insertions(+), 192 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayIssueCardStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayIssueCardStore.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultIssueCardRepository.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayIssueCardRepository.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt index 3b3b111fdf..7f8bbc94ae 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt @@ -4,9 +4,11 @@ import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.DefaultTangemPayCloseCardStore +import com.tangem.datasource.local.visa.DefaultTangemPayIssueCardStore import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayCloseCardStore +import com.tangem.datasource.local.visa.TangemPayIssueCardStore import com.tangem.datasource.local.visa.TangemPayReissueCardStore import dagger.Module import dagger.Provides @@ -42,4 +44,12 @@ internal object TangemPayStoresModule { prefs = prefs, ) } + + @Provides + @Singleton + fun provideTangemPayIssueCardStore(prefs: AppPreferencesStore): TangemPayIssueCardStore { + return DefaultTangemPayIssueCardStore( + prefs = prefs, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayIssueCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayIssueCardStore.kt new file mode 100644 index 0000000000..561e53ea84 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayIssueCardStore.kt @@ -0,0 +1,33 @@ +package com.tangem.datasource.local.visa + +import androidx.datastore.preferences.core.stringPreferencesKey +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.utils.getObjectListSync +import com.tangem.datasource.local.preferences.utils.storeObjectList +import com.tangem.domain.models.wallet.UserWalletId + +internal class DefaultTangemPayIssueCardStore( + private val prefs: AppPreferencesStore, +) : TangemPayIssueCardStore { + + override suspend fun addIssueOrderId(userWalletId: UserWalletId, orderId: String) { + val current = prefs.getObjectListSync(getKey(userWalletId)) + if (orderId !in current) { + prefs.storeObjectList(key = getKey(userWalletId), value = current + orderId) + } + } + + override suspend fun getIssueOrderIds(userWalletId: UserWalletId): List { + return prefs.getObjectListSync(getKey(userWalletId)) + } + + override suspend fun removeIssueOrderId(userWalletId: UserWalletId, orderId: String) { + val current = prefs.getObjectListSync(getKey(userWalletId)) + if (orderId in current) { + prefs.storeObjectList(key = getKey(userWalletId), value = current - orderId) + } + } + + private fun getKey(userWalletId: UserWalletId) = + stringPreferencesKey("tangem_pay_issue_card_orders_${userWalletId.stringValue}") +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayIssueCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayIssueCardStore.kt new file mode 100644 index 0000000000..2c83579003 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayIssueCardStore.kt @@ -0,0 +1,19 @@ +package com.tangem.datasource.local.visa + +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Local store for additional-card issuance order ids. + * + * The backend `customer/me` response does not surface a card while it is still being issued, so the + * order id of an in-flight additional-card issuance is persisted here (per wallet) to render an + * "issuing" placeholder card until the order reaches a terminal state and the real card appears. + */ +interface TangemPayIssueCardStore { + + suspend fun addIssueOrderId(userWalletId: UserWalletId, orderId: String) + + suspend fun getIssueOrderIds(userWalletId: UserWalletId): List + + suspend fun removeIssueOrderId(userWalletId: UserWalletId, orderId: String) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 47dbad2d31..071bda168c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -75,6 +75,10 @@ internal interface TangemPayDataModule { @Singleton fun bindCloseCardRepository(repository: DefaultCloseCardRepository): TangemPayCloseCardRepository + @Binds + @Singleton + fun bindIssueCardRepository(repository: DefaultIssueCardRepository): TangemPayIssueCardRepository + @Binds @Singleton fun bindTangemPayCryptoCurrencyFactory(factory: DefaultTangemPayCurrencyFactory): TangemPayCurrencyFactory @@ -269,8 +273,17 @@ internal interface TangemPayDataModule { fun provideIssueAdditionalCardUseCase( customerOffersRepository: CustomerOffersRepository, customerOrderRepository: CustomerOrderRepository, + issueCardRepository: TangemPayIssueCardRepository, + startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + appCoroutineScope: AppCoroutineScope, ): IssueAdditionalCardUseCase { - return IssueAdditionalCardUseCase(customerOffersRepository, customerOrderRepository) + return IssueAdditionalCardUseCase( + customerOffersRepository = customerOffersRepository, + customerOrderRepository = customerOrderRepository, + issueCardRepository = issueCardRepository, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + appCoroutineScope = appCoroutineScope, + ) } } } \ No newline at end of file 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 0950ff21ae..68cc4759a6 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 @@ -53,6 +53,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val singleQuoteSupplier: SingleQuoteStatusSupplier, private val closeCardRepository: TangemPayCloseCardRepository, private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val issueCardRepository: TangemPayIssueCardRepository, ) : PaymentAccountStatusFetcher { private val logger = TangemLogger.withTag(TAG) @@ -360,13 +361,17 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( if (tangemPayCards.isEmpty()) return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) + // Additional-card issuance: the backend omits the new card until it is provisioned, so surface a + // placeholder for every locally tracked in-flight issuance order alongside the real cards. + val issuingCards = buildIssuingCards(userWalletId) + return PaymentAccountStatusValue.Loaded( source = StatusSource.ACTUAL, customerId = customerId, depositAddress = cryptoBalance.depositAddress, cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), fiatRate = fiatRate, - cards = tangemPayCards, + cards = tangemPayCards + issuingCards, balance = PaymentAccountStatusValue.Balance( fiatBalance = fiatBalance, cryptoBalance = cryptoBalance, @@ -399,6 +404,37 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } } + /** + * Builds the issuing placeholder cards from locally tracked additional-card orders. Each order is + * re-checked against the backend; terminal orders are dropped (and forgotten) because the real card + * is now part of [CustomerInfo], while in-flight orders surface as an issuing placeholder card. + */ + private suspend fun buildIssuingCards(userWalletId: UserWalletId): List { + val orderIds = issueCardRepository.getIssueOrderIds(userWalletId) + return orderIds.mapNotNull { orderId -> + val order = cardDetailsRepository.getOrderInfo(userWalletId, orderId).getOrNull() + if (order != null && order.orderStatus.isTerminal) { + issueCardRepository.removeIssueOrderId(userWalletId, orderId) + null + } else { + issuingPlaceholderCard(orderId) + } + } + } + + /** Placeholder card for an additional card that is still being issued (no backend card yet). */ + private fun issuingPlaceholderCard(orderId: String): TangemPayCard = TangemPayCard( + id = orderId, + productInstanceId = orderId, + cardStatus = TangemPayCard.Status.INACTIVE, + hasPinCode = false, + displayName = null, + limit = null, + frozenState = TangemPayCardFrozenState.Unfrozen, + lastDigits = "", + state = TangemPayCardState.Issuing, + ) + private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { return when (this) { is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultIssueCardRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultIssueCardRepository.kt new file mode 100644 index 0000000000..e1d4d04c30 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultIssueCardRepository.kt @@ -0,0 +1,25 @@ +package com.tangem.data.pay.repository + +import com.tangem.datasource.local.visa.TangemPayIssueCardStore +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.repository.TangemPayIssueCardRepository +import com.tangem.utils.coroutines.runSuspendCatching +import javax.inject.Inject + +internal class DefaultIssueCardRepository @Inject constructor( + private val tangemPayIssueCardStore: TangemPayIssueCardStore, +) : TangemPayIssueCardRepository { + + override suspend fun storeIssueOrderId(userWalletId: UserWalletId, orderId: String) { + runSuspendCatching { tangemPayIssueCardStore.addIssueOrderId(userWalletId, orderId) } + } + + override suspend fun getIssueOrderIds(userWalletId: UserWalletId): List { + return runSuspendCatching { tangemPayIssueCardStore.getIssueOrderIds(userWalletId) } + .getOrDefault(emptyList()) + } + + override suspend fun removeIssueOrderId(userWalletId: UserWalletId, orderId: String) { + runSuspendCatching { tangemPayIssueCardStore.removeIssueOrderId(userWalletId, orderId) } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCardState.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCardState.kt index 0521d18fc0..dca84161e1 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCardState.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCardState.kt @@ -20,18 +20,24 @@ enum class TangemPayCardState { /** A close order is in progress; the card is being closed. */ @SerialName("Closing") Closing, + + /** An issue order is in progress; the (additional) card is being issued and not yet provisioned. */ + @SerialName("Issuing") + Issuing, ; override fun toString() = when (this) { Active -> "Active" Reissuing -> "Reissuing" Closing -> "Closing" + Issuing -> "Issuing" } companion object { fun fromString(value: String) = when (value.lowercase(Locale.US)) { "reissuing" -> Reissuing "closing" -> Closing + "issuing" -> Issuing else -> Active } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayIssueCardRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayIssueCardRepository.kt new file mode 100644 index 0000000000..7117708cb8 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayIssueCardRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.pay.repository + +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Tracks in-flight additional-card issuance orders locally. + * + * The backend does not return a card while it is still being issued, so the issuance order id is + * stored locally to surface an "issuing" placeholder card until the order reaches a terminal state. + */ +interface TangemPayIssueCardRepository { + + suspend fun storeIssueOrderId(userWalletId: UserWalletId, orderId: String) + + suspend fun getIssueOrderIds(userWalletId: UserWalletId): List + + suspend fun removeIssueOrderId(userWalletId: UserWalletId, orderId: String) +} \ No newline at end of file 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 e019b138e4..5850737efd 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,12 +6,15 @@ 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.Order +import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.repository.CustomerOffersRepository import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.TangemPayIssueCardRepository import com.tangem.domain.pay.util.OrderResolver import com.tangem.domain.visa.error.VisaApiError +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.launch import java.util.UUID /** @@ -33,8 +36,11 @@ import java.util.UUID class IssueAdditionalCardUseCase( private val customerOffersRepository: CustomerOffersRepository, private val customerOrderRepository: CustomerOrderRepository, + private val issueCardRepository: TangemPayIssueCardRepository, + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + private val appCoroutineScope: AppCoroutineScope, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { val offer = catch( block = { customerOffersRepository.getOffers(userWalletId) @@ -61,20 +67,18 @@ class IssueAdditionalCardUseCase( idempotencyKey = UUID.randomUUID().toString(), ).bind() - Result(order = order, offer = offer) + issueCardRepository.storeIssueOrderId(userWalletId = userWalletId, orderId = order.id) + + appCoroutineScope.launch { + startTangemPayOrderPollingUseCase( + order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status), + userWalletId = userWalletId, + ) + } } private fun Raise.handleError(throwable: Throwable): Nothing { TangemLogger.e("Error in IssueAdditionalCardUseCase", throwable) raise(VisaApiError.Unspecified) } - - /** - * Outcome of a successful run. - * - - * @property offer the offer that authorised issuance, carried back so the caller can show pricing - * without an extra round trip. - */ - data class Result(val order: Order, val offer: Offer) } \ No newline at end of file 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 9c008202aa..474ac3d59d 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 @@ -10,7 +10,9 @@ import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.OrderType import com.tangem.domain.pay.repository.CustomerOffersRepository import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.TangemPayIssueCardRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.test.core.TestAppCoroutineScope import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk @@ -23,7 +25,15 @@ internal class IssueAdditionalCardUseCaseTest { private val offersRepository: CustomerOffersRepository = mockk() private val orderRepository: CustomerOrderRepository = mockk() - private val useCase = IssueAdditionalCardUseCase(offersRepository, orderRepository) + private val issueCardRepository: TangemPayIssueCardRepository = mockk(relaxed = true) + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase = mockk(relaxed = true) + private val useCase = IssueAdditionalCardUseCase( + customerOffersRepository = offersRepository, + customerOrderRepository = orderRepository, + issueCardRepository = issueCardRepository, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + appCoroutineScope = TestAppCoroutineScope(), + ) private val userWalletId = UserWalletId("1234567890ABCDEF") private val spec = "SP_000004" @@ -62,10 +72,9 @@ internal class IssueAdditionalCardUseCaseTest { val result = useCase(userWalletId) - val resultValue = result.getOrNull() - assertThat(resultValue?.order).isEqualTo(existing) - assertThat(resultValue?.offer).isEqualTo(offer) + assertThat(result.isRight()).isTrue() coVerify(exactly = 0) { orderRepository.createOrder(any(), any(), any(), any()) } + coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(userWalletId, existing.id) } } @Test @@ -118,7 +127,8 @@ internal class IssueAdditionalCardUseCaseTest { val result = useCase(userWalletId) - assertThat(result.getOrNull()?.order).isEqualTo(newOrder) + assertThat(result.isRight()).isTrue() + coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(userWalletId, newOrder.id) } } private fun order(id: String, type: OrderType, status: OrderStatus): Order = Order( 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 46c249f8f3..ab6db2a8ea 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 @@ -57,7 +57,7 @@ internal class TangemPayDetailsStateFactory( } fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM { - val card = status.cards.firstOrNull() + val hasUnfrozenCard = status.cards.any { it.frozenState == TangemPayCardFrozenState.Unfrozen } val errorNotificationConfig = when (status.error) { null -> null PaymentAccountStatusValue.Error.NotSynced -> createRenewSessionNotificationConfig() @@ -75,10 +75,7 @@ internal class TangemPayDetailsStateFactory( onRefresh = intents::onRefreshSwipe, ), balanceBlockState = TangemPayDetailsBalanceBlockState.Loading( - actionButtons = getActionButtonsConfig( - isEnabled = errorNotificationConfig == null && - (card == null || card.frozenState == TangemPayCardFrozenState.Unfrozen), - ), + actionButtons = getActionButtonsConfig(isEnabled = errorNotificationConfig == null && hasUnfrozenCard), cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState( cards = status.cards .let { if (isMultipleCardsEnabled) it else it.take(1) } @@ -86,9 +83,11 @@ internal class TangemPayDetailsStateFactory( TangemPayDetailsBalanceBlockState.Card( lastDigits = cardItem.lastDigits, onClick = { intents.onCardClick(cardItem.id) }, - isReissuing = cardItem.state != TangemPayCardState.Active, + isReissuingOrClosing = cardItem.state == TangemPayCardState.Reissuing || + cardItem.state == TangemPayCardState.Closing, isEnabled = errorNotificationConfig == null, isFrozen = cardItem.isFrozen, + isIssuing = cardItem.state == TangemPayCardState.Issuing, ) } .toImmutableList(), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index 02e00f3fdd..3ee5ed1968 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -94,7 +94,8 @@ internal sealed class TangemPayDetailsBalanceBlockState { data class Card( val lastDigits: String, val onClick: () -> Unit, - val isReissuing: Boolean, + val isReissuingOrClosing: Boolean, + val isIssuing: Boolean, val isFrozen: Boolean, val isEnabled: Boolean, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt index be01c7a721..8e5b386444 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt @@ -207,6 +207,12 @@ private fun LazyListScope.cardState(state: TangemPayCardPageUM) { subtitle = resourceReference(R.string.tangempay_card_page_closing_banner_description), ) } + TangemPayCardState.Issuing -> cardPageItem(key = "Issuing") { + TangemPayReplacingCardBlock( + title = resourceReference(R.string.tangempay_issuing_new_digital_card_title), + subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description), + ) + } } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index a49382d86b..f51ed7762a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -132,7 +132,7 @@ internal fun TangemPayDetailsScreen( }, ) - if (state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuing } == true) { + if (state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuingOrClosing } == true) { item( key = "REISSUE_MESSAGE", content = { @@ -328,7 +328,7 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi .alpha(if (card.isEnabled) 1f else DISABLED_ALPHA) .fillMaxSize(), painter = painterResource( - if (card.isReissuing) { + if (card.isReissuingOrClosing) { R.drawable.img_visa_card_inactive_48_32 } else { R.drawable.img_visa_card_48_32 @@ -336,7 +336,7 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi ), contentDescription = null, ) - if (!card.isReissuing) { + if (!card.isReissuingOrClosing) { Text( modifier = Modifier .align(Alignment.BottomStart) @@ -485,16 +485,18 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider { + state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuingOrClosing } == true -> { item("reissuingBannerBlock") { TangemMessage( modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), @@ -167,6 +167,15 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) { ) } } + state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isIssuing } == true -> { + item("issuingBannerBlock") { + TangemMessage( + 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), + ) + } + } else -> { if (state.errorNotificationConfig != null) { item("errorSessionBannerBlock") { @@ -342,7 +351,7 @@ private fun CardsBlock( ) { items(items = cardsBlockState.cards) { item -> TangemPayCardView( - isReissuing = item.isReissuing, + isIssueInProgress = item.isIssuing || item.isReissuingOrClosing, lastDigits = item.lastDigits, onClick = item.onClick, isEnabled = item.isEnabled, 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 118bd6d6e8..38da0cd2bd 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,211 +1,271 @@ 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 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.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SecondaryButtonIconEnd +import com.tangem.core.ui.components.SpacerH +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.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.core.ui.ds2.button.TangemButton +import com.tangem.core.ui.ds2.row.TangemRow +import com.tangem.core.ui.ds2.row.TangemRowContentLead +import com.tangem.core.ui.ds2.row.TangemRowText +import com.tangem.core.ui.ds2.row.TangemRowTextRole +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview +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 @Composable internal fun TangemPayIssueAdditionalCardContent(state: TangemPayIssueAdditionalCardUM) { - TangemModalBottomSheet( + TangemBottomSheet( config = TangemBottomSheetConfig( isShown = true, onDismissRequest = state.onDismiss, content = TangemBottomSheetConfigContent.Empty, ), - onBack = state.onDismiss, - containerColor = TangemTheme.colors.background.secondary, + type = TangemBottomSheetType.Modal, + containerColor = TangemTheme.colors3.bg.secondary, title = { - TangemModalBottomSheetTitle( - endIconRes = R.drawable.ic_close_24, - onEndClick = state.onDismiss, + 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 = { + Content(state) + }, + ) +} + +@Composable +private fun Content(state: TangemPayIssueAdditionalCardUM) { + val appearance = state.contentAppearance() + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CardIcon() - Text( - modifier = Modifier.padding(top = 16.dp), - text = stringResourceSafe(R.string.tangempay_issue_additional_card_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = stringResourceSafe(R.string.tangempay_issue_additional_card_description), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - FeeRow(modifier = Modifier.padding(top = 16.dp), state = state) - if (state.isBalanceInsufficient) { - InsufficientFundsBlock(modifier = Modifier.padding(top = 8.dp), onAddFundsClick = state.onAddFundsClick) - } - PrimaryButton( - modifier = Modifier - .padding(vertical = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.tangempay_issue_card), - showProgress = state.isLoading, - enabled = !state.isLoading && !state.isBalanceInsufficient, - onClick = state.onIssueClick, - ) - } + 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) } } @Composable -private fun CardIcon() { +private fun StatusIcon(appearance: IssueAdditionalCardContentAppearance) { Box( - contentAlignment = Alignment.Center, modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), - shape = CircleShape, - ) - .size(48.dp), + .size(80.dp) + .clip(CircleShape) + .background(appearance.iconBackgroundColor), + contentAlignment = Alignment.Center, ) { Icon( - modifier = Modifier.size(24.dp), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_credit_card_add_24), + imageVector = appearance.icon, contentDescription = null, - tint = TangemTheme.colors.icon.accent, + tint = appearance.iconColor, + modifier = Modifier.size(28.dp), ) } } @Composable -private fun FeeRow(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier +private fun CenteredMessageText(textRes: Int, style: TextStyle, color: Color) { + Text( + modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.background.action) - .padding(horizontal = 16.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResourceSafe(R.string.tangempay_issue_additional_card_fee_label), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.feeText, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } + .padding(horizontal = 16.dp), + text = stringResourceSafe(textRes), + style = style, + color = color, + textAlign = TextAlign.Center, + ) } @Composable -private fun InsufficientFundsBlock(onAddFundsClick: () -> Unit, modifier: Modifier = Modifier) { +private fun FeeBlock(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) { + TangemRow( + modifier = modifier, + contentLead = TangemRowContentLead.Start, + titleSlot = { + TangemRowText( + text = resourceReference(R.string.tangempay_issue_additional_card_fee_label), + role = TangemRowTextRole.Title, + ) + }, + valueSlot = { + TangemRowText( + text = state.feeText, + role = TangemRowTextRole.Value, + ) + }, + ) +} + +@Composable +private fun BottomButtonsBlock( + state: TangemPayIssueAdditionalCardUM, + appearance: IssueAdditionalCardContentAppearance, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.background.action) - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(top = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Row(verticalAlignment = Alignment.Top) { - Image( - modifier = Modifier.size(20.dp), - painter = painterResource(id = R.drawable.img_usdc_16), - contentDescription = null, - ) - Column(modifier = Modifier.padding(start = 12.dp)) { - Text( - text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = stringResourceSafe( - R.string.tangempay_reissue_card_insufficient_funds_subtitle, - ), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - SecondaryButtonIconEnd( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - text = stringResourceSafe(R.string.common_add_funds), - iconResId = R.drawable.ic_plus_24, - onClick = onAddFundsClick, + 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), ) } } -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +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 }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun TangemPayIssueAdditionalCardContentPreview( @PreviewParameter(IssueAdditionalCardPreviewProvider::class) state: TangemPayIssueAdditionalCardUM, ) { - TangemThemePreview { - TangemPayIssueAdditionalCardContent(state = state) + TangemThemePreviewRedesign { + IssueAdditionalCardSheetPreview(state = state) } } -private class IssueAdditionalCardPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - TangemPayIssueAdditionalCardUM( - isBalanceInsufficient = false, - feeText = "10 $", - isLoading = false, - onIssueClick = {}, - onAddFundsClick = {}, - onDismiss = {}, - ), - TangemPayIssueAdditionalCardUM( - isBalanceInsufficient = true, - feeText = "10 $", - isLoading = false, - onIssueClick = {}, - onAddFundsClick = {}, - onDismiss = {}, - ), - TangemPayIssueAdditionalCardUM( - isBalanceInsufficient = false, - feeText = "10 $", - isLoading = true, - onIssueClick = {}, - onAddFundsClick = {}, - onDismiss = {}, - ), +@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, + ) + }, ) -} \ No newline at end of file + Content(state) + } +} + +private class IssueAdditionalCardPreviewProvider : + CollectionPreviewParameterProvider( + collection = listOf( + stubState(), + stubState(isBalanceInsufficient = true), + stubState(isLoading = true), + ), + ) + +private fun stubState( + isBalanceInsufficient: Boolean = false, + isLoading: Boolean = false, +): TangemPayIssueAdditionalCardUM = TangemPayIssueAdditionalCardUM( + isBalanceInsufficient = isBalanceInsufficient, + feeText = "$4.25", + isLoading = isLoading, + onIssueClick = {}, + onAddFundsClick = {}, + onDismiss = {}, +) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/components/TangemPayCardView.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/components/TangemPayCardView.kt index 606aacb7e2..f61cbd80fb 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/components/TangemPayCardView.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/components/TangemPayCardView.kt @@ -41,7 +41,7 @@ private const val REISSUING_CARD_BG = 0xFF1E1E1E @Composable internal fun TangemPayCardView( - isReissuing: Boolean, + isIssueInProgress: Boolean, isEnabled: Boolean, lastDigits: String, onClick: () -> Unit, @@ -55,7 +55,7 @@ internal fun TangemPayCardView( width = TangemTheme.dimens2.x14, ) .testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON), - isReissuing = isReissuing, + isIssueInProgress = isIssueInProgress, isEnabled = isEnabled, onClick = onClick, ) { @@ -71,7 +71,7 @@ internal fun TangemPayCardView( modifier = Modifier.size(TangemTheme.dimens2.x3), imageVector = when { isFrozen -> Icons.ic_snowflake_16 - isReissuing -> Icons.ic_clock_12 + isIssueInProgress -> Icons.ic_clock_12 else -> Icons.ic_cloud_12_filled }, tint = TangemTheme.colors3.icon.staticDark, @@ -122,14 +122,14 @@ internal fun TangemPayAddCardView(onClick: () -> Unit, modifier: Modifier = Modi @Suppress("MagicNumber") @Composable private fun CardBackground( - isReissuing: Boolean, + isIssueInProgress: Boolean, isEnabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { - val bgColor = remember(isReissuing) { - if (isReissuing) { + val bgColor = remember(isIssueInProgress) { + if (isIssueInProgress) { Color(REISSUING_CARD_BG) } else { Color(DEFAULT_CARD_BG) @@ -151,9 +151,9 @@ private fun CardBackground( brush = Brush.radialGradient( colors = listOf( Color( - if (isReissuing) 0xFFB0B4BC else 0xFF38587F, + if (isIssueInProgress) 0xFFB0B4BC else 0xFF38587F, ).copy( - if (isReissuing) .1f else .41f, + if (isIssueInProgress) .1f else .41f, ), Color.Transparent, ), @@ -183,7 +183,7 @@ private fun CardBackgroundPreview() { height = TangemTheme.dimens2.x10, width = TangemTheme.dimens2.x14, ), - isReissuing = false, + isIssueInProgress = false, onClick = {}, content = {}, isEnabled = true, @@ -194,14 +194,14 @@ private fun CardBackgroundPreview() { height = TangemTheme.dimens2.x10, width = TangemTheme.dimens2.x14, ), - isReissuing = true, + isIssueInProgress = true, onClick = {}, content = {}, isEnabled = true, ) SpacerH(TangemTheme.dimens2.x4) TangemPayCardView( - isReissuing = false, + isIssueInProgress = false, onClick = {}, lastDigits = "1234", isEnabled = true, @@ -209,7 +209,7 @@ private fun CardBackgroundPreview() { ) SpacerH(TangemTheme.dimens2.x4) TangemPayCardView( - isReissuing = true, + isIssueInProgress = true, onClick = {}, lastDigits = "", isEnabled = true, @@ -218,7 +218,13 @@ private fun CardBackgroundPreview() { SpacerH(TangemTheme.dimens2.x4) TangemPayAddCardView(onClick = {}) SpacerH(TangemTheme.dimens2.x4) - TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234", isEnabled = true, isFrozen = true) + TangemPayCardView( + isIssueInProgress = false, + onClick = {}, + lastDigits = "1234", + isEnabled = true, + isFrozen = true, + ) } } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt index dcb2a6e6c0..55c4d6944f 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.message.BottomSheetMessage import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.core.ui.res.generated.icons.Icons +import com.tangem.core.ui.res.generated.icons.ic_error_28 import com.tangem.core.ui.res.generated.icons.ic_snowflake_20 import com.tangem.core.ui.res.generated.icons.ic_sun_20 import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType @@ -152,8 +153,9 @@ internal object TangemPayMessagesFactory { fun createMaximumCardsIssued(maxCards: Int): BottomSheetMessage { return bottomSheetMessage { infoBlock { - icon(R.drawable.img_attention_20) { - backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Attention + vector(Icons.ic_error_28) { + type = MessageBottomSheetUM.Vector.Type.Attention + backgroundType = MessageBottomSheetUM.Vector.BackgroundType.Attention } title = resourceReference(R.string.tangempay_maximum_cards_issued_title) body = resourceReference(