Updated on 2026-08-14
This commit is contained in:
parent
c9804fa0ed
commit
f1e2614b01
8 changed files with 51 additions and 68 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue