Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-21 17:59:40 +05:00
commit a22e77ebe8
669 changed files with 27695 additions and 7905 deletions

View file

@ -61,6 +61,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty()
is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount(
fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(),
)
// Transient statuses are not persisted
is PaymentAccountStatusValue.Loading,
@ -72,6 +73,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
}
fun convertBack(userWalletId: UserWalletId, value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
return when (value) {
is PaymentAccountStatusValueDM.Empty -> PaymentAccountStatusValue.Empty
is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated
@ -89,7 +91,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
availableForWithdrawal = value.availableForWithdrawal,
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
cryptoCurrency = cryptoCurrency,
cards = value.cards.map { card ->
TangemPayCard(
id = card.id,
@ -117,6 +119,8 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
is PaymentAccountStatusValueDM.DeactivatedAccount -> PaymentAccountStatusValue.Deactivated(
source = StatusSource.CACHE,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
cryptoCurrency = cryptoCurrency,
)
null -> PaymentAccountStatusValue.Error.Unavailable
}

View file

@ -33,6 +33,7 @@ import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.domain.pay.usecase.*
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase

View file

@ -263,6 +263,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
val isDeactivated = productInstance?.status == CustomerInfo.ProductInstance.Status.DEACTIVATED
val isFormer = state == CustomerInfo.State.FORMER
val fiatBalance = fiatBalance
val cryptoBalance = cryptoBalance
return when {
kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty() -> {
@ -272,10 +273,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
)
}
fiatBalance != null && (isDeactivated || isFormer) -> {
fiatBalance != null && cryptoBalance != null && (isDeactivated || isFormer) -> {
PaymentAccountStatusValue.Deactivated(
source = StatusSource.ACTUAL,
fiatBalance = fiatBalance,
cryptoBalance = cryptoBalance,
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
)
}
cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState(

View file

@ -2,38 +2,32 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.flatMap
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.data.pay.util.CustomerInfoConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
import com.tangem.datasource.api.pay.models.request.OrderRequest
import com.tangem.datasource.api.pay.models.request.SetTangemPayEnabledRequest
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.datasource.api.pay.models.response.FiatBalance
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.withContext
@ -100,10 +94,10 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) }
.flatMap { response ->
val result = response.result
val status = result?.productInstance?.status
val result = response.result ?: return@flatMap VisaApiError.UnknownWithoutCode.left()
val status = result.productInstance?.status
val isDeactivated = status == CustomerMeResponse.ProductInstance.Status.DEACTIVATED
val isFormer = result?.state?.let { CustomerInfo.State.fromString(it) } == CustomerInfo.State.FORMER
val isFormer = result.state.let { CustomerInfo.State.fromString(it) } == CustomerInfo.State.FORMER
if (isDeactivated || isFormer) {
tangemPayStorage.storeIsTangemPayDeactivated(userWalletId)
}
@ -167,72 +161,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
@Suppress("ComplexCondition")
private suspend fun getCustomerInfo(
userWalletId: UserWalletId,
response: CustomerMeResponse.Result?,
response: CustomerMeResponse.Result,
): CustomerInfo {
val kycStatus = KycStatus.fromString(status = response?.kyc?.status)
sendKycAnalytics(kycStatus)
val customerInfo = CustomerInfoConverter.convert(response)
sendKycAnalytics(customerInfo.kycStatus)
val card = response?.card
val fiatBalance = response?.balance?.fiat
val cryptoBalance = response?.balance?.crypto
val availableForWithdrawal = response?.balance?.availableForWithdrawal
val paymentAccount = response?.paymentAccount
val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) {
CardInfo(
lastFourDigits = card.cardNumberEnd,
balance = fiatBalance.availableBalance,
currencyCode = fiatBalance.currency,
depositAddress = response.depositAddress,
isPinSet = response.card?.isPinSet == true,
fiatBalance = fiatBalance.toDomain(),
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
id = cryptoBalance.id,
chainId = cryptoBalance.chainId.toLong(),
depositAddress = cryptoBalance.depositAddress.orEmpty(),
tokenContractAddress = cryptoBalance.tokenContractAddress,
balance = cryptoBalance.balance,
),
availableForWithdrawal = availableForWithdrawal?.amount.orZero(),
)
} else {
null
customerInfo.productInstance?.let { instance ->
cardFrozenStateStore.store(key = instance.cardId, value = instance.frozenState)
}
val productInstance = response?.productInstance?.let { instance ->
val cardFrozenState = when (instance.status) {
CustomerMeResponse.ProductInstance.Status.ACTIVE -> TangemPayCardFrozenState.Unfrozen
else -> TangemPayCardFrozenState.Frozen
}
cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState)
val displayName = instance.displayName?.ifEmpty { null }
ProductInstance(
id = instance.id,
cardId = instance.cardId,
frozenState = cardFrozenState,
status = instance.status.toDomain(),
displayName = if (displayName != null) CardDisplayName(displayName).getOrElse { null } else null,
actualCardLimit = instance.actualCardLimit?.parseCardLimit(),
adminCardLimit = instance.adminCardLimit?.parseCardLimit(),
)
}
return CustomerInfo(
customerId = response?.id,
productInstance = productInstance,
kycStatus = kycStatus,
cardInfo = cardInfo,
state = response?.state?.let { CustomerInfo.State.fromString(it) } ?: CustomerInfo.State.UNDEFINED,
fiatBalance = fiatBalance?.toDomain(),
).also {
lastFetchedCustomerInfoMap[userWalletId] = it
}
}
private fun CustomerMeResponse.CardLimit.parseCardLimit(): TangemPayCardLimit {
return TangemPayCardLimit(
amount = amount,
period = TangemPayCardLimitPeriod.fromString(periodType),
)
return customerInfo.also { lastFetchedCustomerInfoMap[userWalletId] = it }
}
private fun sendKycAnalytics(kycStatus: KycStatus) {
@ -311,24 +249,4 @@ internal class DefaultOnboardingRepository @Inject constructor(
setHideMainOnboardingBanner(userWalletId)
}
}
}
private fun FiatBalance.toDomain() = PaymentAccountStatusValue.FiatBalance(
availableBalance = availableBalance,
currency = currency,
)
private fun CustomerMeResponse.ProductInstance.Status.toDomain() = when (this) {
CustomerMeResponse.ProductInstance.Status.NEW -> ProductInstance.Status.NEW
CustomerMeResponse.ProductInstance.Status.READY_FOR_MANUFACTURING -> ProductInstance.Status.READY_FOR_MANUFACTURING
CustomerMeResponse.ProductInstance.Status.MANUFACTURING -> ProductInstance.Status.MANUFACTURING
CustomerMeResponse.ProductInstance.Status.SENT_TO_DELIVERY -> ProductInstance.Status.SENT_TO_DELIVERY
CustomerMeResponse.ProductInstance.Status.DELIVERED -> ProductInstance.Status.DELIVERED
CustomerMeResponse.ProductInstance.Status.ACTIVATING -> ProductInstance.Status.ACTIVATING
CustomerMeResponse.ProductInstance.Status.ACTIVE -> ProductInstance.Status.ACTIVE
CustomerMeResponse.ProductInstance.Status.BLOCKED -> ProductInstance.Status.BLOCKED
CustomerMeResponse.ProductInstance.Status.DEACTIVATING -> ProductInstance.Status.DEACTIVATING
CustomerMeResponse.ProductInstance.Status.DEACTIVATED -> ProductInstance.Status.DEACTIVATED
CustomerMeResponse.ProductInstance.Status.CANCELED -> ProductInstance.Status.CANCELED
CustomerMeResponse.ProductInstance.Status.UNKNOWN -> ProductInstance.Status.UNKNOWN
}

View file

@ -11,6 +11,7 @@ import com.tangem.datasource.api.pay.models.response.WithdrawResponse
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.domain.pay.WithdrawalResult
@ -222,13 +223,13 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
}
}
override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean {
val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWallet.walletId)
override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean {
val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWalletId)
if (orderId.isNullOrEmpty()) return false
val orderData = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val orderData = orderRepository.getOrderData(userWalletId = userWalletId, orderId = orderId).getOrNull()
val isActive = orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING
if (!isActive) {
tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWallet.walletId)
tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWalletId)
}
return isActive
}

