Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-24 21:29:11 +04:00
parent 88af2e8aa5
commit 322d0364e5
15 changed files with 70 additions and 114 deletions

View file

@ -0,0 +1,37 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class BalanceResponse(
@Json(name = "fiat") val fiat: FiatBalance,
@Json(name = "crypto") val crypto: CryptoBalance,
@Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal,
)
@JsonClass(generateAdapter = true)
data class FiatBalance(
@Json(name = "currency") val currency: String,
@Json(name = "available_balance") val availableBalance: BigDecimal,
@Json(name = "credit_limit") val creditLimit: BigDecimal,
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
@Json(name = "posted_charges") val postedCharges: BigDecimal,
@Json(name = "balance_due") val balanceDue: BigDecimal,
)
@JsonClass(generateAdapter = true)
data class CryptoBalance(
@Json(name = "id") val id: String,
@Json(name = "chain_id") val chainId: Int,
@Json(name = "deposit_address") val depositAddress: String,
@Json(name = "token_contract_address") val tokenContractAddress: String,
@Json(name = "balance") val balance: BigDecimal,
)
@JsonClass(generateAdapter = true)
data class AvailableForWithdrawal(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
)

View file

@ -1,20 +1,8 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
data class CardBalanceResponse(
@Json(name = "result") val result: Result?,
@Json(name = "result") val result: BalanceResponse?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "currency") val currency: String,
@Json(name = "available_balance") val availableBalance: BigDecimal,
@Json(name = "credit_limit") val creditLimit: BigDecimal,
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
@Json(name = "posted_charges") val postedCharges: BigDecimal,
@Json(name = "balance_due") val balanceDue: BigDecimal,
)
}
)

View file

@ -2,7 +2,6 @@ package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@ -19,7 +18,7 @@ data class CustomerMeResponse(
@Json(name = "kyc") val kyc: Kyc?,
@Json(name = "depositAddress") val depositAddress: String?,
@Json(name = "card") val card: Card?,
@Json(name = "balance") val balance: Balance?,
@Json(name = "balance") val balance: BalanceResponse?,
)
@JsonClass(generateAdapter = true)
@ -99,14 +98,4 @@ data class CustomerMeResponse(
@Json(name = "card_status") val cardStatus: String,
@Json(name = "card_number_end") val cardNumberEnd: String,
)
@JsonClass(generateAdapter = true)
data class Balance(
@Json(name = "currency") val currency: String,
@Json(name = "available_balance") val availableBalance: BigDecimal,
@Json(name = "credit_limit") val creditLimit: BigDecimal,
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
@Json(name = "posted_charges") val postedCharges: BigDecimal,
@Json(name = "balance_due") val balanceDue: BigDecimal,
)
}

View file

