Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-04 22:06:09 +05:00
parent c9804fa0ed
commit f1e2614b01
8 changed files with 51 additions and 68 deletions

View file

@ -2,35 +2,26 @@ package com.tangem.data.pay.datasource
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.WithdrawalSignatureResult
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.sdk.api.TangemSdkManager
import javax.inject.Inject
internal class DefaultTangemPayAuthDataSource @Inject constructor(
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val tangemSdkManager: TangemSdkManager,
) : TangemPayAuthDataSource {
override suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> {
return when (val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)) {
is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error)
is CompletionResult.Success<TangemPayInitialCredentials> -> Either.Right(initialCredentials.data)
is CompletionResult.Failure<*> -> initialCredentials.error.left()
is CompletionResult.Success<TangemPayInitialCredentials> -> initialCredentials.data.right()
}
}
override suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens> = either {
visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens(refreshToken = refreshToken)
.mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") }
.bind()
}
override suspend fun getWithdrawalSignature(
cardId: String,
hash: String,
@ -38,13 +29,13 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) {
is CompletionResult.Failure<*> -> {
if (signResult.error is TangemSdkError.UserCancelled) {
Either.Right(WithdrawalSignatureResult.Cancelled)
WithdrawalSignatureResult.Cancelled.right()
} else {
signResult.error.left()
}
}
is CompletionResult.Success<String> -> {
Either.Right(WithdrawalSignatureResult.Success(signResult.data))
WithdrawalSignatureResult.Success(signResult.data).right()
}
}
}

View file

