Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-22 16:12:30 +05:00
parent 3ffffa73ec
commit 616386c345
18 changed files with 310 additions and 125 deletions

View file

@ -4,9 +4,8 @@ import arrow.core.Either
import arrow.core.raise.either
import com.tangem.common.CompletionResult
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.api.TangemSdkManager
import javax.inject.Inject
@ -16,29 +15,14 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
) : TangemPayAuthDataSource {
override suspend fun generateNewAuthTokens(address: String, cardId: String): Either<Throwable, VisaAuthTokens> =
either {
val challenge = visaAuthRemoteDataSource
.getCustomerWalletAuthChallenge(address)
.mapLeft { IllegalStateException("TangemPay challenge failed. Error code: ${it.errorCode}") }
.bind()
override suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> {
val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)
val signed = tangemSdkManager.visaCustomerWalletApprove(
VisaDataForApprove(
customerWalletCardId = cardId,
targetAddress = address,
dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge),
),
).toEither { IllegalStateException("TangemPay signing failed: $it") }.bind()
visaAuthRemoteDataSource.getTokenWithCustomerWallet(
sessionId = challenge.session.sessionId,
signature = signed.signature,
nonce = signed.dataToSign.hashToSign,
)
.mapLeft { IllegalStateException("TangemPay token fetch failed. Error code: ${it.errorCode}") }
.bind()
return when (initialCredentials) {
is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error)
is CompletionResult.Success<TangemPayInitialCredentials> -> Either.Right(initialCredentials.data)
}
}
override suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens> = either {
visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens(
@ -47,9 +31,4 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
.mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") }
.bind()
}
}
private fun <T> CompletionResult<T>.toEither(map: (Throwable) -> Throwable) = when (this) {
is CompletionResult.Success -> Either.Right(data)
is CompletionResult.Failure -> Either.Left(map(error))
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
import com.tangem.datasource.api.pay.models.request.OrderRequest
@ -30,9 +31,9 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
return requestHelper.runWithErrorLogs(TAG) {
val result = requestHelper.request {
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
}.result
val result = tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
.getOrThrow()
.result
result?.status == VALID_STATUS
}
}

View file

@ -2,9 +2,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.error.UniversalError
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.datasource.api.common.response.ApiResponse
@ -14,10 +12,10 @@ import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -30,11 +28,9 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val tangemPayStorage: TangemPayStorage,
private val authDataSource: TangemPayAuthDataSource,
private val tangemPayWalletsManager: TangemPayWalletsManager,
private val walletManagersFacade: WalletManagersFacade,
private val networkFactory: NetworkFactory,
) {
private var customerWalletAddress: String? = null
private val customerWalletAddress = MutableStateFlow<String?>(null)
private val refreshTokensMutex = Mutex()
private var refreshTokensJob: Deferred<VisaAuthTokens>? = null
@ -81,7 +77,9 @@ internal class TangemPayRequestPerformer @Inject constructor(
getTokens: (suspend () -> VisaAuthTokens),
refreshTokens: (suspend () -> VisaAuthTokens)? = null,
): T = runCatching {
requestBlock("Bearer ${getTokens().accessToken}").getOrThrow()
val tokens = getTokens()
val header = tokens.getAuthHeader()
requestBlock(header).getOrThrow()
}.getOrElse { error ->
val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED
if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) {
@ -116,7 +114,18 @@ internal class TangemPayRequestPerformer @Inject constructor(
return result
}
suspend fun getCustomerWalletAddress(): String = customerWalletAddress ?: fetchAuthInputData().address
suspend fun getCustomerWalletAddress(): String {
val existingAddress = customerWalletAddress.value
if (existingAddress != null) {
return existingAddress
}
val storedAddress = tangemPayStorage.getCustomerWalletAddress(
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId,
) ?: error("Can not find customer address")
customerWalletAddress.value = storedAddress
return storedAddress
}
private suspend fun getAccessTokens(): VisaAuthTokens {
return getAccessTokensIfSaved() ?: fetchTokens()
@ -126,42 +135,33 @@ internal class TangemPayRequestPerformer @Inject constructor(
return tangemPayStorage.getAuthTokens(getCustomerWalletAddress())
}
private suspend fun fetchAuthInputData(): AuthInputData {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
val network = networkFactory.create(
blockchain = Blockchain.Polygon,
extraDerivationPath = null,
derivationStyleProvider = wallet.derivationStyleProvider,
canHandleTokens = true,
) ?: error("Cannot create network")
val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network)
?: error("Cannot get polygon address")
customerWalletAddress = address
return AuthInputData(address, wallet.cardId)
}
private suspend fun fetchTokens(): VisaAuthTokens {
val inputData = fetchAuthInputData()
val tokens = authDataSource.generateNewAuthTokens(inputData.address, inputData.cardId)
.getOrNull() ?: error("Cannot fetch tokens")
tangemPayStorage.storeAuthTokens(inputData.address, tokens)
return tokens
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId)
.getOrThrowWithMessage("Can not produce initial data:")
tangemPayStorage.storeCustomerWalletAddress(
userWalletId = wallet.walletId,
customerWalletAddress = initialCredentials.customerWalletAddress,
)
tangemPayStorage.storeAuthTokens(
customerWalletAddress = initialCredentials.customerWalletAddress,
tokens = initialCredentials.authTokens,
)
return initialCredentials.authTokens
}
private suspend fun refreshAuthTokens(): VisaAuthTokens {
val customerWalletAddress = getCustomerWalletAddress()
val refreshToken = getAccessTokens().refreshToken.value
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull() ?: error("Cannot refresh tokens")
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrThrowWithMessage("Cannot refresh tokens:")
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
return tokens
}
}
internal data class AuthInputData(
val address: String,
val cardId: String,
)
private fun <A : Throwable, B> Either<A, B>.getOrThrowWithMessage(message: String): B {
return this.fold(
ifLeft = { error -> throw IllegalStateException("$message ${error.message}") },
ifRight = { it },
)
}
}