From 640ba5af98978a547a7f8af03a3740d1ccefc087 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jun 2026 03:26:53 -0700 Subject: [PATCH] Updated on 2026-08-14 --- .../PaymentAccountStatusValueDMConverter.kt | 2 + .../DefaultPaymentAccountStatusFetcher.kt | 18 ++- ...aymentAccountStatusValueDMConverterTest.kt | 1 + .../account/PaymentAccountStatusValue.kt | 15 +- .../details/impl/detekt-baseline-debug.xml | 1 - .../entity/TangemPayDetailsStateFactory.kt | 141 +++++++++++++----- .../tangempay/entity/TangemPayDetailsUM.kt | 3 +- .../tangempay/model/TangemPayDetailsModel.kt | 89 ++++++----- .../TangemPayCardDataTransformer.kt | 33 ---- .../TangemPayRenewSessionTransformer.kt | 22 +++ .../tangempay/ui/TangemPayDetailsScreen.kt | 33 +++- .../tangempay/ui/TangemPayDetailsScreenV2.kt | 9 ++ .../ui/components/TangemPayCardView.kt | 27 +++- .../tangempay/utils/TangemPayDetailIntents.kt | 1 + 14 files changed, 268 insertions(+), 127 deletions(-) delete mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayRenewSessionTransformer.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index c586313c67..b9cf0f60a1 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -118,6 +118,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( state = TangemPayCardState.fromString(card.state), ) }, + error = null, ) is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( source = StatusSource.CACHE, @@ -134,6 +135,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( ), cryptoCurrency = cryptoCurrency, fiatRate = value.fiatRate, + error = null, ) null -> PaymentAccountStatusValue.Error.Unavailable } 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 7c205fffd6..e6342cdf4d 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 @@ -7,6 +7,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus 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.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardLimitData @@ -156,8 +157,19 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue { return onboardingRepository.getCustomerInfo(account.userWalletId).fold( ifLeft = { error -> - logger.e("proceedWithoutOrder ${account.userWalletId} error: $error") - error.mapToPaymentAccountStatus(account.userWalletId) + val cache = paymentAccountStatusesStore.getSyncOrNull(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 -> logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}") @@ -300,6 +312,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ), cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), fiatRate = quotesData?.fiatRate, + error = null, ) } cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState( @@ -353,6 +366,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( state = cardState, ), ), + error = null, ) } diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt index d3213bc914..25a40bb989 100644 --- a/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverterTest.kt @@ -75,6 +75,7 @@ internal class PaymentAccountStatusValueDMConverterTest { ), cryptoCurrency = cryptoCurrency, fiatRate = BigDecimal("1.05"), + error = null, ) // WHEN diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index 7d0b33b7de..d32bc79f81 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -3,6 +3,7 @@ package com.tangem.domain.models.account import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance 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.CryptoCurrencyStatus import com.tangem.domain.models.kyc.KycStatus @@ -46,12 +47,12 @@ sealed class PaymentAccountStatusValue { * * @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) { 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 Deactivated -> copy(source = source) + is Deactivated -> copy(source = source, error = error ?: this.error) is Loading, is Empty, is NotCreated, @@ -110,6 +111,8 @@ sealed class PaymentAccountStatusValue { * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, * or `null` if the quote is not yet available. When `null`, * [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 data class Deactivated( @@ -118,6 +121,7 @@ sealed class PaymentAccountStatusValue { val balance: Balance, val cryptoCurrency: CryptoCurrency.Token, val fiatRate: SerializedBigDecimal?, + val error: Error?, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = cryptoCurrency, @@ -143,6 +147,8 @@ sealed class PaymentAccountStatusValue { * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, * or `null` if the quote is not yet available. When `null`, * [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 data class Loaded( @@ -153,6 +159,7 @@ sealed class PaymentAccountStatusValue { val cryptoCurrency: CryptoCurrency.Token, val cards: List, val fiatRate: SerializedBigDecimal?, + val error: Error?, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( 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.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId } diff --git a/features/tangempay/details/impl/detekt-baseline-debug.xml b/features/tangempay/details/impl/detekt-baseline-debug.xml index 553d0e2331..347da6d75a 100644 --- a/features/tangempay/details/impl/detekt-baseline-debug.xml +++ b/features/tangempay/details/impl/detekt-baseline-debug.xml @@ -8,7 +8,6 @@ BooleanPropertyNaming:TangemPayChangePinUM.kt$TangemPayChangePinUM$val submitButtonEnabled: Boolean BooleanPropertyNaming:TangemPayChangePinUM.kt$TangemPayChangePinUM$val submitButtonLoading: Boolean BooleanPropertyNaming:TangemPayDetailsScreen.kt$var showDropdownMenu by rememberSaveable { mutableStateOf(false) } - BooleanPropertyNaming:TangemPayDetailsUM.kt$TangemPayDetailsUM$val addFundsEnabled: Boolean BooleanPropertyNaming:TangemPayTxHistoryListManager.kt$TangemPayTxHistoryListManager$val clearUiBatches = state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating CanBeNonNullable:TangemPayTxHistoryDetailsModel.kt$TangemPayTxHistoryDetailsModel$txHash: String? MultilineLambdaItParameter:TangemPayAddFundsContent.kt${ key(it.title) { TangemPayTopUpItem(state = it) } } 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 260e32bcf3..e0591414d9 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 @@ -10,7 +10,10 @@ import com.tangem.core.ui.extensions.themedColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.generated.icons.Icons 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.TangemPayCardState +import com.tangem.domain.models.pay.isFrozen import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.utils.TangemPayDetailIntents import kotlinx.collections.immutable.ImmutableList @@ -21,55 +24,127 @@ internal class TangemPayDetailsStateFactory( private val onBack: () -> Unit, private val onOpenMenu: () -> Unit, private val intents: TangemPayDetailIntents, - private val cardFrozenState: TangemPayCardFrozenState, private val isRedesignEnabled: Boolean, ) { - @Suppress("LongMethod") - fun getInitialState( - isTangemPayDeactivated: Boolean, - cardNumberEnd: String, - isReissuing: Boolean, - isFrozen: Boolean, - ): TangemPayDetailsUM { + fun getLoadingState(): TangemPayDetailsUM { return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( onBackClick = onBack, onOpenMenu = onOpenMenu, - items = getTopBarMenuItems(isTangemPayDeactivated), - itemsV2 = getTopBarMenuItemsV2(isTangemPayDeactivated), + items = getTopBarMenuItems(), + itemsV2 = getTopBarMenuItemsV2(), ), pullToRefreshConfig = PullToRefreshConfig( isRefreshing = false, onRefresh = intents::onRefreshSwipe, ), balanceBlockState = TangemPayDetailsBalanceBlockState.Loading( - actionButtons = getActionButtonsConfig(), + actionButtons = persistentListOf(), cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState( - cards = persistentListOf( - TangemPayDetailsBalanceBlockState.Card( - lastDigits = cardNumberEnd, - onClick = {}, - isReissuing = isReissuing, - isFrozen = isFrozen, - ), - ), + cards = persistentListOf(), onAddCardClick = intents::onAddCardClick, - ).takeIf { !isTangemPayDeactivated }, + ), ), isBalanceHidden = false, - addFundsEnabled = true, addToWalletBlockState = null, - accountDeactivatedNotificationConfig = 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, - ).takeIf { isTangemPayDeactivated }, + errorNotificationConfig = null, + accountDeactivatedNotificationConfig = null, ) } - private fun getTopBarMenuItems(isTangemPayDeactivated: Boolean): ImmutableList { - if (isTangemPayDeactivated) return persistentListOf() + fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM { + 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 { return persistentListOf( TangemDropdownMenuItem( title = resourceReference(R.string.tangem_pay_terms_limits), @@ -84,9 +159,7 @@ internal class TangemPayDetailsStateFactory( ) } - private fun getTopBarMenuItemsV2(isTangemPayDeactivated: Boolean): ImmutableList { - if (isTangemPayDeactivated) return persistentListOf() - + private fun getTopBarMenuItemsV2(): ImmutableList { return persistentListOf( TangemPayDropDownItemUM( title = resourceReference(R.string.tangem_pay_terms_limits), @@ -111,7 +184,7 @@ internal class TangemPayDetailsStateFactory( ) } - private fun getActionButtonsConfig(): ImmutableList { + private fun getActionButtonsConfig(isEnabled: Boolean): ImmutableList { return persistentListOf( ActionButtonConfig( text = resourceReference(id = R.string.tangempay_card_details_add_funds), @@ -121,13 +194,13 @@ internal class TangemPayDetailsStateFactory( R.drawable.ic_plus_24 }, onClick = intents::onClickAddFunds, - isEnabled = cardFrozenState == TangemPayCardFrozenState.Unfrozen, + isEnabled = isEnabled, ), ActionButtonConfig( text = resourceReference(id = R.string.tangempay_card_details_withdraw), iconResId = R.drawable.ic_arrow_up_24, onClick = intents::onClickWithdraw, - isEnabled = cardFrozenState == TangemPayCardFrozenState.Unfrozen, + isEnabled = isEnabled, ), ) } 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 3257160e50..61ca2044bf 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 @@ -16,7 +16,7 @@ internal data class TangemPayDetailsUM( val balanceBlockState: TangemPayDetailsBalanceBlockState, val addToWalletBlockState: AddToWalletBlockState?, val isBalanceHidden: Boolean, - val addFundsEnabled: Boolean, + val errorNotificationConfig: NotificationConfig?, val accountDeactivatedNotificationConfig: NotificationConfig?, ) @@ -93,6 +93,7 @@ internal sealed class TangemPayDetailsBalanceBlockState { val onClick: () -> Unit, val isReissuing: Boolean, val isFrozen: Boolean, + val isEnabled: Boolean, ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 018bd2a698..2712ea3c99 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -15,27 +15,28 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener 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.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.WalletMetaInfo -import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.PaymentAccountStatusValue 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.PaymentAccountStatusSupplier import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository +import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tangempay.components.AddFundsListener 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.TangemPayDetailsNavigation import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory @@ -73,14 +74,11 @@ internal class TangemPayDetailsModel @Inject constructor( private val expressTransactionsEventListener: ExpressTransactionsEventListener, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + private val produceTangemPayInitialDataUseCase: ProduceTangemPayInitialDataUseCase, ) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener { 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 userWalletId @@ -93,25 +91,20 @@ internal class TangemPayDetailsModel @Inject constructor( onBack = router::pop, onOpenMenu = ::onOpenMenu, intents = this, - cardFrozenState = when { - initialCard == null -> TangemPayCardFrozenState.Unfrozen - else -> initialCard.frozenState - }, isRedesignEnabled = isRedesignEnabled(), ) val uiState: StateFlow field = MutableStateFlow( - stateFactory.getInitialState( - isTangemPayDeactivated = isTangemPayDeactivated, - cardNumberEnd = initialCard?.lastDigits.orEmpty(), - isReissuing = initialCard == null || initialCard.state != TangemPayCardState.Active, - isFrozen = initialCard?.frozenState == TangemPayCardFrozenState.Frozen, - ), + when { + params.initialStatus.isDeactivated -> stateFactory.getDeactivatedState() + else -> stateFactory.getLoadingState() + }, ) private val refreshStateJobHolder = JobHolder() private val addToWalletBannerJobHolder = JobHolder() + private val frozenStateJobHolder = JobHolder() val bottomSheetNavigation: SlotNavigation = SlotNavigation() @@ -119,38 +112,27 @@ internal class TangemPayDetailsModel @Inject constructor( analytics.send(TangemPayAnalyticsEvents.MainScreenOpened()) handleBalanceHiding() - val statusFlow = paymentAccountStatusSupplier.invoke(userWalletId) + paymentAccountStatusSupplier.invoke(userWalletId) .onEach { status -> currentStatus.update { status } } .map { it.value } - - if (isTangemPayDeactivated) { - statusFlow - .filterIsInstance() - .onEach { state -> - uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance)) - } - .launchIn(modelScope) - } else { - if (initialCard != null) { - subscribeToCardFrozenState(initialCard.id) - } - fetchAddToWalletBanner() - statusFlow - .filterIsInstance() - .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() }, - ), - ) + .onEach { state -> + when (state) { + is PaymentAccountStatusValue.Deactivated -> { + uiState.update { stateFactory.getDeactivatedState() } + uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance)) } + 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() { @@ -168,10 +150,12 @@ internal class TangemPayDetailsModel @Inject constructor( fun isRedesignEnabled(): Boolean = tangemPayFeatureToggles.isRedesignEnabled private fun subscribeToCardFrozenState(cardId: String) { + frozenStateJobHolder.cancel() cardDetailsRepository .cardFrozenState(cardId) .onEach { uiState.update(TangemPayFreezeUnfreezeStateTransformer(cardFrozenState = it)) } .launchIn(modelScope) + .saveIn(frozenStateJobHolder) } 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) { uiMessageSender.send(message = TangemPayMessagesFactory.createErrorMessage(errorType = type)) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt deleted file mode 100644 index 2058e56ca1..0000000000 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt +++ /dev/null @@ -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 { - - 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) - } -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayRenewSessionTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayRenewSessionTransformer.kt new file mode 100644 index 0000000000..f7efa293a6 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayRenewSessionTransformer.kt @@ -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 { + + 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), + ), + ) + } +} \ No newline at end of file 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 adb9a8bc5e..a49382d86b 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 @@ -19,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -61,6 +62,8 @@ import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf +private const val DISABLED_ALPHA = 0.5f + @Suppress("LongMethod") @Composable internal fun TangemPayDetailsScreen( @@ -100,6 +103,20 @@ internal fun TangemPayDetailsScreen( .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( key = TangemPayDetailsBalanceBlockState::class.java, content = { @@ -300,11 +317,16 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi Box( modifier = modifier .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = card.onClick) + .clickable( + onClick = card.onClick, + enabled = card.isEnabled, + ) .testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON), ) { Image( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .alpha(if (card.isEnabled) 1f else DISABLED_ALPHA) + .fillMaxSize(), painter = painterResource( if (card.isReissuing) { R.drawable.img_visa_card_inactive_48_32 @@ -464,12 +486,14 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider { + if (state.errorNotificationConfig != null) { + item("errorSessionBannerBlock") { + TangemMessage( + config = state.errorNotificationConfig, + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), + ) + } + } if (state.addToWalletBlockState != null) { item("addToWalletBannerBlock") { TangemPayAddToWalletBlock( @@ -340,6 +348,7 @@ private fun CardsBlock( isReissuing = item.isReissuing, lastDigits = item.lastDigits, onClick = item.onClick, + isEnabled = item.isEnabled, isFrozen = item.isFrozen, ) SpacerW(TangemTheme.dimens2.x2) 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 8c8d9b362a..606aacb7e2 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 @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset @@ -41,6 +42,7 @@ private const val REISSUING_CARD_BG = 0xFF1E1E1E @Composable internal fun TangemPayCardView( isReissuing: Boolean, + isEnabled: Boolean, lastDigits: String, onClick: () -> Unit, isFrozen: Boolean, @@ -54,6 +56,7 @@ internal fun TangemPayCardView( ) .testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON), isReissuing = isReissuing, + isEnabled = isEnabled, onClick = onClick, ) { Row( @@ -120,6 +123,7 @@ internal fun TangemPayAddCardView(onClick: () -> Unit, modifier: Modifier = Modi @Composable private fun CardBackground( isReissuing: Boolean, + isEnabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, @@ -134,6 +138,7 @@ private fun CardBackground( Box( modifier = modifier + .alpha(if (isEnabled) 1f else 0.5f) .clip(RoundedCornerShape(6.dp)) .drawBehind { drawRect(bgColor) @@ -163,7 +168,7 @@ private fun CardBackground( color = TangemTheme.colors3.border.primary, shape = RoundedCornerShape(6.dp), ) - .clickableSingle(onClick = onClick), + .clickableSingle(onClick = onClick, enabled = isEnabled), content = content, ) } @@ -181,6 +186,7 @@ private fun CardBackgroundPreview() { isReissuing = false, onClick = {}, content = {}, + isEnabled = true, ) SpacerH(TangemTheme.dimens2.x4) CardBackground( @@ -191,15 +197,28 @@ private fun CardBackgroundPreview() { isReissuing = true, onClick = {}, content = {}, + isEnabled = true, ) 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) - TangemPayCardView(isReissuing = true, onClick = {}, lastDigits = "", isFrozen = false) + TangemPayCardView( + isReissuing = true, + onClick = {}, + lastDigits = "", + isEnabled = true, + isFrozen = false, + ) SpacerH(TangemTheme.dimens2.x4) TangemPayAddCardView(onClick = {}) SpacerH(TangemTheme.dimens2.x4) - TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234", isFrozen = true) + TangemPayCardView(isReissuing = 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/TangemPayDetailIntents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt index 731154d2a1..c4bd523fcf 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi internal interface TangemPayDetailIntents { fun onContactSupportClicked() fun onRefreshSwipe(refreshState: ShowRefreshState) + fun onRenewSession() fun onClickAddFunds() fun onClickWithdraw() fun onClickTermsAndLimits()