Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-30 10:36:00 +05:00
commit d036b23ba4
1274 changed files with 51752 additions and 15452 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.domain.pay
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.serialization.Serializable
@ -12,4 +13,5 @@ data class TangemPayDetailsConfig(
val cardNumberEnd: String,
val chainId: Int,
val isTangemPayDeactivated: Boolean,
val displayName: CardDisplayName?,
)

View file

@ -5,8 +5,8 @@ import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
@Deprecated("TangemPayCurrencyFactory")
interface TangemPayCryptoCurrencyFactory {
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
fun create(userWallet: UserWallet): Either<UniversalError, CryptoCurrency.Token>
}

View file

@ -1,5 +1,7 @@
package com.tangem.domain.pay.model
import com.tangem.domain.models.pay.TangemPayCardLimit
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.visa.model.TangemPayCardFrozenState
@ -51,6 +53,9 @@ data class CustomerInfo(
val id: String,
val cardId: String,
val frozenState: TangemPayCardFrozenState,
val displayName: CardDisplayName?,
val actualCardLimit: TangemPayCardLimit?,
val adminCardLimit: TangemPayCardLimit?,
val status: Status,
) {
enum class Status {

View file

@ -1,7 +1,6 @@
package com.tangem.domain.pay.model
enum class OrderStatus {
UNKNOWN, // TODO remove it after TangemPay accounts refactor TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED
NEW,
PROCESSING,
COMPLETED,

View file

@ -0,0 +1,6 @@
package com.tangem.domain.pay.model
data class TangemPayReissueOrderInfo(
val orderId: String,
val orderStatus: OrderStatus,
)

View file

@ -2,7 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.visa.error.VisaApiError

View file

@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
@ -31,4 +32,16 @@ interface TangemPayCardDetailsRepository {
fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState>
suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState?
suspend fun updateCardDisplayName(
cardId: String,
userWalletId: UserWalletId,
displayName: CardDisplayName,
): Either<UniversalError, Unit>
suspend fun updateCardLimit(
cardId: String,
userWalletId: UserWalletId,
limit: String,
): Either<UniversalError, Unit>
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.visa.error.VisaApiError
interface TangemPayReissueCardRepository {
suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee>
suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, TangemPayReissueOrderInfo>
suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit>
suspend fun getReissueOrderInfo(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayReissueOrderInfo?>
}

View file

@ -0,0 +1,52 @@
package com.tangem.domain.pay.usecase
import arrow.core.Option
import arrow.core.none
import arrow.core.some
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
class GetPaymentAccountCryptoCurrencyStatusUseCase(
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
) {
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<Pair<Account.Payment, CryptoCurrencyStatus>> {
return paymentAccountStatusSupplier(userWalletId).mapNotNull { accountStatus ->
val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) {
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
else -> return@mapNotNull null
}
if (cryptoCurrencyStatus.currency == cryptoCurrency) {
accountStatus.account to cryptoCurrencyStatus
} else {
null
}
}
}
suspend fun invokeSync(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Option<Pair<Account.Payment, CryptoCurrencyStatus>> {
val accountStatus = paymentAccountStatusSupplier.invoke(userWalletId).firstOrNull() ?: return none()
val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) {
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
else -> return none()
}
return if (cryptoCurrencyStatus.currency == cryptoCurrency) {
(accountStatus.account to cryptoCurrencyStatus).some()
} else {
none()
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import java.math.BigDecimal
class SetTangemPayCardLimitUseCase(
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) {
suspend operator fun invoke(
cardId: String,
userWalletId: UserWalletId,
amount: BigDecimal,
): Either<UniversalError, Unit> {
return cardDetailsRepository.updateCardLimit(cardId, userWalletId, amount.toPlainString())
.onRight {
val params = PaymentAccountStatusFetcher.Params(userWalletId)
paymentAccountStatusFetcher.invoke(params)
}
}
}

View file

@ -1,174 +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)
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
else -> TangemPayCustomerInfoError.UnavailableError
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tangempay
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
sealed class TangemPayAnalyticsEvents(
categoryName: String,
@ -11,7 +12,7 @@ sealed class TangemPayAnalyticsEvents(
class ActivationScreenOpened : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
event = "Visa Activation Screen Opened",
)
), AppsFlyerIncludedEvent
class ViewTermsClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
@ -21,12 +22,12 @@ sealed class TangemPayAnalyticsEvents(
class GetCardClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
event = "Button - Visa Get Card",
)
), AppsFlyerIncludedEvent
class KycFlowOpened : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
event = "Visa KYC Flow Opened",
)
), AppsFlyerIncludedEvent
class IssuingBannerDisplayed : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
@ -41,17 +42,17 @@ sealed class TangemPayAnalyticsEvents(
class ReceiveFundsClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Screen",
event = "Button - Visa Receive",
)
), AppsFlyerIncludedEvent
class AddFundsClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Screen",
event = "Button - Visa Add Funds",
)
), AppsFlyerIncludedEvent
class SwapClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Screen",
event = "Button - Visa Swap",
)
), AppsFlyerIncludedEvent
class ChooseWalletPopup : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
@ -180,7 +181,7 @@ sealed class TangemPayAnalyticsEvents(
class KycPassedAndOrderCreated : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
event = "Visa KYC Passed And Order Created",
)
), AppsFlyerIncludedEvent
class KycRejected : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
@ -192,6 +193,21 @@ sealed class TangemPayAnalyticsEvents(
event = "Visa KYC Canceled",
)
class ReplaceCardClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Screen",
event = "Visa Replace Card Clicked",
)
class ReplaceCardConfirmationPopupOpened : TangemPayAnalyticsEvents(
categoryName = "Visa Screen",
event = "Visa Replace Card Confirmation Popup Opened",
)
class ReplaceCardConfirmed : TangemPayAnalyticsEvents(
categoryName = "Visa Screen",
event = "Visa Replace Card Confirmed",
)
class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding",
event = "Visa Permanent Banner Clicked",