Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-19 18:24:41 +05:00
parent 10584c84a8
commit 096d1e40c9
11 changed files with 79 additions and 53 deletions

View file

@ -4,6 +4,11 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class OrderRequest(
@Json(name = "wallet_address") val walletAddress: String,
)
data class OrderRequest(@Json(name = "data") val data: Data) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "specification_name") val specificationName: String = "SP_000004",
@Json(name = "type") val type: String = "CARD_ISSUE_VIRTUAL_RAIN_KYC",
)
}

View file

@ -73,7 +73,7 @@ data class CustomerMeResponse(
@JsonClass(generateAdapter = true)
data class PaymentAccount(
@Json(name = "id") val id: String,
@Json(name = "address") val address: String,
@Json(name = "address") val address: String?,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)

View file

@ -92,8 +92,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
}
private fun UserWallet.isCompatible(): Boolean = when (this) {
is UserWallet.Cold ->
scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
}

View file

@ -104,9 +104,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
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
if (customerInfo.productInstance == null) {
onboardingRepository.createOrder(userWalletId)
.onLeft { Timber.tag(TAG).e("createOrder failed: $it") }
}
status
},
@ -128,9 +128,6 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
-> 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 -> {

View file

@ -27,6 +27,7 @@ internal class DefaultCustomerOrderRepository @Inject constructor(
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
}
OrderData(
customerId = response.result?.customerId.orEmpty(),
status = status,
withdrawTxHash = response.result?.data?.transactionHash?.ifEmpty { null },
)

View file

@ -1,13 +1,14 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
import com.tangem.datasource.api.pay.models.request.OrderRequest
import com.tangem.datasource.api.pay.models.request.SetTangemPayEnabledRequest
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -23,14 +24,11 @@ import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
private const val VALID_STATUS = "valid"
private const val TAG = "TangemPay: OnboardingRepository"
@Suppress("LongParameterList")
internal class DefaultOnboardingRepository @Inject constructor(
@ -109,25 +107,31 @@ internal class DefaultOnboardingRepository @Inject constructor(
return lastFetchedCustomerInfoMap[userWalletId]
}
override suspend fun createOrder(userWalletId: UserWalletId) = withContext(dispatcherProvider.io) {
launch {
catch(
block = {
val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
}.getOrNull()
override suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> =
withContext(dispatcherProvider.io) {
val existingOrderId = getOrderId(userWalletId)
if (existingOrderId != null) {
val orderStatus = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, existingOrderId)
}.map { it.result?.status }.getOrNull()
val result = requireNotNull(response?.result)
val customerWalletAddress = requireNotNull(result.data.customerWalletAddress)
tangemPayStorage.storeOrderId(customerWalletAddress, result.id)
},
catch = {
Timber.tag(TAG).e("createOrder: $it")
},
)
if (orderStatus == OrderResponse.Result.Status.NEW ||
orderStatus == OrderResponse.Result.Status.PROCESSING
) {
return@withContext existingOrderId.right()
}
}
val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
requestHelper.performRequest(userWalletId) { authHeader ->
val data = OrderRequest.Data(customerWalletAddress = walletAddress)
tangemPayApi.createOrder(authHeader, body = OrderRequest(data = data))
}.map { response ->
val result = requireNotNull(response.result)
tangemPayStorage.storeOrderId(walletAddress, result.id)
result.id
}
}
}
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }

View file

@ -1,6 +1,7 @@
package com.tangem.domain.pay.model
data class OrderData(
val customerId: String,
val status: OrderStatus,
val withdrawTxHash: String?,
)

View file

@ -5,7 +5,6 @@ import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.visa.error.VisaApiError
import kotlinx.coroutines.Job
interface OnboardingRepository {
@ -17,7 +16,7 @@ interface OnboardingRepository {
suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo>
suspend fun createOrder(userWalletId: UserWalletId): Job
suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String>
suspend fun clearOrderId(userWalletId: UserWalletId)

View file

@ -126,11 +126,13 @@ class TangemPayMainScreenCustomerInfoUseCase(
}
.map { customerInfo ->
Timber.tag(TAG).i("customerInfo")
if (customerInfo.cardInfo == null && customerInfo.kycStatus == KycStatus.APPROVED) {
// If order id wasn't saved -> start order creation and get customer info
if (customerInfo.productInstance == null) {
onboardingRepository.createOrder(userWalletId)
Timber.tag("ddk9499").d("TangemPayMainScreenCustomerInfoUseCase.proceedWithoutOrder: ")
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW)
} else {
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.COMPLETED)
}
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.UNKNOWN)
}
}
@ -145,27 +147,33 @@ class TangemPayMainScreenCustomerInfoUseCase(
},
ifRight = { orderData ->
when (orderData.status) {
// Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.NEW,
OrderStatus.PROCESSING,
-> MainScreenCustomerInfo(
info = CustomerInfo(
customerId = null,
productInstance = null,
kycStatus = KycStatus.APPROVED,
cardInfo = null,
),
orderStatus = orderData.status,
).right()
-> {
onboardingRepository.getCustomerInfo(userWalletId = userWalletId)
.mapLeft { it.mapErrorForCustomer() }
.map { customerInfo ->
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status)
}
}
// Order cancelled. No need to get customer info
OrderStatus.CANCELED -> {
MainScreenCustomerInfo(
info = CustomerInfo(
customerId = null,
productInstance = null,
kycStatus = KycStatus.INIT,
cardInfo = null,
),
orderStatus = OrderStatus.CANCELED,
).right()
}
// Order was created/cancelled -> clear order id and get customer info
OrderStatus.COMPLETED,
OrderStatus.CANCELED,
OrderStatus.UNKNOWN,
-> {
onboardingRepository.clearOrderId(userWalletId)
// If order was cancelled -> start order creation
if (orderData.status == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId)
onboardingRepository.getCustomerInfo(userWalletId = userWalletId)
.mapLeft { it.mapErrorForCustomer() }
.map { customerInfo ->

View file

@ -94,9 +94,14 @@ internal class TangemPayOnboardingModel @Inject constructor(
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
repository.getCustomerInfo(userWalletId = userWalletId)
.onRight { customerInfo ->
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
when {
customerInfo.kycStatus != KycStatus.APPROVED -> {
if (customerInfo.productInstance == null) {
repository.createOrder(userWalletId)
.onLeft { error ->
Timber.e("Error creating order before KYC: $error")
}
}
when (params) {
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> openKyc(userWalletId)
else -> startOnboarding(userWalletId)
@ -104,6 +109,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
}
else -> back()
}
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
}
.onLeft { startOnboarding(userWalletId) }
}
@ -174,6 +180,12 @@ internal class TangemPayOnboardingModel @Inject constructor(
if (customerInfo.kycStatus == KycStatus.APPROVED) {
back()
} else {
if (customerInfo.productInstance == null) {
repository.createOrder(userWalletId)
.onLeft { error ->
Timber.e("Error creating order before KYC: $error")
}
}
openKyc(userWalletId)
}
},

View file

@ -56,7 +56,7 @@ internal class TangemPayUpdateInfoStateTransformer(
value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId)
value.info.kycStatus != KycStatus.APPROVED && !value.info.customerId.isNullOrEmpty() ->
createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId)
cardInfo != null && productInstance != null ->
cardInfo != null && productInstance != null && value.orderStatus == OrderStatus.COMPLETED ->
getCardInfoState(customerId, cardInfo, productInstance)
else -> createIssueProgressState()
}