Updated on 2026-08-14
This commit is contained in:
commit
d036b23ba4
1274 changed files with 51752 additions and 15452 deletions
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
data class TangemPayReissueOrderInfo(
|
||||
val orderId: String,
|
||||
val orderStatus: OrderStatus,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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?>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue