Updated on 2026-08-14
This commit is contained in:
parent
e01a65011b
commit
46cf9b8b08
2 changed files with 93 additions and 87 deletions
|
|
@ -1,23 +1,26 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: KycRepository"
|
||||
|
||||
internal class DefaultKycRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
) : KycRepository {
|
||||
|
||||
override suspend fun getKycStartInfo() = withContext(dispatchers.io) {
|
||||
requestHelper.request { authHeader ->
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader)
|
||||
}.map {
|
||||
KycStartInfo(token = it.result.token, locale = it.result.locale)
|
||||
override suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader)
|
||||
}.result
|
||||
|
||||
KycStartInfo(token = result.token, locale = result.locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -11,14 +10,15 @@ import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapte
|
|||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -27,6 +27,8 @@ import kotlinx.coroutines.flow.first
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* For TangemPay Customer Wallet auth we are using polygon address
|
||||
|
|
@ -41,62 +43,60 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
) {
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
private var customerWalletAddress: String? = null
|
||||
|
||||
private val refreshTokensMutex = Mutex()
|
||||
private var refreshTokensJob: Deferred<Either<UniversalError, VisaAuthTokens>>? = null
|
||||
private var refreshTokensJob: Deferred<VisaAuthTokens>? = null
|
||||
|
||||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): Either<UniversalError, T> =
|
||||
either {
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokens,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
).bind()
|
||||
}
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
suspend fun <T : Any> runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either<UniversalError, T> {
|
||||
return try {
|
||||
val result = requestBlock()
|
||||
Either.Right(result)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e("$tag: $exception")
|
||||
Either.Left(mapError(exception))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Any> requestWithPersistedToken(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
): Either<UniversalError, T> = either {
|
||||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokensIfSaved,
|
||||
getTokens = ::getAccessTokens,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
).bind()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> requestWithPersistedToken(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = { getAccessTokensIfSaved() ?: error("Cannot get saved access tokens") },
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> performRequest(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
getTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>),
|
||||
refreshTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>)? = null,
|
||||
): Either<UniversalError, T> = either {
|
||||
runCatching {
|
||||
requestBlock("Bearer ${getTokens().bind().accessToken}").getOrThrow()
|
||||
}.getOrElse { error ->
|
||||
when (error) {
|
||||
is ApiResponseError.HttpException -> {
|
||||
if (refreshTokens != null && error.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) {
|
||||
refreshOrJoin(refreshTokens).bind()
|
||||
performRequest(requestBlock, refreshTokens = null, getTokens = getTokens).bind()
|
||||
} else {
|
||||
raise(mapHttpError(error))
|
||||
}
|
||||
}
|
||||
else -> raise(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
getTokens: (suspend () -> VisaAuthTokens),
|
||||
refreshTokens: (suspend () -> VisaAuthTokens)? = null,
|
||||
): T = runCatching {
|
||||
requestBlock("Bearer ${getTokens().accessToken}").getOrThrow()
|
||||
}.getOrElse { error ->
|
||||
val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED
|
||||
if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) {
|
||||
refreshOrJoin(refreshTokens)
|
||||
performRequest(requestBlock, refreshTokens = null, getTokens = getTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshOrJoin(
|
||||
refreshTokens: suspend () -> Either<UniversalError, VisaAuthTokens>,
|
||||
): Either<UniversalError, VisaAuthTokens> {
|
||||
val jobToAwait: Deferred<Either<UniversalError, VisaAuthTokens>> =
|
||||
private suspend fun refreshOrJoin(refreshTokens: suspend () -> VisaAuthTokens): VisaAuthTokens {
|
||||
val jobToAwait: Deferred<VisaAuthTokens> =
|
||||
refreshTokensMutex.withLock {
|
||||
val current = refreshTokensJob
|
||||
if (current == null || current.isCompleted) {
|
||||
|
|
@ -109,8 +109,6 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
}
|
||||
val result = try {
|
||||
jobToAwait.await()
|
||||
} catch (ignore: Throwable) {
|
||||
Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
} finally {
|
||||
refreshTokensMutex.withLock {
|
||||
if (refreshTokensJob === jobToAwait && jobToAwait.isCompleted) {
|
||||
|
|
@ -121,62 +119,67 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
return result
|
||||
}
|
||||
|
||||
private suspend fun getCustomerWalletAddress(): Either<UniversalError, String> = either {
|
||||
customerWalletAddress ?: fetchAuthInputData().bind().address
|
||||
suspend fun getCustomerWalletAddress(): String = customerWalletAddress ?: fetchAuthInputData().address
|
||||
|
||||
private suspend fun getAccessTokens(): VisaAuthTokens {
|
||||
return getAccessTokensIfSaved() ?: fetchTokens()
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
val address = getCustomerWalletAddress().bind()
|
||||
tangemPayStorage.getAuthTokens(address) ?: fetchTokens().bind()
|
||||
private suspend fun getAccessTokensIfSaved(): VisaAuthTokens? {
|
||||
return tangemPayStorage.getAuthTokens(getCustomerWalletAddress())
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokensIfSaved(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
tangemPayStorage.getAuthTokens(getCustomerWalletAddress().bind())
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
|
||||
private fun mapHttpError(throwable: ApiResponseError.HttpException): UniversalError {
|
||||
val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: throwable.code.numericCode
|
||||
}.map {
|
||||
VisaApiError.fromBackendError(it)
|
||||
}.getOrElse {
|
||||
VisaApiError.UnknownWithoutCode
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAuthInputData(): Either<UniversalError, AuthInputData> = either {
|
||||
private suspend fun fetchAuthInputData(): AuthInputData {
|
||||
val userWallets = getWalletsUseCase()
|
||||
.filter { it.isNotEmpty() }
|
||||
.first()
|
||||
val wallet = userWallets.find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
?: error("Cannot find cold user wallet")
|
||||
|
||||
val address = getCurrencyUseCase.invokeMultiWalletSync(wallet.walletId, CryptoCurrency.ID.fromValue(POL_VALUE))
|
||||
.getOrNull()?.value?.networkAddress?.defaultAddress?.value ?: raise(VisaApiError.UnknownWithoutCode)
|
||||
val address = getCurrencyUseCase.invokeMultiWallet(
|
||||
userWalletId = wallet.walletId,
|
||||
currencyId = CryptoCurrency.ID.fromValue(POL_VALUE),
|
||||
isSingleWalletWithTokens = false,
|
||||
)
|
||||
.filter { it.getAddress() != null }.first().getAddress() ?: error("Cannot find polygon network address")
|
||||
|
||||
customerWalletAddress = address
|
||||
|
||||
AuthInputData(address, wallet.cardId)
|
||||
return AuthInputData(address, wallet.cardId)
|
||||
}
|
||||
|
||||
private suspend fun fetchTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
val inputData = fetchAuthInputData().bind()
|
||||
private fun Either<CurrencyStatusError, CryptoCurrencyStatus>.getAddress() =
|
||||
getOrNull()?.value?.networkAddress?.defaultAddress?.value
|
||||
|
||||
private suspend fun fetchTokens(): VisaAuthTokens {
|
||||
val inputData = fetchAuthInputData()
|
||||
val tokens = authDataSource.generateNewAuthTokens(inputData.address, inputData.cardId)
|
||||
.getOrNull()
|
||||
?: return Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
.getOrNull() ?: error("Cannot fetch tokens")
|
||||
tangemPayStorage.storeAuthTokens(inputData.address, tokens)
|
||||
tokens
|
||||
return tokens
|
||||
}
|
||||
|
||||
private suspend fun refreshAuthTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
val customerWalletAddress = getCustomerWalletAddress().bind()
|
||||
val refreshToken = getAccessTokens().bind().refreshToken.value
|
||||
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull()
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
private suspend fun refreshAuthTokens(): VisaAuthTokens {
|
||||
val customerWalletAddress = getCustomerWalletAddress()
|
||||
val refreshToken = getAccessTokens().refreshToken.value
|
||||
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull() ?: error("Cannot refresh tokens")
|
||||
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
|
||||
tokens
|
||||
return tokens
|
||||
}
|
||||
|
||||
private fun mapError(throwable: Throwable): UniversalError {
|
||||
return if (throwable is ApiResponseError.HttpException) {
|
||||
val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: throwable.code.numericCode
|
||||
}.map {
|
||||
VisaApiError.fromBackendError(it)
|
||||
}.getOrElse {
|
||||
VisaApiError.UnknownWithoutCode
|
||||
}
|
||||
} else {
|
||||
VisaApiError.UnknownWithoutCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue