Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 03:26:53 -07:00
parent b208c0dd7c
commit 640ba5af98
14 changed files with 268 additions and 127 deletions

View file

@ -118,6 +118,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
state = TangemPayCardState.fromString(card.state), state = TangemPayCardState.fromString(card.state),
) )
}, },
error = null,
) )
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
source = StatusSource.CACHE, source = StatusSource.CACHE,
@ -134,6 +135,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
), ),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate, fiatRate = value.fiatRate,
error = null,
) )
null -> PaymentAccountStatusValue.Error.Unavailable null -> PaymentAccountStatusValue.Error.Unavailable
} }

View file

@ -7,6 +7,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.account.hasAccountData
import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitData import com.tangem.domain.models.pay.TangemPayCardLimitData
@ -156,8 +157,19 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue { private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue {
return onboardingRepository.getCustomerInfo(account.userWalletId).fold( return onboardingRepository.getCustomerInfo(account.userWalletId).fold(
ifLeft = { error -> ifLeft = { error ->
logger.e("proceedWithoutOrder ${account.userWalletId} error: $error") val cache = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId)
error.mapToPaymentAccountStatus(account.userWalletId) if (cache != null && cache.value.hasAccountData()) {
cache.value.copySealed(
source = StatusSource.ONLY_CACHE,
error = when (error) {
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
else -> PaymentAccountStatusValue.Error.Unavailable
},
)
} else {
logger.e("proceedWithoutOrder ${account.userWalletId} error: $error")
error.mapToPaymentAccountStatus(account.userWalletId)
}
}, },
ifRight = { customerInfo -> ifRight = { customerInfo ->
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}") logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
@ -300,6 +312,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
), ),
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
fiatRate = quotesData?.fiatRate, fiatRate = quotesData?.fiatRate,
error = null,
) )
} }
cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState( cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState(
@ -353,6 +366,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
state = cardState, state = cardState,
), ),
), ),
error = null,
) )
} }

View file

@ -75,6 +75,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
), ),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = BigDecimal("1.05"), fiatRate = BigDecimal("1.05"),
error = null,
) )
// WHEN // WHEN

View file

