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

@ -115,6 +115,7 @@ internal class DefaultTangemPayStorage @Inject constructor(
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
}
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
@ -144,6 +145,20 @@ internal class DefaultTangemPayStorage @Inject constructor(
}
}
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
}
}
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
) == true
}
}
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"

View file

@ -40,6 +40,8 @@ import com.tangem.features.tangempay.components.TangemPayDetailsContainerCompone
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerOnMain
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerInSettings
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -667,6 +669,9 @@ internal class ChildFactory @Inject constructor(
)
is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink(
deeplink = mode.deeplink,
)
is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings
is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain(
userWalletId = mode.userWalletId,
)
},

View file

@ -433,13 +433,20 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Deeplink(
val deeplink: String,
val userWalletId: UserWalletId?,
) : Mode()
@Serializable
data class ContinueOnboarding(
val userWalletId: UserWalletId?,
val userWalletId: UserWalletId,
) : Mode()
@Serializable
data class FromBannerOnMain(
val userWalletId: UserWalletId,
) : Mode()
@Serializable
data object FromBannerInSettings : Mode()
}
}

View file

@ -128,6 +128,9 @@ 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/order/{order_id}")
suspend fun getOrder(
@Header("Authorization") authHeader: String,

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
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(
@Json(name = "is_tangem_pay_available") val isTangemPayAvailable: Boolean,
)
}

View file

@ -192,6 +192,9 @@ object PreferencesKeys {
fun getTangemPayCheckCustomerByWalletId(userWalletId: UserWalletId) =
booleanPreferencesKey("tangem_pay_check_customer_by_wallet_id_$userWalletId")
fun getTangemPayHideOnboardingKey(userWalletId: UserWalletId) =
booleanPreferencesKey("tangem_pay_hide_onboarding_key_$userWalletId")
// endregion
}

View file

@ -30,5 +30,9 @@ interface TangemPayStorage {
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId)
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean)
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18.327,4.365C20.007,4.365 20.847,4.365 21.489,4.692C22.053,4.98 22.512,5.439 22.8,6.003C23.127,6.645 23.127,7.485 23.127,9.165V14.836C23.127,16.516 23.127,17.356 22.8,17.998C22.512,18.562 22.053,19.021 21.489,19.309C20.847,19.636 20.007,19.636 18.327,19.636H5.675C3.995,19.636 3.154,19.636 2.513,19.309C1.948,19.021 1.49,18.562 1.202,17.998C0.875,17.356 0.875,16.516 0.875,14.836V9.165C0.875,7.485 0.875,6.645 1.202,6.003C1.49,5.439 1.948,4.98 2.513,4.692C3.154,4.365 3.995,4.365 5.675,4.365H18.327ZM14.403,9.195C13.006,9.195 11.988,9.949 11.988,10.995C11.988,11.79 12.708,12.208 13.217,12.46C13.767,12.711 13.979,12.878 13.936,13.129C13.936,13.505 13.513,13.673 13.09,13.673C12.581,13.673 12.073,13.547 11.607,13.338L11.354,14.51C11.862,14.719 12.412,14.803 12.921,14.803C14.488,14.844 15.462,14.092 15.462,12.962C15.462,11.539 13.471,11.455 13.471,10.828C13.513,10.535 13.767,10.367 14.064,10.367C14.53,10.325 15.038,10.409 15.462,10.618L15.716,9.447C15.292,9.28 14.826,9.195 14.403,9.195ZM2.881,9.321V9.489C3.389,9.573 3.855,9.74 4.278,9.949C4.448,10.033 4.563,10.21 4.617,10.41L5.761,14.762H7.285L9.572,9.321H8.09L6.565,13.004L5.973,9.865C5.93,9.573 5.676,9.321 5.337,9.321H2.881ZM10.164,9.321L8.979,14.762H10.419L11.604,9.321H10.164ZM18.13,9.321C17.876,9.321 17.622,9.489 17.537,9.74L15.42,14.762H16.902L17.198,13.967H19.02L19.188,14.762H20.502L19.358,9.321H18.13ZM18.76,12.838H17.574L18.337,10.787L18.76,12.838Z"
android:fillColor="#656565"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

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")
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.pay
import com.tangem.domain.models.wallet.UserWallet
interface TangemPayEligibilityManager {
suspend fun getEligibleWallets(): List<UserWallet>
}

View file