@ -1,17 +1,16 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.data.pay.repository.DefaultKycRepository
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.domain.pay.TangemPayTopUpDataFactory
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
@ -68,11 +67,5 @@ internal interface TangemPayDataModule {
): ProduceTangemPayInitialDataUseCase {
return ProduceTangemPayInitialDataUseCase(repository = repository)
}
@Provides
@Singleton
fun provideTangemPayIssueOrderUseCase(repository: OnboardingRepository): TangemPayIssueOrderUseCase {
return TangemPayIssueOrderUseCase(repository = repository)
}
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -88,8 +89,9 @@ internal class DefaultOnboardingRepository @Inject constructor(
val customerWalletAddress = requestHelper.getCustomerWalletAddress()
when (val orderId = tangemPayStorage.getOrderId(customerWalletAddress)) {
// If order id wasn't saved -> get customer info
// If order id wasn't saved -> start order creation and get customer info
null -> {
createOrder()
MainScreenCustomerInfo(
info = getCustomerInfoWithPersistedToken(),
orderStatus = OrderStatus.UNKNOWN,
@ -98,6 +100,10 @@ internal class DefaultOnboardingRepository @Inject constructor(
// If order id was saved -> check its status
else -> {
val orderStatus = getOrderStatus(orderId)
if (orderStatus == OrderStatus.CANCELED) {
// If order was cancelled -> start order creation
createOrder()
}
val customerInfo = when (orderStatus) {
// Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.NEW,
@ -122,14 +128,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
return lastFetchedCustomerInfo
}
override suspend fun createOrder(): Either<UniversalError, Unit> = withContext(dispatcherProvider.io) {
requestHelper.runWithErrorLogs(TAG) {
val walletAddress = requestHelper.getCustomerWalletAddress()
val result = requestHelper.request { authHeader ->
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
}.result ?: error("Create order result is null")
private suspend fun createOrder() = withContext(dispatcherProvider.io) {
launch {
requestHelper.runWithErrorLogs(TAG) {
val walletAddress = requestHelper.getCustomerWalletAddress()
val result = requestHelper.request { authHeader ->
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
}.result ?: error("Create order result is null")
tangemPayStorage.storeOrderId(result.data.customerWalletAddress, result.id)
tangemPayStorage.storeOrderId(result.data.customerWalletAddress, result.id)
}
}
}
@ -140,8 +148,8 @@ internal class DefaultOnboardingRepository @Inject constructor(
val cardInfo = if (paymentAccount != null && card != null && balance != null) {
CardInfo(
lastFourDigits = card.cardNumberEnd,
balance = balance.availableBalance,
currencyCode = balance.currency,
balance = balance.fiat.availableBalance,
currencyCode = balance.fiat.currency,
customerWalletAddress = paymentAccount.customerWalletAddress,
depositAddress = response.depositAddress,
)

View file

@ -41,8 +41,8 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
tangemPayApi.getCardBalance(authHeader)
}.result ?: error("Cannot get card balance")
TangemPayCardBalance(
balance = result.availableBalance,
currencyCode = result.currency,
balance = result.fiat.availableBalance,
currencyCode = result.fiat.currency,
)
}
}

View file

@ -15,8 +15,6 @@ interface OnboardingRepository {
suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo>
suspend fun createOrder(): Either<UniversalError, Unit>
/**
* Returns only if the user already authorised at least once
*/

View file

@ -1,12 +0,0 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.pay.repository.OnboardingRepository
class TangemPayIssueOrderUseCase(
private val repository: OnboardingRepository,
) {
suspend operator fun invoke(): Either<UniversalError, Unit> = repository.createOrder()
}

View file

@ -23,7 +23,6 @@ import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
@ -96,7 +95,6 @@ internal class WalletModel @Inject constructor(
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
private val tangemPayOnboardingRepository: OnboardingRepository,
@ -398,7 +396,6 @@ internal class WalletModel @Inject constructor(
transformer = TangemPayInitialStateTransformer(
value = info,
cardFrozenState = cardFrozenState,
onClickIssue = ::issueOrder,
onClickKyc = innerWalletRouter::openTangemPayOnboarding,
openDetails = { config ->
innerWalletRouter.openTangemPayDetails(
@ -410,15 +407,6 @@ internal class WalletModel @Inject constructor(
)
}
private fun issueOrder() {
modelScope.launch {
stateHolder.update(TangemPayIssueProgressStateTransformer())
tangemPayIssueOrderUseCase().onLeft {
stateHolder.update(TangemPayIssueAvailableStateTransformer(onClickIssue = ::issueOrder))
}
}
}
private fun needToRefreshTimer() {
modelScope.launch {
delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS)

View file

@ -3,15 +3,11 @@ package com.tangem.feature.wallet.child.wallet.model.intents
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayInitialStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayIssueAvailableStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayIssueProgressStateTransformer
import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
import javax.inject.Inject
internal interface TangemPayIntents {
@ -23,7 +19,6 @@ internal interface TangemPayIntents {
internal class TangemPayClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
private val mainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val issueOrderUseCase: TangemPayIssueOrderUseCase,
private val featureToggles: TangemPayFeatureToggles,
private val onboardingRepository: OnboardingRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
@ -40,7 +35,6 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
stateHolder.update(
transformer = TangemPayInitialStateTransformer(
value = info,
onClickIssue = ::issueOrder,
onClickKyc = router::openTangemPayOnboarding,
openDetails = { config -> router.openTangemPayDetails(userWalletId, config) },
cardFrozenState = cardFrozenState,
@ -48,13 +42,4 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
)
}
}
private fun issueOrder() {
modelScope.launch {
stateHolder.update(TangemPayIssueProgressStateTransformer())
issueOrderUseCase().onLeft {
stateHolder.update(TangemPayIssueAvailableStateTransformer(onClickIssue = ::issueOrder))
}
}
}
}

View file

@ -11,6 +11,7 @@ internal sealed class TangemPayState {
data class Progress(
val title: TextReference,
val description: TextReference,
val buttonText: TextReference,
@DrawableRes val iconRes: Int,
val onButtonClick: () -> Unit,

View file

@ -7,12 +7,9 @@ import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus.CANCELED
import com.tangem.domain.pay.model.OrderStatus.UNKNOWN
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createKycInProgressState
import java.util.Currency
@ -26,7 +23,6 @@ private const val POLYGON_CHAIN_ID = 137
internal class TangemPayInitialStateTransformer(
private val value: MainScreenCustomerInfo? = null,
private val cardFrozenState: TangemPayCardFrozenState,
private val onClickIssue: () -> Unit = {},
private val onClickKyc: () -> Unit = {},
private val openDetails: (config: TangemPayDetailsConfig) -> Unit = {},
) : WalletScreenStateTransformer {
@ -43,7 +39,6 @@ internal class TangemPayInitialStateTransformer(
value == null -> TangemPayState.Empty
!value.info.isKycApproved -> createKycInProgressState(onClickKyc)
cardInfo != null && productInstance != null -> getCardInfoState(cardInfo, productInstance)
value.orderStatus == UNKNOWN || value.orderStatus == CANCELED -> createIssueAvailableState(onClickIssue)
else -> createIssueProgressState()
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState
internal class TangemPayIssueAvailableStateTransformer(
private val onClickIssue: () -> Unit = {},
) : WalletScreenStateTransformer {
override fun transform(prevState: WalletScreenState): WalletScreenState =
prevState.copy(tangemPayState = createIssueAvailableState(onClickIssue))
}

View file

@ -9,20 +9,15 @@ internal object TangemPayStateCreator {
fun createKycInProgressState(onClickKyc: () -> Unit): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
description = TextReference.EMPTY,
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = onClickKyc,
)
fun createIssueAvailableState(onClickIssue: () -> Unit) = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = onClickIssue,
)
fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
description = TextReference.Res(R.string.tangempay_issue_card_notification_description),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},

View file

@ -28,7 +28,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
config = NotificationConfig(
title = state.title,
iconSize = 36.dp,
subtitle = TextReference.EMPTY,
subtitle = state.description,
iconResId = state.iconRes,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = state.buttonText,
@ -52,6 +52,7 @@ private fun ResetCardScreenPreview() {
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
description = TextReference.EMPTY,
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {},
@ -62,6 +63,7 @@ private fun ResetCardScreenPreview() {
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
description = TextReference.EMPTY,
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
@ -72,6 +74,7 @@ private fun ResetCardScreenPreview() {
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
description = TextReference.Res(R.string.tangempay_issue_card_notification_description),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},