Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-20 01:37:34 -07:00
parent 2070f1d21c
commit 8ca7bc0ca7
15 changed files with 132 additions and 53 deletions

View file

@ -231,12 +231,22 @@ internal class DefaultTangemPayStorage @Inject constructor(
) == true
}
override suspend fun storeTangemPayEligibility(eligibility: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility)
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
value = eligibility,
)
}
}
override suspend fun getTangemPayEligibility(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false)
override suspend fun getTangemPayEligibility(): Set<String> {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
default = emptySet(),
)
}
}
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {

View file

@ -37,8 +37,8 @@ interface TangemPayApi {
@POST("v1/deeplink/validate")
suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse<DeeplinkValidityResponse>
@GET("v1/customer/eligibility")
suspend fun checkCustomerEligibility(): ApiResponse<CustomerEligibilityResponse>
@GET("v1/eligibility/channels")
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
@GET("v1/order/{order_id}")
suspend fun getOrder(

View file

@ -4,11 +4,11 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerEligibilityResponse(
@Json(name = "result") val result: Result?,
data class TangemPayEligibilityChannels(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "is_tangem_pay_available") val isTangemPayAvailable: Boolean,
@Json(name = "channels") val channels: List<String>,
)
}

View file

@ -167,7 +167,7 @@ object PreferencesKeys {
val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy {
stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey")
}
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") }
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { stringSetPreferencesKey(name = "tangemPayEligibilityList") }
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
// endregion

View file

@ -52,8 +52,8 @@ interface TangemPayStorage {
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean)
suspend fun storeTangemPayEligibility(eligibility: Boolean)
suspend fun getTangemPayEligibility(): Boolean
suspend fun storeTangemPayEligibility(eligibility: Set<String>)
suspend fun getTangemPayEligibility(): Set<String>
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
}

View file

@ -2,12 +2,14 @@ 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 kotlinx.coroutines.*
@ -22,7 +24,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
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()
@ -30,43 +32,46 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
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 {
@ -78,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 +127,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

@ -11,6 +11,7 @@ import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
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
@ -207,19 +208,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

@ -0,0 +1,17 @@
package com.tangem.domain.models
enum class TangemPayEligibilityType {
BANNER,
DETAILS,
UNKNOWN,
;
companion object {
fun fromString(value: String): TangemPayEligibilityType = when (value.lowercase()) {
"banner" -> BANNER
"details" -> DETAILS
else -> UNKNOWN
}
}
}

View file

@ -2,10 +2,14 @@ package com.tangem.domain.pay
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayEntryPoint
interface TangemPayEligibilityManager {
suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet>
suspend fun getEligibleWallets(
shouldExcludePaeraCustomers: Boolean,
entryPoint: TangemPayEntryPoint?,
): List<UserWallet>
/**
* Returns all compatible user wallets without checking Tangem Pay eligibility, only used when opening deeplink
@ -13,9 +17,9 @@ interface TangemPayEligibilityManager {
* */
suspend fun getPossibleWalletsIds(shouldExcludePaeraCustomers: Boolean): List<UserWalletId>
suspend fun getTangemPayAvailability(): Boolean
suspend fun getTangemPayAvailability(entryPoint: TangemPayEntryPoint): Boolean
suspend fun isPaeraCustomerForAnyWallet(): Boolean
suspend fun isPaeraCustomerForAnyWallet(entryPoint: TangemPayEntryPoint): Boolean
fun reset()
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.pay.model
enum class TangemPayEntryPoint {
BANNER,
DETAILS,
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.visa.error.VisaApiError
@ -25,8 +26,8 @@ interface OnboardingRepository {
suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
suspend fun checkCustomerEligibility(): Boolean
suspend fun getCustomerEligibility(): Boolean
suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType>
suspend fun getCustomerEligibility(): List<TangemPayEligibilityType>
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?

View file

@ -69,12 +69,16 @@ class TangemPayMainScreenCustomerInfoUseCase(
}
private suspend fun showOnboardingBannerIfEligible(userWalletId: UserWalletId) {
if (eligibilityManager.isPaeraCustomerForAnyWallet()) {
val tangemPayEntryPoint = TangemPayEntryPoint.BANNER
if (eligibilityManager.isPaeraCustomerForAnyWallet(tangemPayEntryPoint)) {
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
return
}
val isEligible = eligibilityManager
.getEligibleWallets(shouldExcludePaeraCustomers = false)
.getEligibleWallets(
shouldExcludePaeraCustomers = false,
entryPoint = tangemPayEntryPoint,
)
.any { it.walletId == userWalletId }
if (isEligible) {
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {

View file

@ -20,6 +20,7 @@ import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.model.TangemPayEntryPoint
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
@ -255,7 +256,10 @@ internal class DetailsModel @Inject constructor(
private fun addTangemPayItemIfEligible() {
modelScope.launch {
val isEligible = tangemPayEligibilityManager
.getEligibleWallets(shouldExcludePaeraCustomers = true)
.getEligibleWallets(
shouldExcludePaeraCustomers = true,
entryPoint = TangemPayEntryPoint.DETAILS,
)
.isNotEmpty()
if (isEligible) {
items.update { itemsBuilder.addTangemPayItem(items = it, onClick = ::onTangemPayItemClicked) }
@ -266,7 +270,7 @@ internal class DetailsModel @Inject constructor(
private fun onTangemPayItemClicked() {
modelScope.launch {
analyticsEventHandler.send(TangemPayAnalyticsEvents.DetailsVisaPermanentButtonClicked())
val isEligible = tangemPayEligibilityManager.getTangemPayAvailability()
val isEligible = tangemPayEligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DETAILS)
if (isEligible) {
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings))
} else {

View file

@ -14,6 +14,7 @@ import com.tangem.core.navigation.url.UrlOpener
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.model.TangemPayEntryPoint
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@ -127,7 +128,10 @@ internal class TangemPayOnboardingModel @Inject constructor(
val shouldExcludePaeraCustomers = params is TangemPayOnboardingComponent.Params.FromBannerInSettings
modelScope.launch {
val eligibleWalletsIds = eligibilityManager
.getEligibleWallets(shouldExcludePaeraCustomers = shouldExcludePaeraCustomers)
.getEligibleWallets(
shouldExcludePaeraCustomers = shouldExcludePaeraCustomers,
entryPoint = params.toEntryPointOrNull(),
)
.map { it.walletId }
if (eligibleWalletsIds.isEmpty()) {
back()
@ -204,4 +208,13 @@ internal class TangemPayOnboardingModel @Inject constructor(
override fun onWalletSelectorDismiss() {
bottomSheetNavigation.dismiss()
}
private fun TangemPayOnboardingComponent.Params.toEntryPointOrNull() = when (this) {
is TangemPayOnboardingComponent.Params.FromBannerInSettings -> TangemPayEntryPoint.DETAILS
is TangemPayOnboardingComponent.Params.FromBannerOnMain -> TangemPayEntryPoint.BANNER
is TangemPayOnboardingComponent.Params.Deeplink,
is TangemPayOnboardingComponent.Params.ContinueOnboarding,
-> null
}
}

View file

@ -22,6 +22,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.model.TangemPayEntryPoint
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
@ -246,7 +247,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
override fun onOnboardingBannerClick(userWalletId: UserWalletId) {
modelScope.launch {
analyticsEventHandler.send(TangemPayAnalyticsEvents.MainVisaPermanentBannerClicked())
val isEligible = tangemPayEligibilityManager.getTangemPayAvailability()
val isEligible = tangemPayEligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.BANNER)
if (isEligible) {
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain)
} else {