@ -3,6 +3,7 @@ package com.tangem.domain.models.account
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.PaymentAccountStatusValue.Loaded import com.tangem.domain.models.account.PaymentAccountStatusValue.Loaded
import com.tangem.domain.models.account.PaymentAccountStatusValue.Deactivated
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.kyc.KycStatus
@ -46,12 +47,12 @@ sealed class PaymentAccountStatusValue {
* *
* @param source The new source of the status information. * @param source The new source of the status information.
*/ */
fun copySealed(source: StatusSource): PaymentAccountStatusValue { fun copySealed(source: StatusSource, error: Error? = null): PaymentAccountStatusValue {
return when (this) { return when (this) {
is IssuingCard -> copy(source = source) is IssuingCard -> copy(source = source)
is Loaded -> copy(source = source) is Loaded -> copy(source = source, error = error ?: this.error)
is UnderReview -> copy(source = source) is UnderReview -> copy(source = source)
is Deactivated -> copy(source = source) is Deactivated -> copy(source = source, error = error ?: this.error)
is Loading, is Loading,
is Empty, is Empty,
is NotCreated, is NotCreated,
@ -110,6 +111,8 @@ sealed class PaymentAccountStatusValue {
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
* or `null` if the quote is not yet available. When `null`, * or `null` if the quote is not yet available. When `null`,
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed]. * [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
* @property error Transient error overlaid on top of cached data when a refresh fails
* (see [copySealed]), or `null` when the status is up to date. Not persisted.
*/ */
@Serializable @Serializable
data class Deactivated( data class Deactivated(
@ -118,6 +121,7 @@ sealed class PaymentAccountStatusValue {
val balance: Balance, val balance: Balance,
val cryptoCurrency: CryptoCurrency.Token, val cryptoCurrency: CryptoCurrency.Token,
val fiatRate: SerializedBigDecimal?, val fiatRate: SerializedBigDecimal?,
val error: Error?,
) : PaymentAccountStatusValue() { ) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency, currency = cryptoCurrency,
@ -143,6 +147,8 @@ sealed class PaymentAccountStatusValue {
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
* or `null` if the quote is not yet available. When `null`, * or `null` if the quote is not yet available. When `null`,
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed]. * [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
* @property error Transient error overlaid on top of cached data when a refresh fails
* (see [copySealed]), or `null` when the status is up to date. Not persisted.
*/ */
@Serializable @Serializable
data class Loaded( data class Loaded(
@ -153,6 +159,7 @@ sealed class PaymentAccountStatusValue {
val cryptoCurrency: CryptoCurrency.Token, val cryptoCurrency: CryptoCurrency.Token,
val cards: List<TangemPayCard>, val cards: List<TangemPayCard>,
val fiatRate: SerializedBigDecimal?, val fiatRate: SerializedBigDecimal?,
val error: Error?,
) : PaymentAccountStatusValue() { ) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency, currency = cryptoCurrency,
@ -278,6 +285,8 @@ private fun buildCryptoCurrencyStatusValue(
} }
} }
fun PaymentAccountStatusValue.hasAccountData(): Boolean = this is Loaded || this is Deactivated
fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId } fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId }
fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId } fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId }

View file

@ -8,7 +8,6 @@
<ID>BooleanPropertyNaming:TangemPayChangePinUM.kt$TangemPayChangePinUM$val submitButtonEnabled: Boolean</ID> <ID>BooleanPropertyNaming:TangemPayChangePinUM.kt$TangemPayChangePinUM$val submitButtonEnabled: Boolean</ID>
<ID>BooleanPropertyNaming:TangemPayChangePinUM.kt$TangemPayChangePinUM$val submitButtonLoading: Boolean</ID> <ID>BooleanPropertyNaming:TangemPayChangePinUM.kt$TangemPayChangePinUM$val submitButtonLoading: Boolean</ID>
<ID>BooleanPropertyNaming:TangemPayDetailsScreen.kt$var showDropdownMenu by rememberSaveable { mutableStateOf(false) }</ID> <ID>BooleanPropertyNaming:TangemPayDetailsScreen.kt$var showDropdownMenu by rememberSaveable { mutableStateOf(false) }</ID>
<ID>BooleanPropertyNaming:TangemPayDetailsUM.kt$TangemPayDetailsUM$val addFundsEnabled: Boolean</ID>
<ID>BooleanPropertyNaming:TangemPayTxHistoryListManager.kt$TangemPayTxHistoryListManager$val clearUiBatches = state.status is PaginationStatus.InitialLoading &amp;&amp; batchListState.status is PaginationStatus.Paginating</ID> <ID>BooleanPropertyNaming:TangemPayTxHistoryListManager.kt$TangemPayTxHistoryListManager$val clearUiBatches = state.status is PaginationStatus.InitialLoading &amp;&amp; batchListState.status is PaginationStatus.Paginating</ID>
<ID>CanBeNonNullable:TangemPayTxHistoryDetailsModel.kt$TangemPayTxHistoryDetailsModel$txHash: String?</ID> <ID>CanBeNonNullable:TangemPayTxHistoryDetailsModel.kt$TangemPayTxHistoryDetailsModel$txHash: String?</ID>
<ID>MultilineLambdaItParameter:TangemPayAddFundsContent.kt${ key(it.title) { TangemPayTopUpItem(state = it) } }</ID> <ID>MultilineLambdaItParameter:TangemPayAddFundsContent.kt${ key(it.title) { TangemPayTopUpItem(state = it) } }</ID>

View file

@ -10,7 +10,10 @@ import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_document_20 import com.tangem.core.ui.res.generated.icons.ic_document_20
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.pay.isFrozen
import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.utils.TangemPayDetailIntents import com.tangem.features.tangempay.utils.TangemPayDetailIntents
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -21,55 +24,127 @@ internal class TangemPayDetailsStateFactory(
private val onBack: () -> Unit, private val onBack: () -> Unit,
private val onOpenMenu: () -> Unit, private val onOpenMenu: () -> Unit,
private val intents: TangemPayDetailIntents, private val intents: TangemPayDetailIntents,
private val cardFrozenState: TangemPayCardFrozenState,
private val isRedesignEnabled: Boolean, private val isRedesignEnabled: Boolean,
) { ) {
@Suppress("LongMethod") fun getLoadingState(): TangemPayDetailsUM {
fun getInitialState(
isTangemPayDeactivated: Boolean,
cardNumberEnd: String,
isReissuing: Boolean,
isFrozen: Boolean,
): TangemPayDetailsUM {
return TangemPayDetailsUM( return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig( topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack, onBackClick = onBack,
onOpenMenu = onOpenMenu, onOpenMenu = onOpenMenu,
items = getTopBarMenuItems(isTangemPayDeactivated), items = getTopBarMenuItems(),
itemsV2 = getTopBarMenuItemsV2(isTangemPayDeactivated), itemsV2 = getTopBarMenuItemsV2(),
), ),
pullToRefreshConfig = PullToRefreshConfig( pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false, isRefreshing = false,
onRefresh = intents::onRefreshSwipe, onRefresh = intents::onRefreshSwipe,
), ),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading( balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
actionButtons = getActionButtonsConfig(), actionButtons = persistentListOf(),
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState( cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
cards = persistentListOf( cards = persistentListOf(),
TangemPayDetailsBalanceBlockState.Card(
lastDigits = cardNumberEnd,
onClick = {},
isReissuing = isReissuing,
isFrozen = isFrozen,
),
),
onAddCardClick = intents::onAddCardClick, onAddCardClick = intents::onAddCardClick,
).takeIf { !isTangemPayDeactivated }, ),
), ),
isBalanceHidden = false, isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = null, addToWalletBlockState = null,
accountDeactivatedNotificationConfig = NotificationConfig( errorNotificationConfig = null,
title = resourceReference(R.string.tangempay_account_deactivated_message_title), accountDeactivatedNotificationConfig = null,
subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle),
iconResId = R.drawable.img_attention_20,
).takeIf { isTangemPayDeactivated },
) )
} }
private fun getTopBarMenuItems(isTangemPayDeactivated: Boolean): ImmutableList<TangemDropdownMenuItem> { fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM {
if (isTangemPayDeactivated) return persistentListOf() val card = status.cards.firstOrNull()
val errorNotificationConfig = when (status.error) {
null -> null
PaymentAccountStatusValue.Error.NotSynced -> createRenewSessionNotificationConfig()
else -> createAccountUnavailableConfig()
}
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
onOpenMenu = onOpenMenu,
items = getTopBarMenuItems(),
itemsV2 = getTopBarMenuItemsV2(),
),
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = intents::onRefreshSwipe,
),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
actionButtons = getActionButtonsConfig(
isEnabled = errorNotificationConfig == null &&
(card == null || card.frozenState == TangemPayCardFrozenState.Unfrozen),
),
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
cards = card?.let {
persistentListOf(
TangemPayDetailsBalanceBlockState.Card(
lastDigits = card.lastDigits,
onClick = intents::onCardClick,
isReissuing = card.state != TangemPayCardState.Active,
isEnabled = errorNotificationConfig == null,
isFrozen = card.isFrozen,
),
)
} ?: persistentListOf(),
onAddCardClick = intents::onAddCardClick,
),
),
isBalanceHidden = false,
addToWalletBlockState = null,
errorNotificationConfig = errorNotificationConfig,
accountDeactivatedNotificationConfig = null,
)
}
fun getDeactivatedState(): TangemPayDetailsUM {
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
onOpenMenu = onOpenMenu,
items = persistentListOf(),
itemsV2 = persistentListOf(),
),
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = intents::onRefreshSwipe,
),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
actionButtons = getActionButtonsConfig(isEnabled = true),
cardsBlockState = null,
),
isBalanceHidden = false,
addToWalletBlockState = null,
errorNotificationConfig = null,
accountDeactivatedNotificationConfig = createAccountDeactivatedConfig(),
)
}
private fun createAccountUnavailableConfig() = NotificationConfig(
title = resourceReference(R.string.tangempay_temporarily_unavailable),
subtitle = resourceReference(R.string.tangempay_service_unreachable_try_later),
iconResId = R.drawable.img_attention_20,
iconTint = NotificationConfig.IconTint.Attention,
)
private fun createAccountDeactivatedConfig() = NotificationConfig(
title = resourceReference(R.string.tangempay_account_deactivated_message_title),
subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle),
iconResId = R.drawable.img_attention_20,
)
private fun createRenewSessionNotificationConfig() = NotificationConfig(
title = resourceReference(R.string.tangempay_sync_needed_title),
subtitle = resourceReference(R.string.tangempay_sync_needed_body),
iconResId = R.drawable.img_attention_20,
iconTint = NotificationConfig.IconTint.Attention,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.tangempay_sync_needed_button),
onClick = intents::onRenewSession,
),
)
private fun getTopBarMenuItems(): ImmutableList<TangemDropdownMenuItem> {
return persistentListOf( return persistentListOf(
TangemDropdownMenuItem( TangemDropdownMenuItem(
title = resourceReference(R.string.tangem_pay_terms_limits), title = resourceReference(R.string.tangem_pay_terms_limits),
@ -84,9 +159,7 @@ internal class TangemPayDetailsStateFactory(
) )
} }
private fun getTopBarMenuItemsV2(isTangemPayDeactivated: Boolean): ImmutableList<TangemPayDropDownItemUM> { private fun getTopBarMenuItemsV2(): ImmutableList<TangemPayDropDownItemUM> {
if (isTangemPayDeactivated) return persistentListOf()
return persistentListOf( return persistentListOf(
TangemPayDropDownItemUM( TangemPayDropDownItemUM(
title = resourceReference(R.string.tangem_pay_terms_limits), title = resourceReference(R.string.tangem_pay_terms_limits),
@ -111,7 +184,7 @@ internal class TangemPayDetailsStateFactory(
) )
} }
private fun getActionButtonsConfig(): ImmutableList<ActionButtonConfig> { private fun getActionButtonsConfig(isEnabled: Boolean): ImmutableList<ActionButtonConfig> {
return persistentListOf( return persistentListOf(
ActionButtonConfig( ActionButtonConfig(
text = resourceReference(id = R.string.tangempay_card_details_add_funds), text = resourceReference(id = R.string.tangempay_card_details_add_funds),
@ -121,13 +194,13 @@ internal class TangemPayDetailsStateFactory(
R.drawable.ic_plus_24 R.drawable.ic_plus_24
}, },
onClick = intents::onClickAddFunds, onClick = intents::onClickAddFunds,
isEnabled = cardFrozenState == TangemPayCardFrozenState.Unfrozen, isEnabled = isEnabled,
), ),
ActionButtonConfig( ActionButtonConfig(
text = resourceReference(id = R.string.tangempay_card_details_withdraw), text = resourceReference(id = R.string.tangempay_card_details_withdraw),
iconResId = R.drawable.ic_arrow_up_24, iconResId = R.drawable.ic_arrow_up_24,
onClick = intents::onClickWithdraw, onClick = intents::onClickWithdraw,
isEnabled = cardFrozenState == TangemPayCardFrozenState.Unfrozen, isEnabled = isEnabled,
), ),
) )
} }

View file

@ -16,7 +16,7 @@ internal data class TangemPayDetailsUM(
val balanceBlockState: TangemPayDetailsBalanceBlockState, val balanceBlockState: TangemPayDetailsBalanceBlockState,
val addToWalletBlockState: AddToWalletBlockState?, val addToWalletBlockState: AddToWalletBlockState?,
val isBalanceHidden: Boolean, val isBalanceHidden: Boolean,
val addFundsEnabled: Boolean, val errorNotificationConfig: NotificationConfig?,
val accountDeactivatedNotificationConfig: NotificationConfig?, val accountDeactivatedNotificationConfig: NotificationConfig?,
) )
@ -93,6 +93,7 @@ internal sealed class TangemPayDetailsBalanceBlockState {
val onClick: () -> Unit, val onClick: () -> Unit,
val isReissuing: Boolean, val isReissuing: Boolean,
val isFrozen: Boolean, val isFrozen: Boolean,
val isEnabled: Boolean,
) )
} }

View file

@ -15,27 +15,28 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tangempay.components.AddFundsListener import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
@ -73,14 +74,11 @@ internal class TangemPayDetailsModel @Inject constructor(
private val expressTransactionsEventListener: ExpressTransactionsEventListener, private val expressTransactionsEventListener: ExpressTransactionsEventListener,
private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val produceTangemPayInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener { ) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
private val isTangemPayDeactivated = params.initialStatus.isDeactivated
private val initialCard = params.initialStatus.ifLoadedOrNull { it.cards.firstOrNull() }
private val currentStatus = MutableStateFlow(params.initialStatus) private val currentStatus = MutableStateFlow(params.initialStatus)
private val userWalletId private val userWalletId
@ -93,25 +91,20 @@ internal class TangemPayDetailsModel @Inject constructor(
onBack = router::pop, onBack = router::pop,
onOpenMenu = ::onOpenMenu, onOpenMenu = ::onOpenMenu,
intents = this, intents = this,
cardFrozenState = when {
initialCard == null -> TangemPayCardFrozenState.Unfrozen
else -> initialCard.frozenState
},
isRedesignEnabled = isRedesignEnabled(), isRedesignEnabled = isRedesignEnabled(),
) )
val uiState: StateFlow<TangemPayDetailsUM> val uiState: StateFlow<TangemPayDetailsUM>
field = MutableStateFlow( field = MutableStateFlow(
stateFactory.getInitialState( when {
isTangemPayDeactivated = isTangemPayDeactivated, params.initialStatus.isDeactivated -> stateFactory.getDeactivatedState()
cardNumberEnd = initialCard?.lastDigits.orEmpty(), else -> stateFactory.getLoadingState()
isReissuing = initialCard == null || initialCard.state != TangemPayCardState.Active, },
isFrozen = initialCard?.frozenState == TangemPayCardFrozenState.Frozen,
),
) )
private val refreshStateJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder()
private val addToWalletBannerJobHolder = JobHolder() private val addToWalletBannerJobHolder = JobHolder()
private val frozenStateJobHolder = JobHolder()
val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation() val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation()
@ -119,38 +112,27 @@ internal class TangemPayDetailsModel @Inject constructor(
analytics.send(TangemPayAnalyticsEvents.MainScreenOpened()) analytics.send(TangemPayAnalyticsEvents.MainScreenOpened())
handleBalanceHiding() handleBalanceHiding()
val statusFlow = paymentAccountStatusSupplier.invoke(userWalletId) paymentAccountStatusSupplier.invoke(userWalletId)
.onEach { status -> currentStatus.update { status } } .onEach { status -> currentStatus.update { status } }
.map { it.value } .map { it.value }
.onEach { state ->
if (isTangemPayDeactivated) { when (state) {
statusFlow is PaymentAccountStatusValue.Deactivated -> {
.filterIsInstance<PaymentAccountStatusValue.Deactivated>() uiState.update { stateFactory.getDeactivatedState() }
.onEach { state -> uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
}
.launchIn(modelScope)
} else {
if (initialCard != null) {
subscribeToCardFrozenState(initialCard.id)
}
fetchAddToWalletBanner()
statusFlow
.filterIsInstance<PaymentAccountStatusValue.Loaded>()
.filter { it.source == StatusSource.ACTUAL }
.onEach { state ->
uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
state.cards.firstOrNull()?.let { card ->
uiState.update(
TangemPayCardDataTransformer(
card = card,
onCardClick = { onCardClick() },
),
)
} }
is PaymentAccountStatusValue.Loaded -> {
fetchAddToWalletBanner()
uiState.update { stateFactory.getLoadedState(state) }
uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
state.cards.firstOrNull()?.let { card ->
subscribeToCardFrozenState(card.id)
}
}
else -> uiState.update { stateFactory.getLoadingState() }
} }
.launchIn(modelScope) }
} .launchIn(modelScope)
} }
fun onResume() { fun onResume() {
@ -168,10 +150,12 @@ internal class TangemPayDetailsModel @Inject constructor(
fun isRedesignEnabled(): Boolean = tangemPayFeatureToggles.isRedesignEnabled fun isRedesignEnabled(): Boolean = tangemPayFeatureToggles.isRedesignEnabled
private fun subscribeToCardFrozenState(cardId: String) { private fun subscribeToCardFrozenState(cardId: String) {
frozenStateJobHolder.cancel()
cardDetailsRepository cardDetailsRepository
.cardFrozenState(cardId) .cardFrozenState(cardId)
.onEach { uiState.update(TangemPayFreezeUnfreezeStateTransformer(cardFrozenState = it)) } .onEach { uiState.update(TangemPayFreezeUnfreezeStateTransformer(cardFrozenState = it)) }
.launchIn(modelScope) .launchIn(modelScope)
.saveIn(frozenStateJobHolder)
} }
override fun onClickAddFunds() { override fun onClickAddFunds() {
@ -375,6 +359,21 @@ internal class TangemPayDetailsModel @Inject constructor(
) )
} }
override fun onRenewSession() {
uiState.update(TangemPayRenewSessionTransformer(shouldShowProgress = true))
modelScope.launch {
produceTangemPayInitialDataUseCase(userWalletId)
.onRight {
paymentAccountStatusFetcher.invoke(userWalletId)
uiState.update(TangemPayRenewSessionTransformer(shouldShowProgress = false))
}
.onLeft {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_error)))
uiState.update(TangemPayRenewSessionTransformer(shouldShowProgress = false))
}
}
}
private fun showBottomSheetError(type: TangemPayDetailsErrorType) { private fun showBottomSheetError(type: TangemPayDetailsErrorType) {
uiMessageSender.send(message = TangemPayMessagesFactory.createErrorMessage(errorType = type)) uiMessageSender.send(message = TangemPayMessagesFactory.createErrorMessage(errorType = type))
} }

