Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-16 16:10:57 +04:00
parent 7e02e72a40
commit c2e67ba6bc
41 changed files with 863 additions and 119 deletions

View file

@ -0,0 +1,94 @@
package com.tangem.data.pay
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
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.repository.OnboardingRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
internal class DefaultTangemPayEligibilityManager @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val onboardingRepository: OnboardingRepository,
) : TangemPayEligibilityManager {
private var cachedEligibleWallets: List<UserWallet>? = null
private var eligibleWalletsDeferred: Deferred<List<UserWallet>>? = null
private val loadMutex = Mutex()
override suspend fun getEligibleWallets(): List<UserWallet> {
cachedEligibleWallets?.let { return it }
return loadMutex.withLock {
cachedEligibleWallets?.let { return it }
eligibleWalletsDeferred?.let { return it.await() }
coroutineScope {
val deferred = async {
getPossibleWalletsForTangemPay()
.excludePaeraCustomers()
.also { cachedEligibleWallets = it }
}
eligibleWalletsDeferred = deferred
try {
deferred.await()
} finally {
eligibleWalletsDeferred = null
}
}
}
}
private suspend fun getPossibleWalletsForTangemPay(): List<UserWallet> {
if (!onboardingRepository.checkCustomerEligibility()) {
return emptyList()
}
val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWallets.value
} else {
userWalletsListManager.userWalletsSync
} ?: return emptyList()
return wallets.filter { wallet ->
wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible()
}
}
private fun UserWallet.isCompatible(): Boolean = when (this) {
is UserWallet.Cold ->
scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
is UserWallet.Hot -> true
}
private suspend fun List<UserWallet>.excludePaeraCustomers(): List<UserWallet> {
if (isEmpty()) return this
return coroutineScope {
map { wallet ->
async {
val isCustomer = onboardingRepository
.checkCustomerWallet(wallet.walletId)
.getOrNull() == true
wallet to isCustomer
}
}
.awaitAll()
.mapNotNull { (wallet, isCustomer) ->
wallet.takeUnless { isCustomer }
}
}
}
}

View file

@ -1,10 +1,12 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
import com.tangem.data.pay.repository.*
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
@ -62,6 +64,10 @@ internal interface TangemPayDataModule {
@Singleton
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
@Binds
@Singleton
fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager
companion object {
@Provides
@Singleton
@ -69,11 +75,13 @@ internal interface TangemPayDataModule {
repository: OnboardingRepository,
customerOrderRepository: CustomerOrderRepository,
tangemPayOnboardingRepository: OnboardingRepository,
eligibilityManager: TangemPayEligibilityManager,
): TangemPayMainScreenCustomerInfoUseCase {
return TangemPayMainScreenCustomerInfoUseCase(
repository = repository,
customerOrderRepository = customerOrderRepository,
tangemPayOnboardingRepository = tangemPayOnboardingRepository,
eligibilityManager = eligibilityManager,
)
}

View file

@ -84,7 +84,6 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
// TODO implement selector
return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) }
.map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) }
}
@ -193,4 +192,19 @@ internal class DefaultOnboardingRepository @Inject constructor(
error
}
}
override suspend fun checkCustomerEligibility(): Boolean {
val response = requestHelper.performWithoutToken {
tangemPayApi.checkCustomerEligibility()
}.getOrNull()
return response?.result?.isTangemPayAvailable == true
}
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
return tangemPayStorage.getHideMainOnboardingBanner(userWalletId)
}
override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) {
tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true)
}
}

View file

@ -89,6 +89,19 @@ internal class TangemPayRequestPerformer @Inject constructor(
)
}
suspend fun <T : Any> performWithoutToken(requestBlock: suspend () -> ApiResponse<T>): Either<VisaApiError, T> =
withContext(dispatchers.io) {
catch(
block = {
when (val apiResponse = requestBlock()) {
is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left()
is ApiResponse.Success<T> -> apiResponse.data.right()
}
},
catch = { errorConverter.convert(it).left() },
)
}
suspend fun <T : Any> performRequest(
userWalletId: UserWalletId,
requestBlock: suspend (header: String) -> ApiResponse<T>,

View file

@ -1,38 +0,0 @@
package com.tangem.data.pay.util
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import javax.inject.Inject
// TODO remove after implement wallet selector in pay
class TangemPayWalletsManager @Inject constructor(
private val manager: UserWalletsListManager,
private val repository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
@Deprecated("Don't use and put userWallet in features that need it")
suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold {
val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets
val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first()
return findColdWallet(userWallets)
}
@Deprecated("Don't use and put userWallet in features that need it")
fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold {
val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync
return findColdWallet(userWallets)
}
private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled
private fun findColdWallet(userWallets: List<UserWallet>?): UserWallet.Cold {
return userWallets?.find {
it is UserWallet.Cold && it.isMultiCurrency
} as? UserWallet.Cold ?: error("Cannot find cold user wallet")
}
}