Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-20 15:31:57 +03:00
commit 47dc1c38df
762 changed files with 23960 additions and 10924 deletions

View file

@ -2,14 +2,16 @@ package com.tangem.data.pay
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.model.TangemPayEntryPoint
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.sync.Mutex
@ -17,57 +19,59 @@ import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
internal class DefaultTangemPayEligibilityManager @Inject constructor(
dispatchers: CoroutineDispatcherProvider,
private val userWalletsListRepository: UserWalletsListRepository,
private val coroutineScope: AppCoroutineScope,
private val onboardingRepository: OnboardingRepository,
) : TangemPayEligibilityManager {
private var cachedEligibleWallets: List<UserWalletData>? = null
private var cachedEligibleWallets: Map<TangemPayEntryPoint?, List<UserWalletData>> = emptyMap()
private var eligibleWalletsDeferred: Deferred<List<UserWalletData>>? = null
private val loadMutex = Mutex()
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
init {
resetDataWhenWalletsUpdate()
}
override suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet> {
return getUserWalletsData().mapNotNull {
override suspend fun getEligibleWallets(
shouldExcludePaeraCustomers: Boolean,
entryPoint: TangemPayEntryPoint?,
): List<UserWallet> {
return getUserWalletsData(entryPoint = entryPoint).mapNotNull {
if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet else null
}
}
override suspend fun getPossibleWalletsIds(shouldExcludePaeraCustomers: Boolean): List<UserWalletId> {
return getPossibleWalletsForTangemPay().addPaeraCustomersData().mapNotNull {
return getPossibleWalletsForTangemPay(entryPoint = null).addPaeraCustomersData().mapNotNull {
if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet.walletId else null
}
}
override suspend fun getTangemPayAvailability(): Boolean {
return onboardingRepository.checkCustomerEligibility()
override suspend fun getTangemPayAvailability(entryPoint: TangemPayEntryPoint): Boolean {
val eligibility = onboardingRepository.getCustomerEligibility().ifEmpty {
onboardingRepository.checkCustomerEligibility()
}
val type = entryPoint.toEligibilityType()
return eligibility.any { it == type }
.also { isEligible -> if (!isEligible) reset() }
}
override suspend fun isPaeraCustomerForAnyWallet(): Boolean {
return getUserWalletsData().any { it.isPaeraCustomer }
override suspend fun isPaeraCustomerForAnyWallet(entryPoint: TangemPayEntryPoint): Boolean {
return getUserWalletsData(entryPoint = entryPoint).any { it.isPaeraCustomer }
}
private suspend fun getUserWalletsData(): List<UserWalletData> {
cachedEligibleWallets?.let { return it }
private suspend fun getUserWalletsData(entryPoint: TangemPayEntryPoint?): List<UserWalletData> {
cachedEligibleWallets[entryPoint]?.let { return it }
return loadMutex.withLock {
cachedEligibleWallets?.let { return it }
cachedEligibleWallets[entryPoint]?.let { return it }
eligibleWalletsDeferred?.let { return it.await() }
coroutineScope {
val deferred = async {
if (!checkTangemPayEligibility()) {
emptyList()
} else {
getPossibleWalletsForTangemPay()
.addPaeraCustomersData()
.also { cachedEligibleWallets = it }
}
getPossibleWalletsForTangemPay(entryPoint = entryPoint)
.addPaeraCustomersData()
.also { cachedEligibleWallets += mapOf(entryPoint to it) }
}
eligibleWalletsDeferred = deferred
try {
@ -79,8 +83,8 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
}
}
private suspend fun getPossibleWalletsForTangemPay(): List<UserWallet> {
if (!checkTangemPayEligibility()) {
private suspend fun getPossibleWalletsForTangemPay(entryPoint: TangemPayEntryPoint?): List<UserWallet> {
if (!checkTangemPayEligibility(entryPoint = entryPoint)) {
return emptyList()
}
@ -122,13 +126,25 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
}
override fun reset() {
cachedEligibleWallets = null
cachedEligibleWallets = emptyMap()
eligibleWalletsDeferred?.cancel()
eligibleWalletsDeferred = null
}
private suspend fun checkTangemPayEligibility(): Boolean {
return onboardingRepository.getCustomerEligibility() || onboardingRepository.checkCustomerEligibility()
private suspend fun checkTangemPayEligibility(entryPoint: TangemPayEntryPoint?): Boolean {
val eligibility = onboardingRepository.getCustomerEligibility().ifEmpty {
onboardingRepository.checkCustomerEligibility()
}
return if (entryPoint == null) {
eligibility.isNotEmpty()
} else {
eligibility.any { it == entryPoint.toEligibilityType() }
}
}
private fun TangemPayEntryPoint.toEligibilityType(): TangemPayEligibilityType = when (this) {
TangemPayEntryPoint.BANNER -> TangemPayEligibilityType.BANNER
TangemPayEntryPoint.DETAILS -> TangemPayEligibilityType.DETAILS
}
private data class UserWalletData(

View file

@ -18,6 +18,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
@ -38,8 +39,6 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -112,6 +111,7 @@ internal interface TangemPayDataModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
scope: AppCoroutineScope,
): PaymentAccountStatusesStore {
return PaymentAccountStatusesStore(
runtimeStore = RuntimeSharedStore(),
@ -122,9 +122,9 @@ internal interface TangemPayDataModule {
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
scope = scope,
),
dispatchers = dispatchers,
scope = scope,
)
}

View file

@ -12,6 +12,7 @@ import com.tangem.datasource.api.pay.models.response.OrderResponse
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.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -211,19 +212,20 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
}
override suspend fun checkCustomerEligibility(): Boolean {
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> {
val response = requestHelper.performWithStaticToken {
tangemPayApi.checkCustomerEligibility()
tangemPayApi.getEligibilityChannels()
}.getOrNull()
val isAvailable = response?.result?.isTangemPayAvailable == true
tangemPayStorage.storeTangemPayEligibility(eligibility = isAvailable)
val channels = response?.result?.channels
val eligibility = channels.orEmpty().toSet()
tangemPayStorage.storeTangemPayEligibility(eligibility = eligibility)
return isAvailable
return eligibility.map(TangemPayEligibilityType::fromString)
}
override suspend fun getCustomerEligibility(): Boolean {
return tangemPayStorage.getTangemPayEligibility()
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> {
return tangemPayStorage.getTangemPayEligibility().map(TangemPayEligibilityType::fromString)
}
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {

View file

@ -19,6 +19,7 @@ import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse
import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
@ -45,9 +46,9 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val storage: TangemPayStorage,
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
private val errorConverter: TangemPayErrorConverter,
private val pollingScope: AppCoroutineScope,
) : TangemPayCardDetailsRepository {
private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val pollingJobs = mutableMapOf<String, Job>()
private val storePollingMutex = Mutex()

View file

@ -22,11 +22,9 @@ import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.extensions.addHexPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@ -43,6 +41,7 @@ import kotlin.time.Duration.Companion.seconds
private const val TAG = "TangemPaySwapRepository"
private const val MAX_POLLING_ATTEMPTS = 6
private data class PollingKey(val userWalletId: String, val orderId: String)
@Suppress("LongParameterList")
@ -54,9 +53,9 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
private val tangemPayStorage: TangemPayStorage,
private val swapRepository: SwapRepository,
private val orderRepository: CustomerOrderRepository,
private val withdrawPollingScope: AppCoroutineScope,
) : TangemPayWithdrawRepository {
private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val pollingJobs = mutableMapOf<PollingKey, Job>()
private val pollingMutex = Mutex()
@ -139,7 +138,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
orderId: String,
exchangeData: TangemPayWithdrawExchangeState,
) {
pollingScope.launch {
withdrawPollingScope.launch {
startWithdrawOrderPolling(userWallet, orderId, exchangeData)
}
}
@ -154,7 +153,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
pollingMutex.withLock {
if (pollingJobs.containsKey(key)) return@withLock
val pollingJob = pollingScope.launch {
val pollingJob = withdrawPollingScope.launch {
try {
var attemptCount = 0
while (isActive && attemptCount < MAX_POLLING_ATTEMPTS) {
@ -194,7 +193,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
}
private fun stopPolling(userWalletId: String, orderId: String) {
pollingScope.launch {
withdrawPollingScope.launch {
pollingMutex.withLock {
val key = PollingKey(userWalletId, orderId)
pollingJobs[key]?.cancel()

View file

@ -4,11 +4,9 @@ import androidx.datastore.core.DataStore
import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.PaymentAccountStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
@ -28,11 +26,9 @@ internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatu
internal class PaymentAccountStatusesStore(
private val runtimeStore: RuntimeSharedStore<WalletIdWithPaymentStatus>,
private val persistenceDataStore: DataStore<WalletIdWithPaymentStatusDM>,
dispatchers: CoroutineDispatcherProvider,
private val scope: AppCoroutineScope,
) {
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
init {
scope.launch {
try {