View file

@ -1,33 +0,0 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
internal class TangemPayCardDataTransformer(
private val card: TangemPayCard,
private val onCardClick: () -> Unit,
) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val updatedCard = TangemPayDetailsBalanceBlockState.Card(
lastDigits = card.lastDigits,
onClick = onCardClick,
isReissuing = card.state != TangemPayCardState.Active,
isFrozen = card.frozenState == TangemPayCardFrozenState.Frozen,
)
val cardsBlockState = prevState.balanceBlockState.cardsBlockState?.copy(
cards = persistentListOf(updatedCard),
)
val newBalanceBlockState = when (val bs = prevState.balanceBlockState) {
is TangemPayDetailsBalanceBlockState.Loading -> bs.copy(cardsBlockState = cardsBlockState)
is TangemPayDetailsBalanceBlockState.Content -> bs.copy(cardsBlockState = cardsBlockState)
is TangemPayDetailsBalanceBlockState.Error -> bs.copy(cardsBlockState = cardsBlockState)
}
return prevState.copy(balanceBlockState = newBalanceBlockState)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
internal class TangemPayRenewSessionTransformer(
private val shouldShowProgress: Boolean,
) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val config = prevState.errorNotificationConfig ?: return prevState
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
?: return prevState
return prevState.copy(
errorNotificationConfig = config.copy(
buttonsState = button.copy(shouldShowProgress = shouldShowProgress),
),
)
}
}

View file

@ -19,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -61,6 +62,8 @@ import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
private const val DISABLED_ALPHA = 0.5f
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
internal fun TangemPayDetailsScreen( internal fun TangemPayDetailsScreen(
@ -100,6 +103,20 @@ internal fun TangemPayDetailsScreen(
.fillMaxWidth(), .fillMaxWidth(),
) )
} }
if (state.errorNotificationConfig != null) {
item(
key = "error_message",
content = {
Notification(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = 12.dp)
.fillMaxWidth(),
config = state.errorNotificationConfig,
)
},
)
}
item( item(
key = TangemPayDetailsBalanceBlockState::class.java, key = TangemPayDetailsBalanceBlockState::class.java,
content = { content = {
@ -300,11 +317,16 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi
Box( Box(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.clickable(onClick = card.onClick) .clickable(
onClick = card.onClick,
enabled = card.isEnabled,
)
.testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON), .testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON),
) { ) {
Image( Image(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.alpha(if (card.isEnabled) 1f else DISABLED_ALPHA)
.fillMaxSize(),
painter = painterResource( painter = painterResource(
if (card.isReissuing) { if (card.isReissuing) {
R.drawable.img_visa_card_inactive_48_32 R.drawable.img_visa_card_inactive_48_32
@ -464,12 +486,14 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
lastDigits = "1234", lastDigits = "1234",
onClick = {}, onClick = {},
isReissuing = false, isReissuing = false,
isEnabled = false,
isFrozen = false, isFrozen = false,
), ),
TangemPayDetailsBalanceBlockState.Card( TangemPayDetailsBalanceBlockState.Card(
lastDigits = "3456", lastDigits = "3456",
onClick = {}, onClick = {},
isReissuing = false, isReissuing = false,
isEnabled = true,
isFrozen = false, isFrozen = false,
), ),
), ),
@ -477,12 +501,12 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
), ),
), ),
isBalanceHidden = false, isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = AddToWalletBlockState( addToWalletBlockState = AddToWalletBlockState(
onClick = {}, onClick = {},
onClickClose = {}, onClickClose = {},
), ),
accountDeactivatedNotificationConfig = null, accountDeactivatedNotificationConfig = null,
errorNotificationConfig = null,
), ),
TangemPayDetailsUM( TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig( topBarConfig = TangemPayDetailsTopBarConfig(
@ -501,15 +525,16 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
onClick = {}, onClick = {},
isReissuing = true, isReissuing = true,
isFrozen = false, isFrozen = false,
isEnabled = true,
), ),
), ),
onAddCardClick = {}, onAddCardClick = {},
), ),
), ),
isBalanceHidden = false, isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = null, addToWalletBlockState = null,
accountDeactivatedNotificationConfig = null, accountDeactivatedNotificationConfig = null,
errorNotificationConfig = null,
), ),
), ),
) )