View file

@ -0,0 +1,103 @@
package com.tangem.data.pay.util
import arrow.core.getOrElse
import com.tangem.datasource.api.pay.models.response.CryptoBalance
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.datasource.api.pay.models.response.FiatBalance
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.converter.Converter
internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, CustomerInfo> {
@Suppress("ComplexCondition")
override fun convert(value: CustomerMeResponse.Result): CustomerInfo {
val kycStatus = KycStatus.fromString(status = value.kyc?.status)
val card = value.card
val fiatBalance = value.balance?.fiat
val cryptoBalance = value.balance?.crypto
val paymentAccount = value.paymentAccount
val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) {
CardInfo(
lastFourDigits = card.cardNumberEnd,
balance = fiatBalance.availableBalance,
currencyCode = fiatBalance.currency,
depositAddress = value.depositAddress,
isPinSet = value.card?.isPinSet == true,
fiatBalance = fiatBalance.toDomain(),
cryptoBalance = cryptoBalance.toDomain(),
)
} else {
null
}
val productInstance = value.productInstance?.let { instance ->
val status = instance.status.toDomain()
val cardFrozenState = when (status) {
Status.ACTIVE -> TangemPayCardFrozenState.Unfrozen
else -> TangemPayCardFrozenState.Frozen
}
val displayName = instance.displayName?.ifEmpty { null }
ProductInstance(
id = instance.id,
cardId = instance.cardId,
frozenState = cardFrozenState,
status = status,
displayName = if (displayName != null) CardDisplayName(displayName).getOrElse { null } else null,
actualCardLimit = instance.actualCardLimit?.parseCardLimit(),
adminCardLimit = instance.adminCardLimit?.parseCardLimit(),
)
}
return CustomerInfo(
customerId = value.id,
productInstance = productInstance,
kycStatus = kycStatus,
cardInfo = cardInfo,
state = CustomerInfo.State.fromString(value.state),
fiatBalance = fiatBalance?.toDomain(),
cryptoBalance = cryptoBalance?.toDomain(),
)
}
private fun CustomerMeResponse.CardLimit.parseCardLimit(): TangemPayCardLimit {
return TangemPayCardLimit(
amount = amount,
period = TangemPayCardLimitPeriod.fromString(periodType),
)
}
private fun FiatBalance.toDomain() = PaymentAccountStatusValue.FiatBalance(
availableBalance = availableBalance,
currency = currency,
)
private fun CryptoBalance.toDomain() = PaymentAccountStatusValue.CryptoBalance(
id = id,
chainId = chainId.toLong(),
depositAddress = depositAddress.orEmpty(),
tokenContractAddress = tokenContractAddress,
balance = balance,
)
private fun CustomerMeResponse.ProductInstance.Status.toDomain(): Status = when (this) {
CustomerMeResponse.ProductInstance.Status.NEW -> Status.NEW
CustomerMeResponse.ProductInstance.Status.READY_FOR_MANUFACTURING -> Status.READY_FOR_MANUFACTURING
CustomerMeResponse.ProductInstance.Status.MANUFACTURING -> Status.MANUFACTURING
CustomerMeResponse.ProductInstance.Status.SENT_TO_DELIVERY -> Status.SENT_TO_DELIVERY
CustomerMeResponse.ProductInstance.Status.DELIVERED -> Status.DELIVERED
CustomerMeResponse.ProductInstance.Status.ACTIVATING -> Status.ACTIVATING
CustomerMeResponse.ProductInstance.Status.ACTIVE -> Status.ACTIVE
CustomerMeResponse.ProductInstance.Status.BLOCKED -> Status.BLOCKED
CustomerMeResponse.ProductInstance.Status.DEACTIVATING -> Status.DEACTIVATING
CustomerMeResponse.ProductInstance.Status.DEACTIVATED -> Status.DEACTIVATED
CustomerMeResponse.ProductInstance.Status.CANCELED -> Status.CANCELED
CustomerMeResponse.ProductInstance.Status.UNKNOWN -> Status.UNKNOWN
}
}

