diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt index 1775e41665..e7c91d654c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt @@ -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 { + return requestHelper.runWithErrorLogs(TAG) { + val result = requestHelper.request { authHeader -> + tangemPayApi.getKycAccess(authHeader = authHeader) + }.result + + KycStartInfo(token = result.token, locale = result.locale) } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index ccd82930e5..c6f22818b8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -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>? = null + private var refreshTokensJob: Deferred? = null - suspend fun request(requestBlock: suspend (header: String) -> ApiResponse): Either = - either { - withContext(dispatchers.io) { - performRequest( - requestBlock = requestBlock, - getTokens = ::getAccessTokens, - refreshTokens = ::refreshAuthTokens, - ).bind() - } + private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) + + suspend fun runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either { + return try { + val result = requestBlock() + Either.Right(result) + } catch (exception: Exception) { + Timber.e("$tag: $exception") + Either.Left(mapError(exception)) } + } - suspend fun requestWithPersistedToken( - requestBlock: suspend (header: String) -> ApiResponse, - ): Either = either { + suspend fun request(requestBlock: suspend (header: String) -> ApiResponse): T = withContext(dispatchers.io) { performRequest( requestBlock = requestBlock, - getTokens = ::getAccessTokensIfSaved, + getTokens = ::getAccessTokens, refreshTokens = ::refreshAuthTokens, - ).bind() + ) + } + + suspend fun requestWithPersistedToken(requestBlock: suspend (header: String) -> ApiResponse): T = + withContext(dispatchers.io) { + performRequest( + requestBlock = requestBlock, + getTokens = { getAccessTokensIfSaved() ?: error("Cannot get saved access tokens") }, + refreshTokens = ::refreshAuthTokens, + ) } - } private suspend fun performRequest( requestBlock: suspend (header: String) -> ApiResponse, - getTokens: (suspend () -> Either), - refreshTokens: (suspend () -> Either)? = null, - ): Either = 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, - ): Either { - val jobToAwait: Deferred> = + private suspend fun refreshOrJoin(refreshTokens: suspend () -> VisaAuthTokens): VisaAuthTokens { + val jobToAwait: Deferred = 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 = 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 = either { - val address = getCustomerWalletAddress().bind() - tangemPayStorage.getAuthTokens(address) ?: fetchTokens().bind() + private suspend fun getAccessTokensIfSaved(): VisaAuthTokens? { + return tangemPayStorage.getAuthTokens(getCustomerWalletAddress()) } - private suspend fun getAccessTokensIfSaved(): Either = 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 = 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 = either { - val inputData = fetchAuthInputData().bind() + private fun Either.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 = 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 + } } }