Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-22 19:30:22 +05:00
parent 7b46bfda11
commit 6d686a0820
11 changed files with 103 additions and 26 deletions

View file

@ -48,6 +48,7 @@ class MoshiModule {
)
.add(
NamePolymorphicAdapterFactory.of(PaymentAccountStatusValueDM::class.java)
.withSubtype(PaymentAccountStatusValueDM.Empty::class.java, "empty")
.withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created")
.withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status")
.withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card")

View file

@ -16,6 +16,11 @@ import java.math.BigDecimal
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
sealed interface PaymentAccountStatusValueDM {
@NameLabel("empty")
data class Empty(
@Json(name = "empty") val marker: Boolean = true,
) : PaymentAccountStatusValueDM
@NameLabel("not_created")
data class NotCreated(
@Json(name = "not_created") val marker: Boolean = true,

View file

@ -51,6 +51,7 @@ internal object PaymentAccountStatusValueDMConverter :
is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed(
customerId = value.customerId,
)
is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty()
// Transient statuses are not persisted
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Error.ExposedDevice,
@ -62,6 +63,7 @@ internal object PaymentAccountStatusValueDMConverter :
override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
return when (value) {
is PaymentAccountStatusValueDM.Empty -> PaymentAccountStatusValue.Empty
is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated
is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed(
customerId = value.customerId,

View file

@ -8,10 +8,13 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayEntryPoint
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
@ -28,12 +31,14 @@ import kotlin.time.Duration.Companion.minutes
private const val TAG = "PaymentAccountStatusFetcher"
@Suppress("LongParameterList")
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
private val onboardingRepository: OnboardingRepository,
private val customerOrderRepository: CustomerOrderRepository,
private val deviceSecurity: DeviceSecurityInfoProvider,
private val dispatchers: CoroutineDispatcherProvider,
private val eligibilityManager: TangemPayEligibilityManager,
) : PaymentAccountStatusFetcher {
private val logger = TangemLogger.withTag(TAG)
@ -43,6 +48,16 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
val account = Account.Payment(userWalletId = params.userWalletId)
logger.i("fetch: ${params.userWalletId.stringValue}")
if (onboardingRepository.isTangemPayDeactivated(params.userWalletId)) {
return@catchOn paymentAccountStatusesStore.store(
userWalletId = params.userWalletId,
status = AccountStatus.Payment(
account = account,
value = PaymentAccountStatusValue.Empty,
),
)
}
if (deviceSecurity.isSecurityExposed()) {
logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}")
logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}")
@ -57,22 +72,12 @@ 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 ->
logger.e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}")
when (error) {
is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated
is VisaApiError.NotPaeraCustomer -> constructNotCreatedOrEmptyStatus(params.userWalletId)
else -> PaymentAccountStatusValue.Error.Unavailable
}
},
@ -100,7 +105,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
return if (hasTangemPay) {
fetchTangemPayAccountStatus(account)
} else {
PaymentAccountStatusValue.NotCreated
constructNotCreatedOrEmptyStatus(account.userWalletId)
}
}
@ -133,7 +138,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
return onboardingRepository.getCustomerInfo(account.userWalletId).fold(
ifLeft = { error ->
logger.e("proceedWithoutOrder ${account.userWalletId} error: $error")
error.mapToPaymentAccountStatus()
error.mapToPaymentAccountStatus(account.userWalletId)
},
ifRight = { customerInfo ->
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
@ -153,7 +158,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
val customerInfo = onboardingRepository.getCustomerInfo(account.userWalletId).fold(
ifLeft = { error ->
logger.e("proceedWithOrderId KYC check ${account.userWalletId} error: $error")
return error.mapToPaymentAccountStatus()
return error.mapToPaymentAccountStatus(account.userWalletId)
},
ifRight = { it },
)
@ -172,7 +177,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold(
ifLeft = { error ->
logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error")
error.mapToPaymentAccountStatus()
error.mapToPaymentAccountStatus(account.userWalletId)
},
ifRight = { orderData ->
logger.i("proceedWithOrderId ${account.userWalletId}: $orderId status: ${orderData.status}")
@ -241,7 +246,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
onboardingRepository.clearOrderId(account.userWalletId)
return onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
.fold(
ifLeft = { it.mapToPaymentAccountStatus() },
ifLeft = { it.mapToPaymentAccountStatus(account.userWalletId) },
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() },
)
}
@ -297,12 +302,22 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
}
}
private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatusValue {
private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
return when (this) {
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated
is VisaApiError.Deactivated -> PaymentAccountStatusValue.NotCreated
is VisaApiError.NotPaeraCustomer -> constructNotCreatedOrEmptyStatus(userWalletId)
is VisaApiError.Deactivated -> constructNotCreatedOrEmptyStatus(userWalletId)
else -> PaymentAccountStatusValue.Error.Unavailable
}
}
private suspend fun constructNotCreatedOrEmptyStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
val entryPoint = TangemPayEntryPoint.BANNER
val shouldShowBanner = !eligibilityManager.isPaeraCustomerForAnyWallet(entryPoint) &&
eligibilityManager.getEligibleWallets(shouldExcludePaeraCustomers = false, entryPoint = entryPoint)
.any { it.walletId == userWalletId } &&
!onboardingRepository.getHideMainOnboardingBanner(userWalletId)
return if (shouldShowBanner) PaymentAccountStatusValue.NotCreated else PaymentAccountStatusValue.Empty
}
}

