Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-08 21:28:30 +05:00
parent b9a2ab5fed
commit 5c93d3973a
11 changed files with 139 additions and 86 deletions

View file

@ -3,6 +3,11 @@ package com.tangem.domain.pay.model
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import java.math.BigDecimal
sealed class MainCustomerInfoContentState {
object Loading : MainCustomerInfoContentState()
data class Content(val info: MainScreenCustomerInfo) : MainCustomerInfoContentState()
}
data class MainScreenCustomerInfo(
val info: CustomerInfo,
val orderStatus: OrderStatus,

View file

@ -2,16 +2,13 @@ package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.right
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.model.*
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
import kotlinx.coroutines.flow.*
import timber.log.Timber
private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
@ -26,32 +23,50 @@ class TangemPayMainScreenCustomerInfoUseCase(
private val tangemPayOnboardingRepository: OnboardingRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> = catch(
block = {
Timber.tag(TAG).d("checkCustomerWallet")
repository.checkCustomerWallet(userWalletId)
.fold(
ifLeft = { error ->
Timber.tag(TAG).e("Failed to check customer wallet ${error.javaClass.simpleName}")
TangemPayCustomerInfoError.UnknownError.left()
},
ifRight = { hasTangemPay ->
Timber.tag(TAG).i("checkCustomerWallet $hasTangemPay")
if (hasTangemPay) {
proceedWithPaeraCustomerResult(userWalletId)
} else {
TangemPayCustomerInfoError.UnknownError.left() // ignore if there's no TangemPay
val state: StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>>>
field = MutableStateFlow(value = mapOf())
suspend fun fetch(userWalletId: UserWalletId) {
Timber.tag(TAG).i("fetch: $userWalletId")
repository.checkCustomerWallet(userWalletId)
.fold(
ifLeft = { error ->
Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
},
ifRight = { hasTangemPay ->
Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay")
if (hasTangemPay) {
val oldResult = state.value[userWalletId]
if (oldResult == null) {
updateState(userWalletId, MainCustomerInfoContentState.Loading.right())
}
},
)
},
catch = { error ->
Timber.tag(TAG).e(error)
TangemPayCustomerInfoError.UnknownError.left()
},
)
val result = proceedWithPaeraCustomerResult(userWalletId)
.map(MainCustomerInfoContentState::Content)
updateState(userWalletId, result)
} else {
// ignore if there's no TangemPay
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
}
},
)
}
operator fun invoke(
userWalletId: UserWalletId,
): Flow<Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>> {
return state.mapNotNull { map -> map[userWalletId] }
}
private fun updateState(
userWalletId: UserWalletId,
either: Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>,
) {
state.update { currentMap ->
currentMap.toMutableMap().apply { this[userWalletId] = either }
}
}
private suspend fun proceedWithPaeraCustomerResult(
userWalletId: UserWalletId,
@ -70,14 +85,13 @@ class TangemPayMainScreenCustomerInfoUseCase(
private suspend fun proceedWithoutOrder(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
Timber.tag(TAG).d("proceedWithoutOrder")
return repository.getCustomerInfo(userWalletId)
.mapLeft { error ->
Timber.tag(TAG).e("mapErrorForCustomer: $error")
error.mapErrorForCustomer()
}
.map { customerInfo ->
Timber.tag(TAG).d("customerInfo")
Timber.tag(TAG).i("customerInfo")
if (customerInfo.cardInfo == null && customerInfo.isKycApproved) {
// If order id wasn't saved -> start order creation and get customer info
repository.createOrder(userWalletId)

View file

@ -1,29 +0,0 @@
package com.tangem.feature.wallet.child.wallet.model
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class TangemPayMainInfoManager @Inject constructor(
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
) {
val mainScreenCustomerInfo:
StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainScreenCustomerInfo>>>
field = MutableStateFlow(mapOf())
suspend fun refreshTangemPayInfo(userWalletId: UserWalletId) {
mainScreenCustomerInfo.update { currentMap ->
val info = tangemPayMainScreenCustomerInfoUseCase(userWalletId)
currentMap.toMutableMap().apply { this[userWalletId] = info }
}
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.usecase.*
@ -94,7 +95,7 @@ internal class WalletModel @Inject constructor(
private val tangemPayOnboardingRepository: OnboardingRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayMainInfoManager: TangemPayMainInfoManager,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
) : Model() {
@ -371,15 +372,15 @@ internal class WalletModel @Inject constructor(
if (isShouldLaunchPeriodicUpdate) {
updateTangemPayJobHolder.cancel()
modelScope.launch {
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL)
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
}.saveIn(updateTangemPayJobHolder)
} else {
// Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
}.launchIn(modelScope)
}

View file

@ -13,8 +13,8 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
@ -43,7 +43,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val tangemPayInfoManager: TangemPayMainInfoManager,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val uiMessageSender: UiMessageSender,
) : BaseWalletClickIntents(), TangemPayIntents {
@ -54,13 +54,13 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
) {
return
}
tangemPayInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
override fun onRefreshPayToken(userWalletId: UserWalletId) {
modelScope.launch {
produceInitialDataTangemPay.invoke(userWalletId)
tangemPayInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
}

View file

@ -9,6 +9,8 @@ internal sealed class TangemPayState {
object Empty : TangemPayState()
data object Loading : TangemPayState()
data class Progress(
val title: TextReference,
val description: TextReference,

View file

@ -0,0 +1,15 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(tangemPayState = TangemPayState.Loading)
} else {
prevState
}
}
}

View file

@ -2,26 +2,23 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.MainCustomerInfoContentState
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletTangemPayAnalyticsEventSender
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHiddenStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayUnavailableStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayUpdateInfoStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@ -32,7 +29,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
private val clickIntents: WalletClickIntents,
private val innerWalletRouter: InnerWalletRouter,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val tangemPayMainInfoManager: TangemPayMainInfoManager,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val analytics: WalletTangemPayAnalyticsEventSender,
) : WalletSubscriber() {
@ -41,12 +38,10 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
}
private fun subscribeOnTangemPayInfoUpdates(): Flow<*> {
return tangemPayMainInfoManager.mainScreenCustomerInfo
.filter { it.isNotEmpty() }
return tangemPayMainScreenCustomerInfoUseCase(userWalletId = userWallet.walletId)
.distinctUntilChanged()
.onEach { data ->
.onEach { mainInfoData ->
val userWalletId = userWallet.walletId
val mainInfoData = data[userWalletId] ?: return@onEach
mainInfoData.onLeft { tangemPayError ->
when (tangemPayError) {
TangemPayCustomerInfoError.RefreshNeededError -> {
@ -70,13 +65,23 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
)
}
}
}.onRight { customerInfo ->
updateTangemPay(customerInfo, userWalletId)
analytics.send(customerInfo = customerInfo)
}
}.onRight { contentState -> handleContentState(state = contentState) }
}
}
private suspend fun handleContentState(state: MainCustomerInfoContentState) {
val userWalletId = userWallet.walletId
when (state) {
MainCustomerInfoContentState.Loading -> stateController.update(
transformer = TangemPayLoadingStateTransformer(userWalletId),
)
is MainCustomerInfoContentState.Content -> {
updateTangemPay(data = state.info, userWalletId = userWalletId)
analytics.send(customerInfo = state.info)
}
}
}
private suspend fun updateTangemPay(data: MainScreenCustomerInfo, userWalletId: UserWalletId) {
val cardFrozenState =
data.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) }

View file

@ -224,9 +224,9 @@ private fun WalletContent(
contentType = selectedWallet.tangemPayState::class.java,
) {
TangemPayMainScreenBlock(
selectedWallet.tangemPayState,
state = selectedWallet.tangemPayState,
isBalanceHidden = state.isHidingMode,
itemModifier,
modifier = itemModifier,
)
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun TangemPayLoadingScreenBlock(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircleShimmer(modifier = Modifier.size(36.dp))
Column(
modifier = Modifier.padding(start = 12.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
RectangleShimmer(modifier = Modifier.padding(vertical = 4.dp).sizeIn(minWidth = 70.dp, minHeight = 12.dp))
RectangleShimmer(modifier = Modifier.padding(vertical = 2.dp).sizeIn(minWidth = 52.dp, minHeight = 12.dp))
}
SpacerWMax()
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
RectangleShimmer(modifier = Modifier.padding(vertical = 4.dp).sizeIn(minWidth = 40.dp, minHeight = 12.dp))
RectangleShimmer(modifier = Modifier.padding(vertical = 2.dp).sizeIn(minWidth = 40.dp, minHeight = 12.dp))
}
}
}

View file

@ -23,6 +23,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier)
is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier)
is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier)
TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier)
}
}
@ -32,6 +33,8 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
private fun TangemPayMainScreenBlockPreview() {
TangemThemePreview {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false)
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),