@ -139,7 +139,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
return requestHelper.makeSafeRequest(userWalletId) {
return requestHelper.performRequest(userWalletId) {
tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.onLeft {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Unfrozen)
@ -172,7 +172,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
return requestHelper.makeSafeRequest(userWalletId) {
return requestHelper.performRequest(userWalletId) {
tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.onLeft {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Frozen)
@ -215,7 +215,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
while (isActive && pollingJobs.containsKey(orderId)) {
delay(duration = 5.seconds)
val orderStatus = requestHelper.makeSafeRequest(userWalletId) { authHeader ->
val orderStatus = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}

View file

@ -44,7 +44,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
): Either<UniversalError, WithdrawalResult> {
val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId)
if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError)
return requestHelper.makeSafeRequest(userWalletId) { authHeader ->
return requestHelper.performRequest(userWalletId) { authHeader ->
val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress)
tangemPayApi.getWithdrawData(authHeader = authHeader, body = request)
}.map { data ->
@ -60,7 +60,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
Either.Right(WithdrawalResult.Cancelled)
}
is WithdrawalSignatureResult.Success -> {
requestHelper.makeSafeRequest(userWalletId) { authHeader ->
requestHelper.performRequest(userWalletId) { authHeader ->
val request = WithdrawRequest(
amountInCents = amountInCents,
recipientAddress = receiverAddress,

View file

@ -1,6 +1,7 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.right
@ -8,30 +9,35 @@ import com.squareup.moshi.Moshi
import com.squareup.wire.Instant
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.TangemPayAuthApi
import com.tangem.datasource.api.pay.models.request.RefreshCustomerWalletAccessTokenRequest
import com.tangem.datasource.api.pay.models.response.TangemPayGetTokensResponse
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "TangemPayRequestPerformer"
@Singleton
internal class TangemPayRequestPerformer @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayAuthApi: TangemPayAuthApi,
private val tangemPayStorage: TangemPayStorage,
private val authDataSource: TangemPayAuthDataSource,
) {
private val customerWalletAddress = MutableStateFlow<String?>(null)
@ -56,13 +62,6 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
}
suspend fun <T : Any> makeSafeRequest(
userWalletId: UserWalletId,
requestBlock: suspend (header: String) -> ApiResponse<T>,
): Either<VisaApiError, T> {
return performRequest(userWalletId, requestBlock)
}
@Deprecated("Use perform request instead", replaceWith = ReplaceWith("performRequest"))
suspend fun <T : Any> request(
userWalletId: UserWalletId,
@ -99,7 +98,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
): Either<VisaApiError, T> = withContext(dispatchers.io) {
catch(
block = {
val tokens = getAccessTokens(userWalletId) ?: return@catch VisaApiError.RefreshTokenExpired.left()
val tokens = getAccessTokens(userWalletId).getOrElse { return@catch it.left() }
when (val apiResponse: ApiResponse<T> = requestBlock(tokens.getAuthHeader())) {
is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left()
@ -107,7 +106,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
},
catch = { errorConverter.convert(it).left() },
).onLeft { visaApiError -> Timber.tag("TangemPayRequestPerformer").e(visaApiError.toString()) }
).onLeft { visaApiError -> Timber.tag(TAG).e(visaApiError.toString()) }
}
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String {
@ -123,7 +122,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
return storedAddress
}
private suspend fun getAccessTokens(userWalletId: UserWalletId): TangemPayAuthTokens? {
private suspend fun getAccessTokens(userWalletId: UserWalletId): Either<VisaApiError, TangemPayAuthTokens> {
return tokensMutex.withLock {
val walletAddress = getCustomerWalletAddress(userWalletId)
val tokens = tangemPayStorage.getAuthTokens(walletAddress) ?: error("Auth tokens are not stored")
@ -132,24 +131,41 @@ internal class TangemPayRequestPerformer @Inject constructor(
val refreshExpiresAt = Instant.ofEpochSecond(tokens.refreshExpiresAt)
if (accessExpiresAt.isAfter(now)) {
tokens
tokens.right()
} else if (accessExpiresAt.isBefore(now) && refreshExpiresAt.isAfter(now)) {
val newTokens = refreshAuthTokens(userWalletId = userWalletId, refreshToken = tokens.refreshToken)
newTokens
refreshAuthTokens(userWalletId = userWalletId, refreshToken = tokens.refreshToken)
} else {
null
VisaApiError.RefreshTokenExpired.left()
}
}
}
private suspend fun refreshAuthTokens(userWalletId: UserWalletId, refreshToken: String): TangemPayAuthTokens {
private suspend fun refreshAuthTokens(
userWalletId: UserWalletId,
refreshToken: String,
): Either<VisaApiError, TangemPayAuthTokens> {
val customerWalletAddress = getCustomerWalletAddress(userWalletId)
val tokens = authDataSource.refreshAuthTokens(refreshToken)
.fold(
ifLeft = { error -> error("Cannot refresh tokens: ${error.message}") },
ifRight = { it },
val apiResponse = tangemPayAuthApi.refreshCustomerWalletAccessToken(
request = RefreshCustomerWalletAccessTokenRequest(
authType = "customer_wallet",
refreshToken = refreshToken,
),
)
val responseEither = when (apiResponse) {
is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left()
is ApiResponse.Success<TangemPayGetTokensResponse> -> apiResponse.data.right()
}
return responseEither.map { response ->
TangemPayAuthTokens(
accessToken = response.accessToken,
expiresAt = response.expiresAt,
refreshToken = response.refreshToken,
refreshExpiresAt = response.refreshExpiresAt,
)
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
return tokens
}.onRight { tokens ->
tangemPayStorage.storeAuthTokens(customerWalletAddress = customerWalletAddress, tokens = tokens)
}.onLeft {
Timber.tag(TAG).e("Can not refresh auth tokens: $it")
}
}
}

View file

@ -13,6 +13,7 @@ class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, VisaApiError>
override fun convert(value: Throwable): VisaApiError {
return if (value is ApiResponseError.HttpException) {
if (value.code == ApiResponseError.HttpException.Code.NOT_FOUND) return VisaApiError.NotPaeraCustomer
if (value.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) return VisaApiError.RefreshTokenExpired
val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode
return runCatching {

View file

@ -106,26 +106,6 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
}
}
override suspend fun refreshCustomerWalletAuthTokens(
refreshToken: String,
): Either<VisaApiError, TangemPayAuthTokens> = withContext(dispatchers.io) {
request {
tangemPayAuthApi.refreshCustomerWalletAccessToken(
request = RefreshCustomerWalletAccessTokenRequest(
authType = "customer_wallet",
refreshToken = refreshToken,
),
).getOrThrow()
}.map { response ->
TangemPayAuthTokens(
accessToken = response.accessToken,
expiresAt = response.expiresAt,
refreshToken = response.refreshToken,
refreshExpiresAt = response.refreshExpiresAt,
)
}
}
override suspend fun getAccessTokens(
signedChallenge: VisaAuthSignedChallenge,
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {

View file

@ -2,14 +2,11 @@ package com.tangem.domain.pay.datasource
import arrow.core.Either
import com.tangem.domain.pay.model.WithdrawalSignatureResult
import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.domain.visa.model.TangemPayInitialCredentials
interface TangemPayAuthDataSource {
suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens>
suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, WithdrawalSignatureResult>
}

View file

@ -30,8 +30,6 @@ interface VisaAuthRemoteDataSource {
nonce: String,
): Either<VisaApiError, TangemPayAuthTokens>
suspend fun refreshCustomerWalletAuthTokens(refreshToken: String): Either<VisaApiError, TangemPayAuthTokens>
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either<VisaApiError, VisaAuthTokens>
suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either<VisaApiError, VisaAuthTokens>