View file

@ -5,6 +5,7 @@ import arrow.core.flatMap
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.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
import com.tangem.datasource.api.pay.models.request.OrderRequest
@ -15,6 +16,8 @@ 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.TangemPayEligibilityType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWallet
@ -44,6 +47,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
private val authDataSource: TangemPayAuthDataSource,
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
private val userWalletsListRepository: UserWalletsListRepository,
private val paymentAccountStatusStore: PaymentAccountStatusesStore,
) : OnboardingRepository {
// Save data for a session
@ -265,6 +269,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) {
tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true)
paymentAccountStatusStore.store(
userWalletId = userWalletId,
status = AccountStatus.Payment(
account = Account.Payment(userWalletId),
value = PaymentAccountStatusValue.Empty,
),
)
}
override suspend fun disableTangemPay(userWalletId: UserWalletId): Either<VisaApiError, Unit> {

View file

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.map
* Accounts mode is considered enabled if any [com.tangem.domain.account.models.AccountStatusList] produced for the
* user's wallets has more than one [AccountStatus.CryptoPortfolio], or has a [AccountStatus.Payment] with any
* [PaymentAccountStatusValue.Empty].
*
* @property multiAccountStatusListSupplier supplier that provides a list of
* [com.tangem.domain.account.models.AccountStatusList]s for all user wallets
@ -45,6 +46,17 @@ class IsAccountsModeEnabledUseCase(
}
private fun PaymentAccountStatusValue.isActivePayment(): Boolean {
return this !is PaymentAccountStatusValue.NotCreated
return when (this) {
is PaymentAccountStatusValue.Empty,
is PaymentAccountStatusValue.NotCreated,
-> false
is PaymentAccountStatusValue.Error,
is PaymentAccountStatusValue.IssuingCard,
is PaymentAccountStatusValue.Loaded,
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Locked,
is PaymentAccountStatusValue.UnderReview,
-> true
}
}
}

View file

@ -102,6 +102,18 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when payment account is Empty`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.Empty)),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when payment account is UnderReview`() = runTest {
val statusList = createAccountStatusList(
@ -234,6 +246,18 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when payment account is Empty`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.Empty)),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when payment account is UnderReview`() = runTest {
val statusList = createAccountStatusList(

View file

@ -20,6 +20,7 @@ sealed class PaymentAccountStatusValue {
get() = when (this) {
is Error,
is IssuingCard,
is Empty,
is NotCreated,
is UnderReview,
-> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source)
@ -40,12 +41,19 @@ sealed class PaymentAccountStatusValue {
is Locked -> copy(source = source)
is UnderReview -> copy(source = source)
is Loading,
is Empty,
is NotCreated,
is Error,
-> this
}
}
/** Represents an empty payment account status when no specific state is available. */
@Serializable
data object Empty : PaymentAccountStatusValue() {
override val source: StatusSource = StatusSource.ACTUAL
}
/** Represents the Loading state of a payment account, typically while fetching its details. */
@Serializable
data object Loading : PaymentAccountStatusValue() {

View file

@ -29,13 +29,13 @@ import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokensync.model.TokenSyncProgress
import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.extensions.addIf
@ -43,11 +43,7 @@ import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]")
@ -214,6 +210,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Locked,
is PaymentAccountStatusValue.UnderReview,
is PaymentAccountStatusValue.Empty,
-> null
}
notification?.let(::add)

View file

@ -239,6 +239,7 @@ internal class GetWalletNotificationsFactory @Inject constructor(
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Locked,
is PaymentAccountStatusValue.UnderReview,
is PaymentAccountStatusValue.Empty,
-> null
}
notification?.let(::add)

View file

@ -53,6 +53,7 @@ internal class TangemPayMainBlockConverter(
}
},
)
is PaymentAccountStatusValue.Empty -> TangemPayMainUM.Empty
is PaymentAccountStatusValue.NotCreated -> TangemPayMainUM.Empty
is PaymentAccountStatusValue.Loading -> TangemPayMainUM.Loading
is PaymentAccountStatusValue.Locked -> TangemPayMainUM.Content(