Updated on 2026-08-14
This commit is contained in:
parent
7b46bfda11
commit
6d686a0820
11 changed files with 103 additions and 26 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue