Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-20 21:03:08 +05:00
parent a41dd9aace
commit 41fb5d5bcc
55 changed files with 709 additions and 173 deletions

View file

@ -7,6 +7,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
@ -378,6 +379,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
@ -388,6 +390,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)

View file

@ -221,7 +221,7 @@ internal class CardSettingsModel @Inject constructor(
val card = scanResponse.card
modelScope.launch {
val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true
val hasTangemPay = onboardingRepository.hasTangemPayInWallet(userWalletId).getOrNull() == true
store.dispatchNavigationAction {
push(
route = AppRoute.ResetToFactory(

View file

@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class EditAccount(
val account: Account,
val account: Account.CryptoPortfolio,
) : AppRoute(path = "/edit_account/${account.accountId.value}")
@Serializable
data class AccountDetails(
val account: Account,
val account: Account.CryptoPortfolio,
) : AppRoute(path = "/account_details/${account.accountId.value}")
@Serializable

View file

@ -59,5 +59,9 @@
{
"name": "GASLESS_APPROVAL_ENABLED",
"version": "undefined"
},
{
"name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED",
"version": "undefined"
}
]

View file

@ -12,7 +12,7 @@ data class OrderResponse(
@Json(name = "id") val id: String,
@Json(name = "customer_id") val customerId: String?,
@Json(name = "type") val type: String?,
@Json(name = "status") val status: String,
@Json(name = "status") val status: Status,
@Json(name = "step") val step: String?,
@Json(name = "data") val data: Data,
@Json(name = "step_change_code") val stepChangeCode: Int?,
@ -29,5 +29,20 @@ data class OrderResponse(
@Json(name = "payment_account_id") val paymentAccountId: String?,
@Json(name = "transaction_hash") val transactionHash: String?,
)
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "NEW")
NEW,
@Json(name = "PROCESSING")
PROCESSING,
@Json(name = "COMPLETED")
COMPLETED,
@Json(name = "CANCELED")
CANCELED,
}
}
}

View file

@ -104,7 +104,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
map { wallet ->
async {
val isCustomer = onboardingRepository
.checkCustomerWallet(wallet.walletId)
.hasTangemPayInWallet(wallet.walletId)
.getOrNull() == true
wallet to isCustomer
}

View file

@ -2,12 +2,17 @@ package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer
import com.tangem.data.pay.repository.*
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
@ -75,7 +80,29 @@ internal interface TangemPayDataModule {
@Singleton
fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager
@Binds
@Singleton
fun bindPaymentAccountStatusProducerFactory(
impl: DefaultPaymentAccountStatusProducer.Factory,
): PaymentAccountStatusProducer.Factory
@Binds
@Singleton
fun bindPaymentAccountStatusFetcher(impl: DefaultPaymentAccountStatusFetcher): PaymentAccountStatusFetcher
companion object {
@Provides
@Singleton
fun providePaymentAccountStatusSupplier(
factory: PaymentAccountStatusProducer.Factory,
): PaymentAccountStatusSupplier {
return object : PaymentAccountStatusSupplier(
factory = factory,
keyCreator = { "payment_account_status_${it.userWalletId.stringValue}" },
) {}
}
@Provides
@Singleton
fun provideTangemPayMainScreenCustomerInfoUseCase(

View file

@ -0,0 +1,178 @@
package com.tangem.data.pay.flow
import arrow.core.Either
import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.PaymentAccountStatus
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import timber.log.Timber
import javax.inject.Inject
private const val TAG = "PaymentAccountStatusFetcher"
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
private val onboardingRepository: OnboardingRepository,
private val customerOrderRepository: CustomerOrderRepository,
private val deviceSecurity: DeviceSecurityInfoProvider,
private val dispatchers: CoroutineDispatcherProvider,
) : PaymentAccountStatusFetcher {
override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either<Throwable, Unit> =
eitherOn(dispatchers.default) {
Timber.tag(TAG).i("fetch: ${params.userWalletId.stringValue}")
if (deviceSecurity.isSecurityExposed()) {
Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}")
Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}")
Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
return@eitherOn paymentAccountStatusesStore.store(
userWalletId = params.userWalletId,
status = PaymentAccountStatus.Error.ExposedDevice,
)
}
val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId)
.fold(
ifLeft = { error ->
Timber.tag(TAG).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}")
when (error) {
is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated
else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL)
}
},
ifRight = { hasTangemPay ->
proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay)
},
)
Timber.tag(TAG).i("invoke status ${params.userWalletId}: $status")
paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status)
}
private suspend fun proceedHasTangemPayResult(
userWalletId: UserWalletId,
hasTangemPay: Boolean,
): PaymentAccountStatus {
Timber.tag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay")
return if (hasTangemPay) {
fetchTangemPayAccountStatus(userWalletId = userWalletId)
} else {
PaymentAccountStatus.NotCreated
}
}
private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus {
val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId)
if (prevResult == null || prevResult is PaymentAccountStatus.Error) {
paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading)
}
return proceedWithOrderId(userWalletId = userWalletId)
}
private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus {
return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) {
PaymentAccountStatus.Error.NotSynced
} else {
val orderId = onboardingRepository.getOrderId(userWalletId)
if (orderId != null) {
proceedWithOrderId(userWalletId = userWalletId, orderId = orderId)
} else {
proceedWithoutOrder(userWalletId = userWalletId)
}
}
}
private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus {
return onboardingRepository.getCustomerInfo(userWalletId).fold(
ifLeft = { error ->
Timber.tag(TAG).e("proceedWithoutOrder $userWalletId error: $error")
error.mapToPaymentAccountStatus()
},
ifRight = { customerInfo ->
Timber.tag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId")
val status = customerInfo.mapToPaymentAccountStatus()
if (status is PaymentAccountStatus.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) {
// If order id wasn't saved -> start order creation and get customer info
onboardingRepository.createOrder(userWalletId)
}
status
},
)
}
private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus {
return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold(
ifLeft = { error ->
Timber.tag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error")
error.mapToPaymentAccountStatus()
},
ifRight = { orderData ->
Timber.tag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}")
when (orderData.status) {
// Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.NEW,
OrderStatus.PROCESSING,
-> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL)
OrderStatus.CANCELED -> {
// If order was cancelled -> clear previous order from local storage and start order creation
onboardingRepository.clearOrderId(userWalletId)
onboardingRepository.createOrder(userWalletId)
PaymentAccountStatus.Error.CardIssueFailed
}
OrderStatus.COMPLETED -> {
// Order was completed -> clear order id and get customer info
onboardingRepository.clearOrderId(userWalletId)
onboardingRepository.getCustomerInfo(userWalletId = userWalletId)
.fold(
ifLeft = { it.mapToPaymentAccountStatus() },
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() },
)
}
OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL)
}
},
)
}
private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus {
val cardInfo = this.cardInfo
val productInstance = this.productInstance
return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) {
PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus)
} else if (cardInfo != null && productInstance != null) {
PaymentAccountStatus.Loaded(
source = StatusSource.ACTUAL,
cardId = productInstance.cardId,
lastFourDigits = cardInfo.lastFourDigits,
balance = cardInfo.balance,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
isPinSet = cardInfo.isPinSet,
)
} else {
PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL)
}
}
private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus {
return when (this) {
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced
is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated
else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL)
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.data.pay.flow
import arrow.core.Option
import arrow.core.some
import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.StatusSource
import com.tangem.domain.pay.PaymentAccountStatus
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEmpty
internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor(
@Assisted private val params: PaymentAccountStatusProducer.Params,
override val flowProducerTools: FlowProducerTools,
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : PaymentAccountStatusProducer {
override val fallback: Option<PaymentAccountStatus>
get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some()
override fun produce(): Flow<PaymentAccountStatus> {
return paymentAccountStatusesStore.get(userWalletId = params.userWalletId)
.onEmpty { emit(value = PaymentAccountStatus.NotCreated) }
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory : PaymentAccountStatusProducer.Factory {
override fun create(params: PaymentAccountStatusProducer.Params): DefaultPaymentAccountStatusProducer
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.pay.model.OrderStatus
@ -19,11 +20,11 @@ internal class DefaultCustomerOrderRepository @Inject constructor(
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
}.map { response ->
val status = when (response.result?.status) {
null -> OrderStatus.UNKNOWN
OrderStatus.NEW.apiName -> OrderStatus.NEW
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
else -> OrderStatus.CANCELED
null -> OrderStatus.PROCESSING
OrderResponse.Result.Status.NEW -> OrderStatus.NEW
OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING
OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
}
OrderData(
status = status,

View file

@ -10,6 +10,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.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
@ -27,9 +28,6 @@ import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
private const val VALID_STATUS = "valid"
private const val APPROVED_KYC_STATUS = "approved"
private const val IN_PROGRESS_KYC_STATUS = "in_progress"
private const val DECLINED_KYC_STATUS = "declined"
private const val TAG = "TangemPay: OnboardingRepository"
@Suppress("LongParameterList")
@ -145,7 +143,6 @@ internal class DefaultOnboardingRepository @Inject constructor(
lastFourDigits = card.cardNumberEnd,
balance = fiatBalance.availableBalance,
currencyCode = fiatBalance.currency,
customerWalletAddress = paymentAccount.customerWalletAddress,
depositAddress = response.depositAddress,
isPinSet = response.card?.isPinSet == true,
)
@ -159,19 +156,19 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState)
ProductInstance(id = instance.id, cardId = instance.cardId, cardFrozenState = cardFrozenState)
ProductInstance(id = instance.id, cardId = instance.cardId)
}
return CustomerInfo(
customerId = response?.id,
productInstance = productInstance,
kycStatus = getKycStatus(status = response?.kyc?.status),
kycStatus = KycStatus.fromString(status = response?.kyc?.status),
cardInfo = cardInfo,
).also {
lastFetchedCustomerInfoMap[userWalletId] = it
}
}
override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> {
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> {
val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId)
if (hasTangemPay != null) {
return Either.Right(hasTangemPay)
@ -228,13 +225,4 @@ internal class DefaultOnboardingRepository @Inject constructor(
setHideMainOnboardingBanner(userWalletId)
}
}
private fun getKycStatus(status: String?): CustomerInfo.KycStatus {
return when (status?.lowercase()) {
IN_PROGRESS_KYC_STATUS -> CustomerInfo.KycStatus.PENDING
DECLINED_KYC_STATUS -> CustomerInfo.KycStatus.REJECTED
APPROVED_KYC_STATUS -> CustomerInfo.KycStatus.APPROVED
else -> CustomerInfo.KycStatus.INIT
}
}
}

View file

@ -16,10 +16,10 @@ import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest
import com.tangem.datasource.api.pay.models.request.SetPinRequest
import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse
import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
@ -279,15 +279,15 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
orderStatus.onRight { response ->
val status = response.result?.status
if (status == OrderStatus.COMPLETED.apiName || status == OrderStatus.CANCELED.apiName) {
if (status == Status.COMPLETED || status == Status.CANCELED) {
// Remove from jobs
pollingJobs.remove(key = orderId)
// Final card state
val finalState = when {
status == OrderStatus.COMPLETED.apiName && isFreeze
status == Status.COMPLETED && isFreeze
-> TangemPayCardFrozenState.Frozen
status == OrderStatus.COMPLETED.apiName && !isFreeze
status == Status.COMPLETED && !isFreeze
-> TangemPayCardFrozenState.Unfrozen
else -> return@launch
}

View file

@ -77,34 +77,22 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
limit: Int,
): List<TangemPayTxHistoryItem> {
cacheRegistry.invokeOnExpire(
key = getCacheKey(customerWalletAddress = config.customerWalletAddress, cursor = cursor),
key = getCacheKey(userWalletId = userWalletId, cursor = cursor),
skipCache = config.shouldRefresh,
block = {
fetch(
userWalletId = userWalletId,
customerWalletAddress = config.customerWalletAddress,
cursor = cursor,
pageSize = limit,
)
},
block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) },
)
return txHistoryItemsStore.getSyncOrNull(
key = config.customerWalletAddress,
key = userWalletId.stringValue,
cursor = cursor ?: INITIAL_CURSOR,
).orEmpty()
}
private fun getCacheKey(customerWalletAddress: String, cursor: String?): String {
return "tangem_pay_tx_history_${customerWalletAddress}_${cursor ?: INITIAL_CURSOR}"
private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String {
return "tangem_pay_tx_history_${userWalletId.stringValue}_${cursor ?: INITIAL_CURSOR}"
}
private suspend fun fetch(
userWalletId: UserWalletId,
customerWalletAddress: String,
cursor: String?,
pageSize: Int,
) {
private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) {
requestPerformer.performRequest(userWalletId = userWalletId) { authHeader ->
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
}.onLeft {
@ -112,7 +100,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
}.onRight { response ->
val result = response.result
val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull()
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
txHistoryItemsStore.store(key = userWalletId.stringValue, cursor = cursor ?: INITIAL_CURSOR, value = items)
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.pay.store
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.PaymentAccountStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import javax.inject.Inject
import javax.inject.Singleton
@Suppress("UnusedParameter", "EmptyFunctionBlock", "FunctionOnlyReturningConstant")
@Singleton
internal class PaymentAccountStatusesStore @Inject constructor() {
fun get(userWalletId: UserWalletId): Flow<PaymentAccountStatus> {
return emptyFlow()
}
fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? {
return null
}
fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) {
}
}

View file

@ -22,4 +22,8 @@ abstract class SingleAccountSupplier(
fun filterPaymentAccount(accountId: AccountId): Flow<Account.Payment> {
return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance()
}
fun filterCryptoPortfolioAccount(accountId: AccountId): Flow<Account.CryptoPortfolio> {
return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance()
}
}

1
domain/kyc/models/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
implementation(deps.kotlin.serialization)
}

View file

@ -0,0 +1,33 @@
package com.tangem.domain.models.kyc
private const val APPROVED_KYC_STATUS = "approved"
private const val IN_PROGRESS_KYC_STATUS = "in_progress"
private const val DECLINED_KYC_STATUS = "declined"
enum class KycStatus {
/** Initial state */
INIT,
/** Performing the check */
PENDING,
/** SumSub approved */
APPROVED,
/** The check failed, documents rejected */
REJECTED,
;
companion object {
fun fromString(status: String?, default: KycStatus = INIT): KycStatus {
return when (status?.lowercase()) {
IN_PROGRESS_KYC_STATUS -> PENDING
DECLINED_KYC_STATUS -> REJECTED
APPROVED_KYC_STATUS -> APPROVED
else -> default
}
}
}
}

View file

@ -21,6 +21,7 @@ dependencies {
implementation(projects.domain.walletManager)
implementation(projects.domain.card)
implementation(projects.domain.staking)
implementation(projects.domain.visa)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)

View file

@ -9,4 +9,5 @@ enum class FetchingSource {
NETWORK,
QUOTE,
STAKING,
TANGEM_PAY,
}

View file

@ -2,11 +2,13 @@ package com.tangem.domain.tokens.wallet
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.right
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
@ -45,6 +47,7 @@ class WalletBalanceFetcher internal constructor(
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val stakingIdFactory: StakingIdFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : FlowFetcher<WalletBalanceFetcher.Params> {
@ -57,6 +60,7 @@ class WalletBalanceFetcher internal constructor(
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
) : this(
@ -72,6 +76,7 @@ class WalletBalanceFetcher internal constructor(
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)
@ -91,10 +96,18 @@ class WalletBalanceFetcher internal constructor(
error("UserWallet doesn't contain crypto-currencies: $userWalletId")
}
fetcher.fetch(userWalletId = userWalletId, currencies = currencies)
fetcher.fetch(
userWalletId = userWalletId,
currencies = currencies,
paymentAccountRefactorEnabled = params.isPaymentAccountRefactorEnabled,
)
}
private suspend fun BaseWalletBalanceFetcher.fetch(userWalletId: UserWalletId, currencies: Set<CryptoCurrency>) {
private suspend fun BaseWalletBalanceFetcher.fetch(
userWalletId: UserWalletId,
currencies: Set<CryptoCurrency>,
paymentAccountRefactorEnabled: Boolean,
) {
coroutineScope {
val results = fetchingSources.map { source ->
async {
@ -102,6 +115,10 @@ class WalletBalanceFetcher internal constructor(
FetchingSource.NETWORK -> fetchNetworks(userWalletId = userWalletId, currencies = currencies)
FetchingSource.QUOTE -> fetchQuotes(currencies = currencies)
FetchingSource.STAKING -> fetchStaking(userWalletId = userWalletId, currencies = currencies)
FetchingSource.TANGEM_PAY -> fetchPaymentAccount(
userWalletId = userWalletId,
paymentAccountRefactorEnabled = paymentAccountRefactorEnabled,
)
}
source to maybeResult
@ -173,10 +190,19 @@ class WalletBalanceFetcher internal constructor(
}
}
private suspend fun fetchPaymentAccount(
userWalletId: UserWalletId,
paymentAccountRefactorEnabled: Boolean,
): Either<Throwable, Unit> {
if (!paymentAccountRefactorEnabled) return Unit.right()
return paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
}
/**
* Params of [WalletBalanceFetcher]
*
* @property userWalletId user wallet id
*/
data class Params(val userWalletId: UserWalletId)
data class Params(val userWalletId: UserWalletId, val isPaymentAccountRefactorEnabled: Boolean)
}

View file

@ -27,6 +27,7 @@ internal class MultiWalletBalanceFetcher(
FetchingSource.NETWORK,
FetchingSource.QUOTE,
FetchingSource.STAKING,
FetchingSource.TANGEM_PAY,
)
override suspend fun getCryptoCurrencies(userWalletId: UserWalletId): Set<CryptoCurrency> {

View file

@ -9,6 +9,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.model.StakingIntegrationID
@ -42,6 +43,7 @@ internal class WalletBalanceFetcherTest {
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk()
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk()
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk()
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
private val stakingIdFactory: StakingIdFactory = mockk()
private val fetcher = WalletBalanceFetcher(
@ -52,6 +54,7 @@ internal class WalletBalanceFetcherTest {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -76,7 +79,12 @@ internal class WalletBalanceFetcherTest {
every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } throws exception
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = exception.left()
@ -107,7 +115,12 @@ internal class WalletBalanceFetcherTest {
every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = IllegalStateException("Unknown type of wallet: $userWalletId").left()
@ -139,7 +152,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } throws exception
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = exception.left()
@ -171,7 +189,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns emptySet()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = IllegalStateException("UserWallet doesn't contain crypto-currencies: $userWalletId").left()
@ -213,7 +236,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = IllegalStateException(
@ -259,7 +287,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = IllegalStateException(
@ -311,7 +344,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = IllegalStateException(
@ -354,7 +392,12 @@ internal class WalletBalanceFetcherTest {
} returns Either.Left(StakingIdFactory.Error.UnsupportedCurrency)
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
assertEitherRight(actual)
@ -396,7 +439,12 @@ internal class WalletBalanceFetcherTest {
coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns stakingId
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
assertEitherRight(actual)
@ -444,7 +492,12 @@ internal class WalletBalanceFetcherTest {
} returns stellarStakingId
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
assertEitherRight(actual)
@ -506,7 +559,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = IllegalStateException(
@ -572,7 +630,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns Unit.right()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = Unit.right()
@ -624,7 +687,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = Unit.right()
@ -674,7 +742,12 @@ internal class WalletBalanceFetcherTest {
coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right()
// Act
val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
val actual = fetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = false
)
)
// Assert
val expected = Unit.right()

View file

@ -47,7 +47,12 @@ class MultiWalletBalanceFetcherTest {
val actual = fetcher.fetchingSources
// Assert
val expected = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING)
val expected = setOf(
FetchingSource.NETWORK,
FetchingSource.QUOTE,
FetchingSource.STAKING,
FetchingSource.TANGEM_PAY,
)
Truth.assertThat(actual).isEqualTo(expected)
}

View file

@ -24,8 +24,6 @@ dependencies {
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.features.swap.domain)
/** Security */
implementation(deps.spongecastle.core)

View file

@ -0,0 +1,66 @@
package com.tangem.domain.pay
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
@Serializable
sealed class PaymentAccountStatus {
abstract val source: StatusSource
@Serializable
data object Loading : PaymentAccountStatus() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data object NotCreated : PaymentAccountStatus() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data class UnderReview(
override val source: StatusSource,
val kycStatus: KycStatus,
) : PaymentAccountStatus()
@Serializable
data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus()
@Serializable
data class Locked(override val source: StatusSource) : PaymentAccountStatus()
@Serializable
data class Loaded(
override val source: StatusSource,
val cardId: String,
val lastFourDigits: String,
val balance: SerializedBigDecimal,
val currencyCode: String,
val depositAddress: String?,
val isPinSet: Boolean,
) : PaymentAccountStatus()
@Serializable
sealed class Error : PaymentAccountStatus() {
@Serializable
data object ExposedDevice : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data class Unavailable(override val source: StatusSource) : Error()
@Serializable
data object NotSynced : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data object CardIssueFailed : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
}
}

View file

@ -9,7 +9,6 @@ data class TangemPayDetailsConfig(
val cardId: String,
val isPinSet: Boolean,
val cardFrozenState: TangemPayCardFrozenState,
val customerWalletAddress: String,
val cardNumberEnd: String,
val chainId: Int,
)

View file

@ -0,0 +1,8 @@
package com.tangem.domain.pay.flow
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.models.wallet.UserWalletId
interface PaymentAccountStatusFetcher : FlowFetcher<PaymentAccountStatusFetcher.Params> {
data class Params(val userWalletId: UserWalletId)
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.pay.flow
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.PaymentAccountStatus
interface PaymentAccountStatusProducer : FlowProducer<PaymentAccountStatus> {
data class Params(val userWalletId: UserWalletId)
interface Factory : FlowProducer.Factory<Params, PaymentAccountStatusProducer>
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.pay.flow
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.pay.PaymentAccountStatus
@Suppress("UnnecessaryAbstractClass")
abstract class PaymentAccountStatusSupplier(
override val factory: PaymentAccountStatusProducer.Factory,
override val keyCreator: (PaymentAccountStatusProducer.Params) -> String,
) : FlowCachingSupplier<PaymentAccountStatusProducer, PaymentAccountStatusProducer.Params, PaymentAccountStatus>()

View file

@ -1,6 +1,6 @@
package com.tangem.domain.pay.model
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.domain.models.kyc.KycStatus
import java.math.BigDecimal
sealed class MainCustomerInfoContentState {
@ -22,31 +22,15 @@ data class CustomerInfo(
val cardInfo: CardInfo?,
) {
enum class KycStatus {
/** Initial state */
INIT,
/** Performing the check */
PENDING,
/** SumSub approved */
APPROVED,
/** The check failed, documents rejected */
REJECTED,
}
data class ProductInstance(
val id: String,
val cardId: String,
val cardFrozenState: TangemPayCardFrozenState,
)
data class CardInfo(
val lastFourDigits: String,
val balance: BigDecimal,
val currencyCode: String,
val customerWalletAddress: String,
val depositAddress: String?,
val isPinSet: Boolean,
)

View file

@ -1,9 +1,9 @@
package com.tangem.domain.pay.model
enum class OrderStatus(val apiName: String) {
UNKNOWN(""),
NEW("NEW"),
PROCESSING("PROCESSING"),
COMPLETED("COMPLETED"),
CANCELED("CANCELED"),
enum class OrderStatus {
UNKNOWN, // TODO remove it after TangemPay accounts refactor TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED
NEW,
PROCESSING,
COMPLETED,
CANCELED,
}

View file

@ -23,7 +23,7 @@ interface OnboardingRepository {
suspend fun getOrderId(userWalletId: UserWalletId): String?
suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
suspend fun checkCustomerEligibility(): Boolean
suspend fun getCustomerEligibility(): Boolean

View file

@ -3,6 +3,7 @@ package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
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.*
@ -38,7 +39,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
return // fast exit
}
onboardingRepository.checkCustomerWallet(userWalletId)
onboardingRepository.hasTangemPayInWallet(userWalletId)
.fold(
ifLeft = { error ->
Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
@ -125,7 +126,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
}
.map { customerInfo ->
Timber.tag(TAG).i("customerInfo")
if (customerInfo.cardInfo == null && customerInfo.kycStatus == CustomerInfo.KycStatus.APPROVED) {
if (customerInfo.cardInfo == null && customerInfo.kycStatus == KycStatus.APPROVED) {
// If order id wasn't saved -> start order creation and get customer info
onboardingRepository.createOrder(userWalletId)
}
@ -151,7 +152,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
info = CustomerInfo(
customerId = null,
productInstance = null,
kycStatus = CustomerInfo.KycStatus.APPROVED,
kycStatus = KycStatus.APPROVED,
cardInfo = null,
),
orderStatus = orderData.status,

View file

@ -1,3 +1,3 @@
package com.tangem.domain.tangempay.model
data class TangemPayTxHistoryListConfig(val customerWalletAddress: String, val shouldRefresh: Boolean)
data class TangemPayTxHistoryListConfig(val shouldRefresh: Boolean)

View file

@ -15,7 +15,7 @@ interface AccountCreateEditComponent : ComposableContentComponent {
) : Params
data class Edit(
val account: Account,
val account: Account.CryptoPortfolio,
) : Params
}
}

View file

@ -7,5 +7,5 @@ import com.tangem.domain.models.account.Account
interface AccountDetailsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, AccountDetailsComponent>
data class Params(val account: Account)
data class Params(val account: Account.CryptoPortfolio)
}

View file

@ -25,7 +25,6 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
@ -73,7 +72,7 @@ internal class AccountCreateEditModel @Inject constructor(
when (params) {
is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId)
is AccountCreateEditComponent.Params.Edit -> {
val derivationIndex = params.account.derivationIndex?.value
val derivationIndex = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex)
analyticsEventHandler.send(event)
}
@ -170,7 +169,7 @@ internal class AccountCreateEditModel @Inject constructor(
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
val isNewName = name != params.account.accountName
val isNewIcon = icon != params.account.portfolioIcon
val derivationIndex = params.account.derivationIndex?.value
val derivationIndex = params.account.derivationIndex.value
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex))
uiState.value = uiState.value.toggleProgress(showProgress = true)
@ -182,7 +181,7 @@ internal class AccountCreateEditModel @Inject constructor(
uiState.value = uiState.value.toggleProgress(showProgress = false)
result
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) }
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex.value) }
.onRight {
showMessage(R.string.account_edit_success_message)
router.pop()

View file

@ -14,12 +14,10 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.account.AccountDetailsComponent
@ -54,29 +52,29 @@ internal class AccountDetailsModel @Inject constructor(
init {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened())
singleAccountSupplier(SingleAccountProducer.Params(accountId))
singleAccountSupplier.filterCryptoPortfolioAccount(accountId)
.onEach { account -> uiState.update { buildUI(account) } }
.launchIn(modelScope)
}
private fun onEditAccountClick(account: Account) {
private fun onEditAccountClick(account: Account.CryptoPortfolio) {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit())
router.push(AppRoute.EditAccount(account))
}
private fun onManageTokensClick(account: Account) {
private fun onManageTokensClick(account: Account.CryptoPortfolio) {
val route = AppRoute.ManageTokens(
source = AppRoute.ManageTokens.Source.ACCOUNT,
portfolioId = PortfolioId(account.accountId),
)
analyticsEventHandler.send(
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value),
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value),
)
router.push(route)
}
private fun onArchiveAccountClick() {
val accountDerivation = params.account.derivationIndex?.value
val accountDerivation = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation)
analyticsEventHandler.send(event)
confirmArchiveDialog()
@ -86,7 +84,7 @@ internal class AccountDetailsModel @Inject constructor(
val secondAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {
val accountDerivation = params.account.derivationIndex?.value
val accountDerivation = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation)
analyticsEventHandler.send(event)
},
@ -107,7 +105,7 @@ internal class AccountDetailsModel @Inject constructor(
}
private fun archiveCryptoPortfolio() = modelScope.launch {
val accountDerivation = params.account.derivationIndex?.value
val accountDerivation = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation)
analyticsEventHandler.send(event)
uiState.update { it.toggleProgress(true) }
@ -128,7 +126,7 @@ internal class AccountDetailsModel @Inject constructor(
val event = AccountSettingsAnalyticEvents.AccountError(
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
error = error.tag,
accountDerivation = params.account.derivationIndex?.value,
accountDerivation = params.account.derivationIndex.value,
)
analyticsEventHandler.send(event)
val titleRes: Int
@ -155,16 +153,13 @@ internal class AccountDetailsModel @Inject constructor(
messageSender.send(dialogMessage)
}
private fun buildUI(account: Account): AccountDetailsUM {
val archiveMode = when (account) {
is Account.CryptoPortfolio -> when (account.isMainAccount) {
true -> ArchiveMode.None
false -> ArchiveMode.Available(
onArchiveAccountClick = ::onArchiveAccountClick,
isLoading = false,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
private fun buildUI(account: Account.CryptoPortfolio): AccountDetailsUM {
val archiveMode = when (account.isMainAccount) {
true -> ArchiveMode.None
false -> ArchiveMode.Available(
onArchiveAccountClick = ::onArchiveAccountClick,
isLoading = false,
)
}
val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId).getOrNull()
?.isMultiCurrency == true

View file

@ -26,7 +26,6 @@ import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -277,9 +276,9 @@ internal class HotCryptoModel @Inject constructor(
private fun updateCryptoCurrency(
cryptoCurrency: CryptoCurrency,
userWallet: UserWallet,
account: AccountStatus,
account: AccountStatus.CryptoPortfolio,
): CryptoCurrency? {
val derivationIndex = account.account.derivationIndex ?: return null
val derivationIndex = account.account.derivationIndex
val blockchain = cryptoCurrency.network.toBlockchain()
val network = networkFactory.create(

View file

@ -0,0 +1,5 @@
package com.tangem.features.tangempay
interface TangemPayFeatureToggles {
val isTangemPayAccountsRefactorEnabled: Boolean
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.tangempay
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
internal class DefaultTangemPayFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TangemPayFeatureToggles {
override val isTangemPayAccountsRefactorEnabled
get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED")
}

View file

@ -45,7 +45,6 @@ internal class TangemPayDetailsComponent(
appComponentContext = child("txHistoryComponent"),
params = DefaultTangemPayTxHistoryComponent.Params(
userWalletId = params.userWalletId,
customerWalletAddress = params.config.customerWalletAddress,
uiActions = model,
),
)

View file

@ -25,7 +25,6 @@ internal class DefaultTangemPayTxHistoryComponent(
data class Params(
val userWalletId: UserWalletId,
val customerWalletAddress: String,
val uiActions: TangemPayTxHistoryUiActions,
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.features.tangempay.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TangemPayDetailsModule {
@Provides
@Singleton
fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles {
return DefaultTangemPayFeatureToggles(featureTogglesManager)
}
}

View file

@ -31,7 +31,6 @@ internal class TangemPayTxHistoryModel @Inject constructor(
private val listManager = TangemPayTxHistoryListManager(
repository = tangemPayTxHistoryRepository,
dispatchers = dispatchers,
customerWalletAddress = params.customerWalletAddress,
txHistoryUiActions = params.uiActions,
)
@ -104,7 +103,7 @@ internal class TangemPayTxHistoryModel @Inject constructor(
}
private fun loadMoreItems(): Boolean {
modelScope.launch { listManager.loadMore(params.customerWalletAddress) }
modelScope.launch { listManager.loadMore() }
return true
}

View file

@ -22,7 +22,6 @@ private typealias TangemPayTxHistoryBatchAction = BatchAction<Int, TangemPayTxHi
internal class TangemPayTxHistoryListManager(
private val repository: TangemPayTxHistoryRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val customerWalletAddress: String,
private val txHistoryUiActions: TangemPayTxHistoryUiActions,
) {
private val jobHolder = JobHolder()
@ -56,20 +55,13 @@ internal class TangemPayTxHistoryListManager(
suspend fun reload() {
actionsFlow.emit(
BatchAction.Reload(
requestParams = TangemPayTxHistoryListConfig(
customerWalletAddress = customerWalletAddress,
shouldRefresh = true,
),
),
BatchAction.Reload(requestParams = TangemPayTxHistoryListConfig(shouldRefresh = true)),
)
}
suspend fun loadMore(customerWalletAddress: String) {
suspend fun loadMore() {
actionsFlow.emit(
BatchAction.LoadMore(
requestParams = TangemPayTxHistoryListConfig(customerWalletAddress, shouldRefresh = false),
),
BatchAction.LoadMore(requestParams = TangemPayTxHistoryListConfig(shouldRefresh = false)),
)
}

View file

@ -11,9 +11,9 @@ 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.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.model.CustomerInfo.KycStatus
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents

View file

@ -108,6 +108,7 @@ dependencies {
implementation(projects.features.sendV2.api)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.tangempay.details.api)
implementation(deps.decompose.ext.compose)

View file

@ -23,6 +23,7 @@ import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
import dagger.assisted.Assisted
@ -48,6 +49,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val walletBalanceFetcher: WalletBalanceFetcher,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) : TokenDetailsDeepLinkHandler {
@ -128,7 +130,10 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
id = cryptoCurrency.id,
)
!isMultiCurrency -> walletBalanceFetcher(
params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId),
params = WalletBalanceFetcher.Params(
userWalletId = userWallet.walletId,
isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
),
)
}
}

View file

@ -145,7 +145,7 @@ internal class AccountItemsDelegate @Inject constructor(
return this.sortedBy { positionByAccountId[it.id] ?: Int.MAX_VALUE }
}
private fun openAccountDetails(account: Account) {
private fun openAccountDetails(account: Account.CryptoPortfolio) {
router.push(AppRoute.AccountDetails(account))
}

View file

@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@ -29,7 +29,7 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor(
// ignore cancelled state on analytics
customerInfo.orderStatus == OrderStatus.CANCELED -> return
// ignore kyc not approved state on analytics
customerInfo.info.kycStatus != CustomerInfo.KycStatus.APPROVED -> return
customerInfo.info.kycStatus != KycStatus.APPROVED -> return
cardInfo != null && productInstance != null -> return
else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed()
}

View file

@ -1,7 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveInAndJoin
@ -27,6 +28,7 @@ import javax.inject.Singleton
internal class WalletContentFetcher @Inject constructor(
private val walletBalanceFetcher: WalletBalanceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
) {
private val fetchingJobMap = ConcurrentHashMap<UserWalletId, JobHolder>()
@ -64,8 +66,12 @@ internal class WalletContentFetcher @Inject constructor(
Timber.d("Start fetching for $userWalletId")
val maybeResult = launch {
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
.onLeft(Timber::e)
walletBalanceFetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
),
).onLeft(Timber::e)
}
.saveInAndJoin(jobHolder)

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
@ -16,8 +17,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.domain.pay.model.CustomerInfo.KycStatus.APPROVED
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import java.util.Currency
@ -55,7 +54,7 @@ internal class TangemPayUpdateInfoStateTransformer(
// when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing.
return when {
value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId)
value.info.kycStatus != APPROVED && !value.info.customerId.isNullOrEmpty() ->
value.info.kycStatus != KycStatus.APPROVED && !value.info.customerId.isNullOrEmpty() ->
createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId)
cardInfo != null && productInstance != null ->
getCardInfoState(customerId, cardInfo, productInstance)
@ -79,7 +78,6 @@ internal class TangemPayUpdateInfoStateTransformer(
cardId = productInstance.cardId,
isPinSet = cardInfo.isPinSet,
cardFrozenState = cardFrozenState,
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
),
@ -94,25 +92,24 @@ internal class TangemPayUpdateInfoStateTransformer(
}
}
private fun createKycInProgressState(kycStatus: CustomerInfo.KycStatus, customerId: String): TangemPayState =
Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = when (kycStatus) {
CustomerInfo.KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {
when (kycStatus) {
CustomerInfo.KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked(
userWalletId = userWalletId,
customerId = customerId,
)
else -> tangemPayClickIntents.onKycProgressClicked(userWalletId)
}
},
)
private fun createKycInProgressState(kycStatus: KycStatus, customerId: String): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = when (kycStatus) {
KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {
when (kycStatus) {
KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked(
userWalletId = userWalletId,
customerId = customerId,
)
else -> tangemPayClickIntents.onKycProgressClicked(userWalletId)
}
},
)
private fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),

View file

@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock
@Composable
@ -36,6 +38,17 @@ private fun TangemPayMainScreenBlockPreview() {
TangemThemePreview {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false)
TangemPayMainScreenBlock(
state = TangemPayState.RefreshNeeded(
TangemPayRefreshNeeded(
tangemIcon = R.drawable.ic_tangem_24,
buttonText = resourceReference(id = R.string.home_button_scan),
onRefreshClick = {},
shouldShowProgress = false,
),
),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false)