View file

@ -5,7 +5,9 @@ import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
@ -17,9 +19,30 @@ internal class PaymentAccountStatusValueDMConverterTest {
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory = mockk()
private val userWalletId = UserWalletId("1234567890ABCDEF")
private val cryptoCurrency: CryptoCurrency.Token = mockk()
init {
every { tangemPayCurrencyFactory.create(userWalletId) } returns cryptoCurrency
}
private val converter = PaymentAccountStatusValueDMConverter(tangemPayCurrencyFactory)
private fun cryptoBalance() = PaymentAccountStatusValue.CryptoBalance(
id = "usd-coin",
chainId = 137,
depositAddress = "0xDEPOSIT",
tokenContractAddress = "0xCONTRACT",
balance = BigDecimal("10"),
)
private fun cryptoBalanceDM() = PaymentAccountStatusValueDM.CryptoBalanceDM(
id = "usd-coin",
chainId = 137,
depositAddress = "0xDEPOSIT",
tokenContractAddress = "0xCONTRACT",
balance = BigDecimal("10"),
)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Convert {
@ -44,7 +67,9 @@ internal class PaymentAccountStatusValueDMConverterTest {
fiatBalance = PaymentAccountStatusValue.FiatBalance(
availableBalance = BigDecimal("100"),
currency = "USD",
)
),
cryptoBalance = cryptoBalance(),
cryptoCurrency = cryptoCurrency,
)
// WHEN
@ -117,7 +142,8 @@ internal class PaymentAccountStatusValueDMConverterTest {
fiatBalance = PaymentAccountStatusValueDM.FiatBalanceDM(
availableBalance = BigDecimal("200"),
currency = "EUR",
)
),
cryptoBalance = cryptoBalanceDM(),
)
// WHEN