Updated on 2026-08-14
This commit is contained in:
parent
a8faaffcab
commit
2db9958dd1
17 changed files with 120 additions and 83 deletions
|
|
@ -4,5 +4,4 @@ import com.squareup.moshi.Json
|
|||
|
||||
data class CardBalanceResponse(
|
||||
@Json(name = "result") val result: BalanceResponse?,
|
||||
@Json(name = "error") val error: String?,
|
||||
)
|
||||
|
|
@ -5,7 +5,6 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
data class CardDetailsResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class CheckCustomerWalletResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
data class Result(
|
||||
@Json(name = "id") val id: String?,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerEligibilityResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerMeResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class DeeplinkValidityResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
class FreezeUnfreezeCardResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class OrderResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import java.math.BigDecimal
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TangemPayTxHistoryResponse(
|
||||
@Json(name = "error") val error: String?,
|
||||
@Json(name = "result") val result: Result,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class WithdrawDataResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class WithdrawResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
|
|
|
|||
|
|
@ -9,26 +9,43 @@ 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 com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
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 var cachedEligibleWallets: List<UserWalletData>? = null
|
||||
private var eligibleWalletsDeferred: Deferred<List<UserWalletData>>? = null
|
||||
private val loadMutex = Mutex()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override suspend fun getEligibleWallets(): List<UserWallet> {
|
||||
init {
|
||||
resetDataWhenWalletsUpdate()
|
||||
}
|
||||
|
||||
override suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet> {
|
||||
return getUserWalletsData().mapNotNull {
|
||||
if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet else null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUserWalletsData(): List<UserWalletData> {
|
||||
cachedEligibleWallets?.let { return it }
|
||||
|
||||
return loadMutex.withLock {
|
||||
|
|
@ -38,7 +55,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
coroutineScope {
|
||||
val deferred = async {
|
||||
getPossibleWalletsForTangemPay()
|
||||
.excludePaeraCustomers()
|
||||
.addPaeraCustomersData()
|
||||
.also { cachedEligibleWallets = it }
|
||||
}
|
||||
eligibleWalletsDeferred = deferred
|
||||
|
|
@ -73,8 +90,8 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
is UserWallet.Hot -> true
|
||||
}
|
||||
|
||||
private suspend fun List<UserWallet>.excludePaeraCustomers(): List<UserWallet> {
|
||||
if (isEmpty()) return this
|
||||
private suspend fun List<UserWallet>.addPaeraCustomersData(): List<UserWalletData> {
|
||||
if (isEmpty()) return emptyList()
|
||||
|
||||
return coroutineScope {
|
||||
map { wallet ->
|
||||
|
|
@ -86,9 +103,30 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.mapNotNull { (wallet, isCustomer) ->
|
||||
wallet.takeUnless { isCustomer }
|
||||
.map { (userWallet, isPaeraCustomer) ->
|
||||
UserWalletData(userWallet, isPaeraCustomer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetDataWhenWalletsUpdate() {
|
||||
coroutineScope.launch {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.collectLatest { reset() }
|
||||
} else {
|
||||
userWalletsListManager.userWallets.collectLatest { reset() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
cachedEligibleWallets = null
|
||||
eligibleWalletsDeferred?.cancel()
|
||||
eligibleWalletsDeferred = null
|
||||
}
|
||||
|
||||
private data class UserWalletData(
|
||||
val userWallet: UserWallet,
|
||||
val isPaeraCustomer: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
|
||||
interface TangemPayEligibilityManager {
|
||||
|
||||
suspend fun getEligibleWallets(): List<UserWallet>
|
||||
suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet>
|
||||
}
|
||||
|
|
@ -46,7 +46,11 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
.fold(
|
||||
ifLeft = { error ->
|
||||
Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
if (error is VisaApiError.NotPaeraCustomer) {
|
||||
showOnboardingBannerIfEligible(userWalletId)
|
||||
} else {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
}
|
||||
},
|
||||
ifRight = { hasTangemPay ->
|
||||
Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay")
|
||||
|
|
@ -61,21 +65,27 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
updateState(userWalletId, result)
|
||||
} else {
|
||||
// if there's no tangem pay, check eligibility and show onboarding banner
|
||||
val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId }
|
||||
if (isEligible) {
|
||||
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
} else {
|
||||
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
|
||||
}
|
||||
} else {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
}
|
||||
showOnboardingBannerIfEligible(userWalletId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun showOnboardingBannerIfEligible(userWalletId: UserWalletId) {
|
||||
val isEligible = eligibilityManager
|
||||
.getEligibleWallets(shouldExcludePaeraCustomers = false)
|
||||
.any { it.walletId == userWalletId }
|
||||
if (isEligible) {
|
||||
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
} else {
|
||||
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
|
||||
}
|
||||
} else {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
}
|
||||
}
|
||||
|
||||
operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
): Flow<Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>> {
|
||||
|
|
|
|||
|
|
@ -248,7 +248,9 @@ internal class DetailsModel @Inject constructor(
|
|||
private fun addTangemPayItemIfEligible() {
|
||||
if (!tangemPayFeatureToggles.isTangemPayEnabled) return
|
||||
modelScope.launch {
|
||||
val isEligible = tangemPayEligibilityManager.getEligibleWallets().isNotEmpty()
|
||||
val isEligible = tangemPayEligibilityManager
|
||||
.getEligibleWallets(shouldExcludePaeraCustomers = true)
|
||||
.isNotEmpty()
|
||||
if (isEligible) {
|
||||
items.update { itemsBuilder.addVisaItem(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
.onRight { isValid -> if (isValid) showOnboarding() else back() }
|
||||
.onLeft { back() }
|
||||
}
|
||||
is TangemPayOnboardingComponent.Params.ContinueOnboarding,
|
||||
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
|
||||
openKyc(userWalletId = params.userWalletId)
|
||||
}
|
||||
is TangemPayOnboardingComponent.Params.FromBannerInSettings,
|
||||
is TangemPayOnboardingComponent.Params.FromBannerOnMain,
|
||||
-> showOnboarding()
|
||||
|
|
@ -113,8 +115,13 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
|
||||
private fun onGetCardClick() {
|
||||
analytics.send(TangemPayAnalyticsEvents.GetCardClicked())
|
||||
// if user came from deeplink or banner in settings and already is a paera customer -> exclude this wallet
|
||||
val shouldExcludePaeraCustomers = params is TangemPayOnboardingComponent.Params.FromBannerInSettings ||
|
||||
params is TangemPayOnboardingComponent.Params.Deeplink
|
||||
modelScope.launch {
|
||||
val eligibleWalletsIds = eligibilityManager.getEligibleWallets().map { it.walletId }
|
||||
val eligibleWalletsIds = eligibilityManager
|
||||
.getEligibleWallets(shouldExcludePaeraCustomers = shouldExcludePaeraCustomers)
|
||||
.map { it.walletId }
|
||||
if (eligibleWalletsIds.isEmpty()) {
|
||||
back()
|
||||
return@launch
|
||||
|
|
|
|||
|
|
@ -4,21 +4,9 @@ import android.content.res.Configuration
|
|||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
|
@ -28,8 +16,9 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -57,28 +46,20 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m
|
|||
)
|
||||
.clickable(onClick = state.onClick),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
ConstraintLayout(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.width(55.dp)
|
||||
.height(95.dp)
|
||||
.offset(y = 10.dp),
|
||||
)
|
||||
|
||||
SpacerH4()
|
||||
val (image, text, close) = createRefs()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.constrainAs(text) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(image.end, margin = 12.dp)
|
||||
end.linkTo(parent.end)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
.padding(top = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.tangempay_onboarding_banner_title),
|
||||
|
|
@ -94,22 +75,32 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m
|
|||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(onClick = state.closeOnClick)
|
||||
.padding(4.dp)
|
||||
.padding(top = 12.dp)
|
||||
.size(12.dp),
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.constrainAs(image) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(text.top)
|
||||
bottom.linkTo(text.bottom)
|
||||
height = Dimension.fillToConstraints
|
||||
}
|
||||
.padding(top = 8.dp, start = 24.dp),
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(onClick = state.closeOnClick)
|
||||
.constrainAs(close) {
|
||||
top.linkTo(parent.top, margin = 16.dp)
|
||||
end.linkTo(parent.end, margin = 16.dp)
|
||||
},
|
||||
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue