Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-29 20:50:41 +05:00
parent f086d6bb74
commit 40fe8bb5cb
56 changed files with 82 additions and 1491 deletions

View file

@ -1,7 +1,6 @@
package com.tangem.domain.tokens.wallet
import arrow.core.Either
import arrow.core.right
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -147,18 +146,10 @@ class WalletBalanceFetcher internal constructor(
fetchExpressAssets(userWallet = userWallet, currencies = currencies)
fetcher.fetch(
userWalletId = userWalletId,
currencies = currencies,
paymentAccountRefactorEnabled = params.isPaymentAccountRefactorEnabled,
)
fetcher.fetch(userWalletId = userWalletId, currencies = currencies)
}
private suspend fun BaseWalletBalanceFetcher.fetch(
userWalletId: UserWalletId,
currencies: Set<CryptoCurrency>,
paymentAccountRefactorEnabled: Boolean,
) {
private suspend fun BaseWalletBalanceFetcher.fetch(userWalletId: UserWalletId, currencies: Set<CryptoCurrency>) {
coroutineScope {
// Fetch balance sources in parallel
val balanceErrors = fetchingSources.filterIsInstance<WalletFetchingSource.Balance>()
@ -182,7 +173,7 @@ class WalletBalanceFetcher internal constructor(
// Fetch TangemPay separately — may run long-polling, so it must not block balance error checking
if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) {
fetchPaymentAccount(userWalletId, paymentAccountRefactorEnabled)
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
}
}
}
@ -197,19 +188,10 @@ class WalletBalanceFetcher internal constructor(
expressServiceFetcher.fetch(userWallet = userWallet, assetIds = assetIds)
}
private suspend fun fetchPaymentAccount(
userWalletId: UserWalletId,
paymentAccountRefactorEnabled: Boolean,
): Either<Throwable, Unit> {
if (!paymentAccountRefactorEnabled) return Unit.right()
return paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
}
/**
* Params of [WalletBalanceFetcher]
*
* @property userWalletId user wallet id
*/
data class Params(val userWalletId: UserWalletId, val isPaymentAccountRefactorEnabled: Boolean)
data class Params(val userWalletId: UserWalletId)
}

View file

@ -96,7 +96,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -133,7 +132,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -171,7 +169,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -209,7 +206,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -260,7 +256,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -315,7 +310,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -376,7 +370,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -428,7 +421,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -479,7 +471,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -536,7 +527,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -607,7 +597,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -682,7 +671,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -743,7 +731,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -802,7 +789,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)
@ -869,7 +855,6 @@ internal class WalletBalanceFetcherTest {
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false,
),
)

View file

@ -1,22 +1,8 @@
package com.tangem.domain.pay.model
import java.util.Locale
enum class OrderStatus {
UNKNOWN, // TODO remove it after TangemPay accounts refactor TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED
NEW,
PROCESSING,
COMPLETED,
CANCELED,
;
companion object {
fun fromString(value: String) = when (value.uppercase(Locale.US)) {
"NEW" -> NEW
"PROCESSING" -> PROCESSING
"COMPLETED" -> COMPLETED
"CANCELED" -> CANCELED
else -> UNKNOWN
}
}
}

View file

@ -1,179 +0,0 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayEligibilityManager
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 com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.*
class TangemPayMainScreenCustomerInfoUseCase(
private val onboardingRepository: OnboardingRepository,
private val customerOrderRepository: CustomerOrderRepository,
private val eligibilityManager: TangemPayEligibilityManager,
private val deviceSecurity: DeviceSecurityInfoProvider,
) {
val state: StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>>>
field = MutableStateFlow(value = mapOf())
private val logger = TangemLogger.withTag("TangemPayMainScreenCustomerInfoUseCase")
suspend fun fetch(userWalletId: UserWalletId) {
logger.i("fetch: ${userWalletId.stringValue}")
if (onboardingRepository.isTangemPayDeactivated(userWalletId)) {
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
return
}
if (deviceSecurity.isSecurityExposed()) {
logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}")
logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}")
logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left())
return // fast exit
}
onboardingRepository.hasTangemPayInWallet(userWalletId)
.fold(
ifLeft = { error ->
logger.e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
if (error is VisaApiError.NotPaeraCustomer) {
showOnboardingBannerIfEligible(userWalletId)
} else {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
}
},
ifRight = { hasTangemPay ->
logger.i("checkCustomerWallet for $userWalletId: $hasTangemPay")
if (hasTangemPay) {
val oldResult = state.value[userWalletId]
if (oldResult == null) {
updateState(userWalletId, MainCustomerInfoContentState.Loading.right())
}
val result = proceedWithPaeraCustomerResult(userWalletId)
if (result.leftOrNull() is TangemPayCustomerInfoError.DeactivatedError) {
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
return
}
updateState(userWalletId, result.map(MainCustomerInfoContentState::Content))
} else {
// if there's no tangem pay, check eligibility and show onboarding banner
showOnboardingBannerIfEligible(userWalletId)
}
},
)
}
private suspend fun showOnboardingBannerIfEligible(userWalletId: UserWalletId) {
val tangemPayEntryPoint = TangemPayEntryPoint.BANNER
if (eligibilityManager.isPaeraCustomerForAnyWallet(tangemPayEntryPoint)) {
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
return
}
val isEligible = eligibilityManager
.getEligibleWallets(
shouldExcludePaeraCustomers = false,
entryPoint = tangemPayEntryPoint,
)
.any { it.walletId == userWalletId }
if (isEligible) {
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
} else {
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
}
} else {
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
}
}
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,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) {
return TangemPayCustomerInfoError.RefreshNeededError.left()
}
val orderId = onboardingRepository.getOrderId(userWalletId)
return if (orderId != null) {
proceedWithOrderId(userWalletId = userWalletId, orderId = orderId)
} else {
proceedWithoutOrder(userWalletId = userWalletId)
}
}
private suspend fun proceedWithoutOrder(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return onboardingRepository.getCustomerInfo(userWalletId)
.mapLeft { error ->
logger.e("mapErrorForCustomer: $error")
error.mapErrorForCustomer()
}
.map { customerInfo ->
logger.i("customerInfo")
if (customerInfo.productInstance == null) {
onboardingRepository.createOrder(userWalletId)
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW)
} else {
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.COMPLETED)
}
}
}
private suspend fun proceedWithOrderId(
userWalletId: UserWalletId,
orderId: String,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return customerOrderRepository.getOrderData(userWalletId, orderId = orderId)
.fold(
ifLeft = { error ->
error.mapErrorForCustomer().left()
},
ifRight = { orderData ->
if (orderData.status in setOf(OrderStatus.COMPLETED, OrderStatus.UNKNOWN)) {
onboardingRepository.clearOrderId(userWalletId)
}
onboardingRepository.getCustomerInfo(userWalletId = userWalletId)
.mapLeft { it.mapErrorForCustomer() }
.map { customerInfo ->
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status)
}
},
)
}
private fun VisaApiError.mapErrorForCustomer(): TangemPayCustomerInfoError {
return when (this) {
is VisaApiError.RefreshTokenExpired -> TangemPayCustomerInfoError.RefreshNeededError
is VisaApiError.NotPaeraCustomer -> TangemPayCustomerInfoError.UnknownError
is VisaApiError.Deactivated -> TangemPayCustomerInfoError.DeactivatedError
else -> TangemPayCustomerInfoError.UnavailableError
}
}
}