Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 04:30:38 -07:00
parent 6a7391fd73
commit bfeea010d0
12 changed files with 92 additions and 8 deletions

View file

@ -249,6 +249,15 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
}
override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) {
appPreferencesStore.store(PreferencesKeys.getTangemPayDeactivatedKey(userWalletId), true)
}
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
val key = PreferencesKeys.getTangemPayDeactivatedKey(userWalletId)
return appPreferencesStore.getSyncOrNull(key) == true
}
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
withContext(dispatcherProvider.io) {
secureStorage.delete(createAuthTokensKey(customerWalletAddress))

View file

@ -209,6 +209,9 @@ object PreferencesKeys {
fun getTangemPayHideOnboardingKey(userWalletId: UserWalletId) =
booleanPreferencesKey("tangem_pay_hide_onboarding_key_$userWalletId")
fun getTangemPayDeactivatedKey(userWalletId: UserWalletId) =
booleanPreferencesKey("tangem_pay_deactivated_$userWalletId")
// endregion
}

View file

@ -56,4 +56,7 @@ interface TangemPayStorage {
suspend fun getTangemPayEligibility(): Set<String>
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId)
suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean
}

View file

@ -84,15 +84,20 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
}
private suspend fun getPossibleWalletsForTangemPay(entryPoint: TangemPayEntryPoint?): List<UserWallet> {
val wallets = userWalletsListRepository.userWallets.value ?: return emptyList()
val candidates = wallets.filter { wallet ->
wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible() &&
!onboardingRepository.isTangemPayDeactivated(wallet.walletId)
}
if (candidates.isEmpty()) return emptyList()
if (!checkTangemPayEligibility(entryPoint = entryPoint)) {
return emptyList()
}
val wallets = userWalletsListRepository.userWallets.value ?: return emptyList()
return wallets.filter { wallet ->
wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible()
}
return candidates
}
private fun UserWallet.isCompatible(): Boolean = when (this) {

View file

@ -52,6 +52,16 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
)
}
if (onboardingRepository.isTangemPayDeactivated(params.userWalletId)) {
return@catchOn paymentAccountStatusesStore.store(
userWalletId = params.userWalletId,
status = AccountStatus.Payment(
account = account,
value = PaymentAccountStatusValue.NotCreated,
),
)
}
val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId)
.fold(
ifLeft = { error ->
@ -236,6 +246,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
return when (this) {
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated
is VisaApiError.Deactivated -> PaymentAccountStatusValue.NotCreated
else -> PaymentAccountStatusValue.Error.Unavailable
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.flatMap
import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.api.pay.TangemPayApi
@ -88,7 +90,22 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) }
.map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) }
.flatMap { response ->
val result = response.result
val status = result?.productInstance?.status
val isDeactivated = status == CustomerMeResponse.ProductInstance.Status.DEACTIVATED
val isBlocked = result?.state?.let { CustomerInfo.State.fromString(it) } == CustomerInfo.State.BLOCKED
if (isDeactivated || isBlocked) {
tangemPayStorage.storeIsTangemPayDeactivated(userWalletId)
VisaApiError.Deactivated.left()
} else {
getCustomerInfo(userWalletId = userWalletId, response = result).right()
}
}
}
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
return tangemPayStorage.isTangemPayDeactivated(userWalletId)
}
override suspend fun clearOrderId(userWalletId: UserWalletId) {

View file

@ -57,6 +57,7 @@ sealed class VisaApiError(
data object CustomerIsBlocked : VisaApiError(104110210)
data object UnknownWithoutCode : VisaApiError(104110999)
data class Unknown(override val errorCode: Int) : VisaApiError(errorCode)
data object Deactivated : VisaApiError(0)
fun isUnknown() = this is UnknownWithoutCode || this is Unknown

View file

@ -23,6 +23,24 @@ data class CustomerInfo(
val kycStatus: KycStatus,
val cardInfo: CardInfo?,
) {
enum class State {
NEW,
ACTIVE,
BLOCKED,
IN_PROGRESS,
UNDEFINED,
;
companion object {
fun fromString(value: String) = when (value.uppercase()) {
"NEW" -> NEW
"ACTIVE" -> ACTIVE
"BLOCKED" -> BLOCKED
"IN_PROGRESS" -> IN_PROGRESS
else -> UNDEFINED
}
}
}
data class ProductInstance(
val id: String,

View file

@ -6,4 +6,5 @@ sealed interface TangemPayCustomerInfoError {
data object RefreshNeededError : TangemPayCustomerInfoError
data object UnknownError : TangemPayCustomerInfoError
data object ExposedDeviceError : TangemPayCustomerInfoError
data object DeactivatedError : TangemPayCustomerInfoError
}

View file

@ -35,4 +35,6 @@ interface OnboardingRepository {
suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId)
suspend fun disableTangemPay(userWalletId: UserWalletId): Either<VisaApiError, Unit>
suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean
}

View file

@ -29,6 +29,11 @@ class 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}")
@ -57,8 +62,11 @@ class TangemPayMainScreenCustomerInfoUseCase(
}
val result = proceedWithPaeraCustomerResult(userWalletId)
.map(MainCustomerInfoContentState::Content)
updateState(userWalletId, result)
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)
@ -164,6 +172,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
return when (this) {
is VisaApiError.RefreshTokenExpired -> TangemPayCustomerInfoError.RefreshNeededError
is VisaApiError.NotPaeraCustomer -> TangemPayCustomerInfoError.UnknownError
is VisaApiError.Deactivated -> TangemPayCustomerInfoError.DeactivatedError
else -> TangemPayCustomerInfoError.UnavailableError
}
}

View file

@ -65,6 +65,11 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
TangemPayCustomerInfoError.ExposedDeviceError -> {
stateController.update(TangemPayExposedDeviceTransformer(userWalletId))
}
TangemPayCustomerInfoError.DeactivatedError -> {
stateController.update(
transformer = TangemPayHiddenStateTransformer(userWalletId),
)
}
TangemPayCustomerInfoError.UnknownError -> {
// hide TangemPay block
TangemLogger.e("Failed when loading main screen TangemPay info: $tangemPayError")