@ -5,6 +5,7 @@ import java.math.BigDecimal
sealed class MainCustomerInfoContentState {
object Loading : MainCustomerInfoContentState()
object OnboardingBanner : MainCustomerInfoContentState()
data class Content(val info: MainScreenCustomerInfo) : MainCustomerInfoContentState()
}

View file

@ -25,5 +25,11 @@ interface OnboardingRepository {
suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
suspend fun checkCustomerEligibility(): Boolean
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId)
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.model.*
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository
@ -21,6 +22,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
private val repository: OnboardingRepository,
private val customerOrderRepository: CustomerOrderRepository,
private val tangemPayOnboardingRepository: OnboardingRepository,
private val eligibilityManager: TangemPayEligibilityManager,
) {
val state: StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>>>
@ -46,8 +48,17 @@ class TangemPayMainScreenCustomerInfoUseCase(
.map(MainCustomerInfoContentState::Content)
updateState(userWalletId, result)
} else {
// ignore if there's no TangemPay
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
// if there's no tangem pay, check eligibility and show onboarding banner
val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId }
if (isEligible) {
if (tangemPayOnboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
} else {
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
}
} else {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
}
}
},
)

View file

@ -20,6 +20,7 @@ dependencies {
implementation(projects.features.tester.api)
implementation(projects.features.createWalletSelection.api)
implementation(projects.features.hotWallet.api)
implementation(projects.features.tangempay.details.api)
/* Project - Core */
implementation(projects.core.decompose)
@ -46,6 +47,7 @@ dependencies {
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.legacy)
implementation(projects.domain.visa)
/* SDK */
// TODO: For TangemError model, should be removed after card domain scanning refactoring

View file

@ -20,6 +20,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
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.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
@ -34,6 +35,7 @@ import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
import com.tangem.features.details.utils.ItemsBuilder
import com.tangem.features.details.utils.SocialsBuilder
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.version.AppVersionProvider
import kotlinx.collections.immutable.ImmutableList
@ -51,7 +53,7 @@ import javax.inject.Inject
@Suppress("LongParameterList")
internal class DetailsModel @Inject constructor(
socialsBuilder: SocialsBuilder,
itemsBuilder: ItemsBuilder,
private val itemsBuilder: ItemsBuilder,
private val appVersionProvider: AppVersionProvider,
private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase,
private val router: Router,
@ -68,6 +70,8 @@ internal class DetailsModel @Inject constructor(
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
private val tangemPayEligibilityManager: TangemPayEligibilityManager,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
) : Model() {
private val params: DetailsComponent.Params = paramsContainer.require()
@ -100,6 +104,8 @@ internal class DetailsModel @Inject constructor(
),
)
addTangemPayItemIfEligible()
state = MutableStateFlow(
value = DetailsUM(
items = items.value,
@ -239,6 +245,16 @@ internal class DetailsModel @Inject constructor(
}
}
private fun addTangemPayItemIfEligible() {
if (!tangemPayFeatureToggles.isTangemPayEnabled) return
modelScope.launch {
val isEligible = tangemPayEligibilityManager.getEligibleWallets().isNotEmpty()
if (isEligible) {
items.update { itemsBuilder.addVisaItem(it) }
}
}
}
private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})"
private suspend fun buildBuyLink(): String {

View file

@ -37,6 +37,17 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
).let(::add)
}.toImmutableList()
fun addVisaItem(items: ImmutableList<DetailsItemUM>): ImmutableList<DetailsItemUM> {
return items.toMutableList().map { block ->
if (block.id == "shop" && block is DetailsItemUM.Basic) {
val newItems = block.items.toMutableList().apply { add(getVisaItem()) }
block.copy(items = newItems.toImmutableList())
} else {
block
}
}.toImmutableList()
}
private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? {
return if (isWalletConnectAvailable) {
DetailsItemUM.WalletConnect(
@ -114,4 +125,15 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
).let(::add)
}.toPersistentList(),
)
private fun getVisaItem(): DetailsItemUM.Basic.Item = DetailsItemUM.Basic.Item(
id = "get_tangem_visa",
block = BlockUM(
text = resourceReference(R.string.details_get_visa),
iconRes = R.drawable.ic_tangem_pay_24,
onClick = {
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings))
},
),
)
}

View file

@ -8,16 +8,19 @@ interface TangemPayOnboardingComponent : ComposableContentComponent {
sealed class Params {
abstract val userWalletId: UserWalletId?
data class Deeplink(
val deeplink: String,
override val userWalletId: UserWalletId?,
) : Params()
data class ContinueOnboarding(
override val userWalletId: UserWalletId?,
val userWalletId: UserWalletId,
) : Params()
data class FromBannerOnMain(
val userWalletId: UserWalletId,
) : Params()
data object FromBannerInSettings : Params()
}
interface Factory : ComponentFactory<Params, TangemPayOnboardingComponent>

View file

@ -27,6 +27,8 @@ dependencies {
implementation(projects.features.tangempay.onboarding.api)
implementation(projects.features.tangempay.details.api)
implementation(projects.features.kyc.api)
implementation(projects.features.wallet.api)
implementation(projects.features.hotWallet.api)
/** Domain */
implementation(projects.domain.visa)
@ -40,6 +42,7 @@ dependencies {
implementation(deps.compose.material3)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.decompose.ext.compose)
/** DI */
implementation(deps.hilt.android)
@ -48,4 +51,5 @@ dependencies {
/** Other */
implementation(deps.timber)
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
}

View file

@ -4,9 +4,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.ComponentContext
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.tangempay.model.TangemPayOnboardingModel
import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation
import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -19,10 +25,32 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor(
private val model: TangemPayOnboardingModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = TangemPayOnboardingNavigation.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
TandemPayOnboardingScreen(modifier = modifier, state = state)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
navigation: TangemPayOnboardingNavigation,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val context = childByContext(componentContext)
return when (navigation) {
is TangemPayOnboardingNavigation.WalletSelector -> TangemPayWalletSelectorComponent(
appComponentContext = context,
params = TangemPayWalletSelectorComponent.Params(listener = model, walletsIds = navigation.walletsIds),
)
}
}
@AssistedFactory

View file

@ -0,0 +1,39 @@
package com.tangem.features.tangempay.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tangempay.model.TangemPayWalletSelectorModel
import com.tangem.features.tangempay.ui.WalletSelectorBottomSheet
internal class TangemPayWalletSelectorComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: TangemPayWalletSelectorModel = getOrCreateModel(params = params)
override fun dismiss() {
model.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
WalletSelectorBottomSheet(state = state)
}
data class Params(
val listener: WalletSelectorListener,
val walletsIds: List<UserWalletId>,
)
}
internal interface WalletSelectorListener {
fun onWalletSelected(userWalletId: UserWalletId)
fun onWalletSelectorDismiss()
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.tangempay.deeplink
import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tangempay.TangemPayFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -13,15 +12,12 @@ internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor(
@Assisted uri: Uri,
appRouter: AppRouter,
tangemPayFeatureToggles: TangemPayFeatureToggles,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) : OnboardVisaDeepLinkHandler {
init {
if (tangemPayFeatureToggles.isTangemPayEnabled) {
val userWallet = getSelectedWalletSyncUseCase.invoke().getOrNull()
val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink(
deeplink = uri.toString(),
userWalletId = userWallet?.walletId,
)
appRouter.push(AppRoute.TangemPayOnboarding(mode))
} else {

View file

@ -3,6 +3,7 @@ package com.tangem.features.tangempay.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.tangempay.model.TangemPayOnboardingModel
import com.tangem.features.tangempay.model.TangemPayWalletSelectorModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -16,5 +17,10 @@ internal interface TangemPayOnboardingModelsModule {
@Binds
@IntoMap
@ClassKey(TangemPayOnboardingModel::class)
fun bindModel(model: TangemPayOnboardingModel): Model
fun bindTangemPayOnboardingModel(model: TangemPayOnboardingModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayWalletSelectorModel::class)
fun bindTangemPayWalletSelectorModel(model: TangemPayWalletSelectorModel): Model
}

View file

@ -1,6 +1,9 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@ -8,17 +11,16 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.WalletSelectorListener
import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer
import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
@ -40,27 +42,33 @@ internal class TangemPayOnboardingModel @Inject constructor(
private val repository: OnboardingRepository,
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
private val urlOpener: UrlOpener,
private val tangemPayWalletsManager: TangemPayWalletsManager,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
) : Model() {
private val eligibilityManager: TangemPayEligibilityManager,
) : Model(), WalletSelectorListener {
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
val bottomSheetNavigation: SlotNavigation<TangemPayOnboardingNavigation> = SlotNavigation()
val uiState: StateFlow<TangemPayOnboardingScreenState>
field = MutableStateFlow(getInitialState())
init {
analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened())
init()
}
private fun init() {
modelScope.launch {
when (params) {
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
checkCustomerInfo()
}
is TangemPayOnboardingComponent.Params.Deeplink -> {
repository.validateDeeplink(params.deeplink)
.onRight { isValid -> if (isValid) showOnboarding() else back() }
.onLeft { back() }
}
is TangemPayOnboardingComponent.Params.ContinueOnboarding,
is TangemPayOnboardingComponent.Params.FromBannerInSettings,
is TangemPayOnboardingComponent.Params.FromBannerOnMain,
-> showOnboarding()
}
}
}
@ -78,42 +86,25 @@ internal class TangemPayOnboardingModel @Inject constructor(
}
}
private suspend fun checkCustomerInfo() {
// TODO implement selector
val userWalletId = getUserWalletForPay(params.userWalletId)
repository.getCustomerInfo(
userWalletId = userWalletId,
)
// selector
.onRight { customerInfo ->
when {
!customerInfo.isKycApproved -> {
when (params) {
is TangemPayOnboardingComponent.Params.Deeplink -> showOnboarding()
else -> openKyc()
private fun checkCustomerInfo(userWalletId: UserWalletId) {
modelScope.launch {
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
repository.getCustomerInfo(
userWalletId = userWalletId,
)
.onRight { customerInfo ->
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
when {
!customerInfo.isKycApproved -> {
when (params) {
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> openKyc(userWalletId)
else -> startOnboarding(userWalletId)
}
}
else -> back()
}
else -> back()
}
}
.onLeft { back() }
}
private fun getUserWalletForPay(userWalletId: UserWalletId?): UserWalletId {
val userWallet = userWalletId?.let { getUserWalletUseCase(it).getOrNull() }
return if (userWallet?.isMultiCurrency == true) {
userWallet.walletId
} else {
tryGetSelectedWalletId()
}
}
private fun tryGetSelectedWalletId(): UserWalletId {
val selectedWallet = getSelectedWalletUseCase.sync().getOrNull()
return if (selectedWallet?.isMultiCurrency == true) {
selectedWallet.walletId
} else {
tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId
.onLeft { back() }
}
}
@ -122,21 +113,52 @@ internal class TangemPayOnboardingModel @Inject constructor(
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
}
@Suppress("NullableToStringCall")
private fun onGetCardClick() {
analytics.send(TangemPayAnalyticsEvents.GetCardClicked())
modelScope.launch {
val eligibleWalletsIds = eligibilityManager.getEligibleWallets().map { it.walletId }
if (eligibleWalletsIds.isEmpty()) {
back()
return@launch
}
when (params) {
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
checkCustomerInfo(params.userWalletId)
}
is TangemPayOnboardingComponent.Params.FromBannerOnMain -> {
if (eligibleWalletsIds.any { walletId -> walletId == params.userWalletId }) {
checkCustomerInfo(userWalletId = params.userWalletId)
} else {
openWalletSelectorIfNeeds(eligibleWalletsIds)
}
}
is TangemPayOnboardingComponent.Params.Deeplink,
is TangemPayOnboardingComponent.Params.FromBannerInSettings,
-> {
openWalletSelectorIfNeeds(eligibleWalletsIds)
}
}
}
}
private fun openWalletSelectorIfNeeds(eligibleWalletsIds: List<UserWalletId>) {
if (eligibleWalletsIds.size == 1) {
checkCustomerInfo(userWalletId = eligibleWalletsIds[0])
} else {
bottomSheetNavigation.activate(TangemPayOnboardingNavigation.WalletSelector(eligibleWalletsIds))
}
}
private fun startOnboarding(userWalletId: UserWalletId) {
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
modelScope.launch {
// TODO implement selector
val userWalletId = getUserWalletForPay(params.userWalletId)
val result = produceInitialDataUseCase(userWalletId)
if (result.isLeft()) {
Timber.e("Error producing initial data: ${result.leftOrNull()?.message}")
val errorMessage = result.leftOrNull()?.message ?: "Unknown error"
Timber.e("Error producing initial data: $errorMessage")
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
return@launch
}
// TODO implement selector
repository.getCustomerInfo(
userWalletId = userWalletId,
).fold(
@ -148,20 +170,17 @@ internal class TangemPayOnboardingModel @Inject constructor(
if (customerInfo.isKycApproved) {
back()
} else {
openKyc()
openKyc(userWalletId)
}
},
)
}
}
private fun openKyc() {
// TODO implement selector
private fun openKyc(userWalletId: UserWalletId) {
router.replaceAll(
AppRoute.Wallet,
AppRoute.Kyc(
userWalletId = getUserWalletForPay(params.userWalletId),
),
AppRoute.Kyc(userWalletId = userWalletId),
)
}
@ -172,4 +191,13 @@ internal class TangemPayOnboardingModel @Inject constructor(
private fun getInitialState(): TangemPayOnboardingScreenState {
return TangemPayOnboardingScreenState.Loading(onBack = ::back)
}
override fun onWalletSelected(userWalletId: UserWalletId) {
bottomSheetNavigation.dismiss()
checkCustomerInfo(userWalletId)
}
override fun onWalletSelectorDismiss() {
bottomSheetNavigation.dismiss()
}
}

View file

@ -0,0 +1,73 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.features.tangempay.components.TangemPayWalletSelectorComponent
import com.tangem.features.tangempay.ui.WalletSelectorBSContentUM
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayWalletSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
messageSender: UiMessageSender,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<TangemPayWalletSelectorComponent.Params>()
private val userWalletsFetcher = userWalletsFetcherFactory.create(
messageSender = messageSender,
onlyMultiCurrency = true,
isAuthMode = false,
isClickableIfLocked = false,
onWalletClick = { params.listener.onWalletSelected(it) },
)
init {
fetchUserWalletsUM()
}
val uiState: StateFlow<WalletSelectorBSContentUM>
field = MutableStateFlow(getInitialState())
private fun getInitialState(): WalletSelectorBSContentUM {
return WalletSelectorBSContentUM(
userWallets = persistentListOf(),
onDismiss = { params.listener.onWalletSelectorDismiss() },
)
}
private fun fetchUserWalletsUM() {
modelScope.launch {
val eligibleWalletsIds = params.walletsIds.map { it.stringValue }.toSet()
userWalletsFetcher.userWallets.collectLatest { userWalletsListUM ->
uiState.update {
WalletSelectorBSContentUM(
userWallets = userWalletsListUM
.filter { wallet -> wallet.id in eligibleWalletsIds }
.toImmutableList(),
onDismiss = { params.listener.onWalletSelectorDismiss() },
)
}
}
}
}
fun onDismiss() {
params.listener.onWalletSelectorDismiss()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.tangempay.ui
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
internal sealed class TangemPayOnboardingNavigation {
@Serializable
data class WalletSelector(
val walletsIds: List<UserWalletId>,
) : TangemPayOnboardingNavigation()
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.tangempay.ui
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import kotlinx.collections.immutable.ImmutableList
internal data class WalletSelectorBSContentUM(
val userWallets: ImmutableList<UserWalletItemUM>,
val onDismiss: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,124 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.onboarding.impl.R
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun WalletSelectorBottomSheet(state: WalletSelectorBSContentUM) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = state.onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = state.onDismiss,
containerColor = TangemTheme.colors.background.tertiary,
title = { content ->
TangemTopAppBar(
title = resourceReference(R.string.common_choose_wallet),
titleAlignment = Alignment.CenterHorizontally,
endButton = TopAppBarButtonUM.Close(onCloseClick = state.onDismiss),
)
},
) { content ->
Content(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing8,
),
state = state,
)
}
}
@Composable
private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
BlockCard(
modifier = Modifier.fillMaxSize(),
colors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
) {
state.userWallets.forEach { state ->
key(state.id) {
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
blockColors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
state = state,
)
}
}
}
SpacerH(bottomBarHeight)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewWalletSelectorBottomSheet() {
TangemThemePreview {
WalletSelectorBottomSheet(
state = WalletSelectorBSContentUM(
userWallets = persistentListOf(
UserWalletItemUM(
id = "1",
name = stringReference("Wallet 1"),
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
balance = UserWalletItemUM.Balance.Loading,
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.None,
onClick = {},
),
UserWalletItemUM(
id = "2",
name = stringReference("Wallet 2"),
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
balance = UserWalletItemUM.Balance.Loading,
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.None,
onClick = {},
),
),
onDismiss = {},
),
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.child.wallet.model.intents
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.res.R
@ -14,6 +15,7 @@ import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer
import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -29,6 +31,10 @@ internal interface TangemPayIntents {
fun onIssuingFailedClicked()
fun onPaySupportClick()
fun onOnboardingBannerClick(userWalletId: UserWalletId)
fun onOnboardingBannerCloseClick(userWalletId: UserWalletId)
}
@Suppress("LongParameterList")
@ -115,4 +121,15 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
)
}
}
override fun onOnboardingBannerClick(userWalletId: UserWalletId) {
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId))
}
override fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) {
modelScope.launch {
stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId))
onboardingRepository.setHideMainOnboardingBanner(userWalletId)
}
}
}

View file

@ -111,14 +111,8 @@ internal class DefaultWalletRouter @Inject constructor(
)
}
override fun openTangemPayOnboarding(userWalletId: UserWalletId) {
router.push(
AppRoute.TangemPayOnboarding(
AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(
userWalletId = userWalletId,
),
),
)
override fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode) {
router.push(route = AppRoute.TangemPayOnboarding(mode = mode))
}
override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.common.routing.AppRoute
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -63,7 +64,7 @@ internal interface InnerWalletRouter {
fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig)
fun openTangemPayOnboarding(userWalletId: UserWalletId)
fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode)
fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)

View file

@ -11,6 +11,11 @@ internal sealed class TangemPayState {
data object Loading : TangemPayState()
data class OnboardingBanner(
val onClick: () -> Unit,
val closeOnClick: () -> Unit,
) : TangemPayState()
data class Progress(
val title: TextReference,
val description: TextReference,

View file

@ -0,0 +1,18 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayHideOnboardingStateTransformer(
userWalletId: UserWalletId,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(tangemPayState = TangemPayState.Empty)
} else {
prevState
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayOnboardingBannerStateTransformer(
userWalletId: UserWalletId,
private val onClick: (UserWalletId) -> Unit,
private val closeOnClick: (UserWalletId) -> Unit,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(
tangemPayState = TangemPayState.OnboardingBanner(
onClick = { onClick(userWalletId) },
closeOnClick = { closeOnClick(userWalletId) },
),
)
} else {
prevState
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.common.routing.AppRoute
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.MainCustomerInfoContentState
@ -79,6 +80,13 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
updateTangemPay(data = state.info, userWalletId = userWalletId)
analytics.send(customerInfo = state.info)
}
is MainCustomerInfoContentState.OnboardingBanner -> stateController.update(
transformer = TangemPayOnboardingBannerStateTransformer(
userWalletId = userWalletId,
onClick = clickIntents::onOnboardingBannerClick,
closeOnClick = clickIntents::onOnboardingBannerCloseClick,
),
)
}
}
@ -91,7 +99,11 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
userWalletId = userWalletId,
value = data,
cardFrozenState = cardFrozenState,
onClickKyc = { innerWalletRouter.openTangemPayOnboarding(userWalletId) },
onClickKyc = {
innerWalletRouter.openTangemPayOnboarding(
mode = AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId),
)
},
onIssuingCard = clickIntents::onIssuingCardClicked,
onIssuingFailed = clickIntents::onIssuingFailedClicked,
openDetails = { config ->

View file

@ -23,6 +23,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier)
is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier)
is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier)
is TangemPayState.OnboardingBanner -> TangemPayOnboardingBanner(state, modifier)
TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier)
}
}

View file

@ -0,0 +1,129 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
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.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
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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 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
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
private const val GRADIENT_START_COLOR = 0xFF252934
private const val GRADIENT_END_COLOR = 0xFF12141E
private const val GRADIENT_OFFSET_X = 0f
private const val GRADIENT_OFFSET_Y = 80F
private const val GRADIENT_RADIUS = 200F
@Composable
internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
brush = Brush.radialGradient(
colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)),
center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y),
radius = GRADIENT_RADIUS,
),
)
.clickable(onClick = state.onClick),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
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()
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResourceSafe(R.string.tangempay_onboarding_banner_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
SpacerH(6.dp)
Text(
text = stringResourceSafe(R.string.tangempay_onboarding_banner_description),
style = TangemTheme.typography.caption2,
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,
)
}
}
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewTangemOnboardingBanner() {
TangemThemePreview {
TangemPayOnboardingBanner(
TangemPayState.OnboardingBanner(
onClick = {},
closeOnClick = {},
),
)
}
}