View file

@ -184,6 +184,14 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
} }
} }
else -> { else -> {
if (state.errorNotificationConfig != null) {
item("errorSessionBannerBlock") {
TangemMessage(
config = state.errorNotificationConfig,
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
)
}
}
if (state.addToWalletBlockState != null) { if (state.addToWalletBlockState != null) {
item("addToWalletBannerBlock") { item("addToWalletBannerBlock") {
TangemPayAddToWalletBlock( TangemPayAddToWalletBlock(
@ -340,6 +348,7 @@ private fun CardsBlock(
isReissuing = item.isReissuing, isReissuing = item.isReissuing,
lastDigits = item.lastDigits, lastDigits = item.lastDigits,
onClick = item.onClick, onClick = item.onClick,
isEnabled = item.isEnabled,
isFrozen = item.isFrozen, isFrozen = item.isFrozen,
) )
SpacerW(TangemTheme.dimens2.x2) SpacerW(TangemTheme.dimens2.x2)

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -41,6 +42,7 @@ private const val REISSUING_CARD_BG = 0xFF1E1E1E
@Composable @Composable
internal fun TangemPayCardView( internal fun TangemPayCardView(
isReissuing: Boolean, isReissuing: Boolean,
isEnabled: Boolean,
lastDigits: String, lastDigits: String,
onClick: () -> Unit, onClick: () -> Unit,
isFrozen: Boolean, isFrozen: Boolean,
@ -54,6 +56,7 @@ internal fun TangemPayCardView(
) )
.testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON), .testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON),
isReissuing = isReissuing, isReissuing = isReissuing,
isEnabled = isEnabled,
onClick = onClick, onClick = onClick,
) { ) {
Row( Row(
@ -120,6 +123,7 @@ internal fun TangemPayAddCardView(onClick: () -> Unit, modifier: Modifier = Modi
@Composable @Composable
private fun CardBackground( private fun CardBackground(
isReissuing: Boolean, isReissuing: Boolean,
isEnabled: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit, content: @Composable BoxScope.() -> Unit,
@ -134,6 +138,7 @@ private fun CardBackground(
Box( Box(
modifier = modifier modifier = modifier
.alpha(if (isEnabled) 1f else 0.5f)
.clip(RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp))
.drawBehind { .drawBehind {
drawRect(bgColor) drawRect(bgColor)
@ -163,7 +168,7 @@ private fun CardBackground(
color = TangemTheme.colors3.border.primary, color = TangemTheme.colors3.border.primary,
shape = RoundedCornerShape(6.dp), shape = RoundedCornerShape(6.dp),
) )
.clickableSingle(onClick = onClick), .clickableSingle(onClick = onClick, enabled = isEnabled),
content = content, content = content,
) )
} }
@ -181,6 +186,7 @@ private fun CardBackgroundPreview() {
isReissuing = false, isReissuing = false,
onClick = {}, onClick = {},
content = {}, content = {},
isEnabled = true,
) )
SpacerH(TangemTheme.dimens2.x4) SpacerH(TangemTheme.dimens2.x4)
CardBackground( CardBackground(
@ -191,15 +197,28 @@ private fun CardBackgroundPreview() {
isReissuing = true, isReissuing = true,
onClick = {}, onClick = {},
content = {}, content = {},
isEnabled = true,
) )
SpacerH(TangemTheme.dimens2.x4) SpacerH(TangemTheme.dimens2.x4)
TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234", isFrozen = false) TangemPayCardView(
isReissuing = false,
onClick = {},
lastDigits = "1234",
isEnabled = true,
isFrozen = false,
)
SpacerH(TangemTheme.dimens2.x4) SpacerH(TangemTheme.dimens2.x4)
TangemPayCardView(isReissuing = true, onClick = {}, lastDigits = "", isFrozen = false) TangemPayCardView(
isReissuing = true,
onClick = {},
lastDigits = "",
isEnabled = true,
isFrozen = false,
)
SpacerH(TangemTheme.dimens2.x4) SpacerH(TangemTheme.dimens2.x4)
TangemPayAddCardView(onClick = {}) TangemPayAddCardView(onClick = {})
SpacerH(TangemTheme.dimens2.x4) SpacerH(TangemTheme.dimens2.x4)
TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234", isFrozen = true) TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234", isEnabled = true, isFrozen = true)
} }
} }
} }

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
internal interface TangemPayDetailIntents { internal interface TangemPayDetailIntents {
fun onContactSupportClicked() fun onContactSupportClicked()
fun onRefreshSwipe(refreshState: ShowRefreshState) fun onRefreshSwipe(refreshState: ShowRefreshState)
fun onRenewSession()
fun onClickAddFunds() fun onClickAddFunds()
fun onClickWithdraw() fun onClickWithdraw()
fun onClickTermsAndLimits() fun onClickTermsAndLimits()