Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-15 05:44:29 -07:00
parent 8a749438bf
commit 8098476fde
15 changed files with 459 additions and 180 deletions

View file

@ -26,9 +26,11 @@ 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.ChangeCardFrozenStateUseCase
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
@ -172,5 +174,27 @@ internal interface TangemPayDataModule {
): ProduceTangemPayInitialDataUseCase {
return ProduceTangemPayInitialDataUseCase(repository = repository)
}
@Provides
fun provideChangeCardFrozenStateUseCase(
cardDetailsRepository: TangemPayCardDetailsRepository,
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
appCoroutineScope: AppCoroutineScope,
): ChangeCardFrozenStateUseCase {
return ChangeCardFrozenStateUseCase(
cardDetailsRepository = cardDetailsRepository,
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
appCoroutineScope = appCoroutineScope,
)
}
@Provides
@Singleton
fun provideStartTangemPayPollingUseCase(
cardDetailsRepository: TangemPayCardDetailsRepository,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): StartTangemPayOrderPollingUseCase {
return StartTangemPayOrderPollingUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
}
}
}

View file

@ -7,12 +7,11 @@ import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.OrderStatusConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.ReissueCardRequest
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.runSuspendCatching
@ -22,6 +21,7 @@ internal class DefaultReissueCardRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayReissueCardStore: TangemPayReissueCardStore,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : TangemPayReissueCardRepository {
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
@ -53,14 +53,14 @@ internal class DefaultReissueCardRepository @Inject constructor(
override suspend fun reissueCard(
userWalletId: UserWalletId,
cardId: String,
): Either<VisaApiError, TangemPayReissueOrderInfo> = either {
): Either<VisaApiError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.reissueCard(
authHeader = authHeader,
body = ReissueCardRequest(cardId = cardId),
)
}.bind()
TangemPayReissueOrderInfo(
TangemPayOrderInfo(
orderId = response.result.orderId,
orderStatus = OrderStatusConverter.convert(response.result.status),
)
@ -77,28 +77,14 @@ internal class DefaultReissueCardRepository @Inject constructor(
override suspend fun getReissueOrderInfo(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayReissueOrderInfo?> = either {
): Either<UniversalError, TangemPayOrderInfo?> = either {
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
if (orderId == null) {
return null.right()
}
val order = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}.bind()
val result = order.result ?: raise(VisaApiError.Unspecified)
TangemPayReissueOrderInfo(
orderId = result.id,
orderStatus = when (result.status) {
OrderResponse.Result.Status.NEW -> OrderStatus.NEW
OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING
OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
},
)
cardDetailsRepository.getOrderInfo(userWalletId, orderId).bind()
}
private companion object {

View file

@ -3,6 +3,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.RainCryptoUtil
@ -22,22 +23,17 @@ import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.account.CardDisplayName
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
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
private const val TAG = "TangemPay: CardDetailsRepository"
@ -51,12 +47,8 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val storage: TangemPayStorage,
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
private val errorConverter: TangemPayErrorConverter,
private val pollingScope: AppCoroutineScope,
) : TangemPayCardDetailsRepository {
private val pollingJobs = mutableMapOf<String, Job>()
private val storePollingMutex = Mutex()
override suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance> {
return catch(
block = {
@ -201,123 +193,43 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
override suspend fun freezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
return requestHelper.performRequest(userWalletId) {
): Either<UniversalError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) {
tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.onLeft {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Unfrozen)
}.map { response ->
val state = when (response.result?.status) {
FreezeUnfreezeCardResponse.Status.COMPLETED -> TangemPayCardFrozenState.Frozen
FreezeUnfreezeCardResponse.Status.NEW,
FreezeUnfreezeCardResponse.Status.PROCESSING,
-> TangemPayCardFrozenState.Pending
FreezeUnfreezeCardResponse.Status.CANCELED,
null,
-> TangemPayCardFrozenState.Unfrozen
}
if (state == TangemPayCardFrozenState.Pending) {
startOrderIdPolling(
userWalletId = userWalletId,
cardId = cardId,
orderId = response.result?.orderId,
isFreeze = true,
)
}
cardFrozenStateStore.store(cardId, state)
}.bind()
state
}
val result = response.result ?: raise(VisaApiError.Unspecified)
TangemPayOrderInfo(
orderId = result.orderId,
orderStatus = when (result.status) {
FreezeUnfreezeCardResponse.Status.NEW -> OrderStatus.NEW
FreezeUnfreezeCardResponse.Status.PROCESSING -> OrderStatus.PROCESSING
FreezeUnfreezeCardResponse.Status.COMPLETED -> OrderStatus.COMPLETED
FreezeUnfreezeCardResponse.Status.CANCELED -> OrderStatus.CANCELED
},
)
}
override suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
return requestHelper.performRequest(userWalletId) {
): Either<UniversalError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) {
tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.onLeft {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Frozen)
}.map { response ->
val state = when (response.result?.status) {
FreezeUnfreezeCardResponse.Status.COMPLETED -> TangemPayCardFrozenState.Unfrozen
FreezeUnfreezeCardResponse.Status.NEW,
FreezeUnfreezeCardResponse.Status.PROCESSING,
-> TangemPayCardFrozenState.Pending
FreezeUnfreezeCardResponse.Status.CANCELED,
null,
-> TangemPayCardFrozenState.Frozen
}
if (state == TangemPayCardFrozenState.Pending) {
startOrderIdPolling(
userWalletId = userWalletId,
cardId = cardId,
orderId = response.result?.orderId,
isFreeze = false,
)
}
cardFrozenStateStore.store(cardId, state)
}.bind()
state
}
}
val result = response.result ?: raise(VisaApiError.Unspecified)
private suspend fun startOrderIdPolling(
userWalletId: UserWalletId,
cardId: String,
orderId: String?,
isFreeze: Boolean,
) {
if (orderId.isNullOrEmpty()) return
storePollingMutex.withLock {
if (pollingJobs.containsKey(orderId)) return
val pollingJob = pollingScope.launch {
try {
var retryCount = 0
while (isActive && pollingJobs.containsKey(orderId)) {
delay(duration = 5.seconds)
val orderStatus = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}
orderStatus.onRight { response ->
val status = response.result?.status
if (status == Status.COMPLETED || status == Status.CANCELED) {
// Remove from jobs
pollingJobs.remove(key = orderId)
// Final card state
val finalState = when {
status == Status.COMPLETED && isFreeze
-> TangemPayCardFrozenState.Frozen
status == Status.COMPLETED && !isFreeze
-> TangemPayCardFrozenState.Unfrozen
else -> return@launch
}
cardFrozenStateStore.store(cardId, finalState)
}
}.onLeft { error ->
TangemLogger.e("error ${error.errorCode}")
// stop retrying after 3 errors
if (retryCount > MAX_POLLING_RETRIES) {
pollingJobs.remove(key = orderId)
}
}
retryCount++
}
} catch (e: Exception) {
TangemLogger.e("Error", e)
storePollingMutex.withLock {
pollingJobs.remove(orderId)
}
}
}
pollingJobs[orderId] = pollingJob
}
TangemPayOrderInfo(
orderId = result.orderId,
orderStatus = when (result.status) {
FreezeUnfreezeCardResponse.Status.NEW -> OrderStatus.NEW
FreezeUnfreezeCardResponse.Status.PROCESSING -> OrderStatus.PROCESSING
FreezeUnfreezeCardResponse.Status.COMPLETED -> OrderStatus.COMPLETED
FreezeUnfreezeCardResponse.Status.CANCELED -> OrderStatus.CANCELED
},
)
}
override suspend fun updateCardDisplayName(
@ -376,6 +288,31 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
return cardFrozenStateStore.getSyncOrNull(cardId)
}
override suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState) {
cardFrozenStateStore.store(cardId, state)
}
override suspend fun getOrderInfo(
userWalletId: UserWalletId,
orderId: String,
): Either<UniversalError, TangemPayOrderInfo> = either {
val order = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}.bind()
val result = order.result ?: raise(VisaApiError.Unspecified)
TangemPayOrderInfo(
orderId = result.id,
orderStatus = when (result.status) {
Status.NEW -> OrderStatus.NEW
Status.PROCESSING -> OrderStatus.PROCESSING
Status.COMPLETED -> OrderStatus.COMPLETED
Status.CANCELED -> OrderStatus.CANCELED
},
)
}
private suspend fun getPublicKeyBase64(): String {
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
return when (env) {
@ -395,8 +332,4 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
TangemLogger.withTag(TAG).e("Error", throwable)
return errorConverter.convert(throwable).left()
}
private companion object {
const val MAX_POLLING_RETRIES = 3
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.coroutines.flow.Flow
@ -65,12 +66,12 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
override suspend fun freezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.freezeCard(userWalletId, cardId)
): Either<UniversalError, TangemPayOrderInfo> = real.freezeCard(userWalletId, cardId)
override suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.unfreezeCard(userWalletId, cardId)
): Either<UniversalError, TangemPayOrderInfo> = real.unfreezeCard(userWalletId, cardId)
override fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState> =
real.cardFrozenState(cardId)
@ -78,6 +79,14 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
override suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? =
real.cardFrozenStateSync(cardId)
override suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState) =
real.setCardFrozenState(cardId, state)
override suspend fun getOrderInfo(
userWalletId: UserWalletId,
orderId: String,
): Either<UniversalError, TangemPayOrderInfo> = real.getOrderInfo(userWalletId, orderId)
override suspend fun updateCardDisplayName(
cardId: String,
userWalletId: UserWalletId,