Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-20 10:27:00 +03:00
parent 36aaeca687
commit f27a28f0b7
18 changed files with 357 additions and 303 deletions

View file

@ -1,5 +1,8 @@
package com.tangem.data.visa
import arrow.core.Either
import arrow.core.getOrElse
import com.squareup.moshi.Moshi
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.data.visa.converter.VisaActivationStatusConverterWithState
import com.tangem.datasource.api.common.config.ApiConfig
@ -8,14 +11,11 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.visa.TangemVisaApi
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.request.*
import com.tangem.datasource.api.visa.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
@ -28,6 +28,7 @@ import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class DefaultVisaActivationRepository @AssistedInject constructor(
@Assisted private val visaCardId: VisaCardId,
@NetworkMoshi private val moshi: Moshi,
private val visaApi: TangemVisaApi,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
@ -36,27 +37,30 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
private val apiConfigsManager: ApiConfigsManager,
) : VisaActivationRepository {
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
val result = request {
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
visaApi.getRemoteActivationStatus(
authHeader = authTokens.getAuthHeader(),
request = ActivationStatusRequest(
cardId = visaCardId.cardId,
cardPublicKey = visaCardId.cardPublicKey,
),
).getOrThrow()
override suspend fun getActivationRemoteState(): Either<VisaApiError, VisaActivationRemoteState> =
withContext(dispatcherProvider.io) {
request {
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
visaApi.getRemoteActivationStatus(
authHeader = authTokens.getAuthHeader(),
request = ActivationStatusRequest(
cardId = visaCardId.cardId,
cardPublicKey = visaCardId.cardPublicKey,
),
).getOrThrow()
}.map {
VisaActivationStatusConverterWithState.convert(it)
}
}
VisaActivationStatusConverterWithState.convert(result)
}
override suspend fun getCardWalletAcceptanceData(
request: VisaCardWalletDataToSignRequest,
): VisaDataToSignByCardWallet = withContext(dispatcherProvider.io) {
val result = request {
): Either<VisaApiError, VisaDataToSignByCardWallet> = withContext(dispatcherProvider.io) {
request {
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
@ -67,18 +71,18 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
cardWalletAddress = request.cardWalletAddress,
),
).getOrThrow()
}.map {
VisaDataToSignByCardWallet(
request = request,
hashToSign = it.result.hash,
)
}
VisaDataToSignByCardWallet(
request = request,
hashToSign = result.result.hash,
)
}
override suspend fun getCustomerWalletAcceptanceData(
request: VisaCustomerWalletDataToSignRequest,
): VisaDataToSignByCustomerWallet = withContext(dispatcherProvider.io) {
val result = request {
): Either<VisaApiError, VisaDataToSignByCustomerWallet> = withContext(dispatcherProvider.io) {
request {
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
@ -89,15 +93,15 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
customerWalletAddress = request.customerWalletAddress,
),
).getOrThrow()
}.map {
VisaDataToSignByCustomerWallet(
request = request,
hashToSign = it.result.hash,
)
}
VisaDataToSignByCustomerWallet(
request = request,
hashToSign = result.result.hash,
)
}
override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) {
override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet): Either<VisaApiError, Unit> =
withContext(dispatcherProvider.io) {
request {
val authTokens =
@ -120,9 +124,8 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
).getOrThrow()
}
}
}
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) {
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) =
withContext(dispatcherProvider.io) {
request {
val authTokens =
@ -140,24 +143,21 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
).getOrThrow()
}
}
}
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {
withContext(dispatcherProvider.io) {
request {
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) = withContext(dispatcherProvider.io) {
request {
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
visaApi.setPinCode(
authHeader = authTokens.getAuthHeader(),
body = SetPinCodeRequest(
orderId = pinCode.activationOrderId,
sessionId = pinCode.sessionId,
iv = pinCode.iv,
pin = pinCode.encryptedPin,
),
).getOrThrow()
}
visaApi.setPinCode(
authHeader = authTokens.getAuthHeader(),
body = SetPinCodeRequest(
orderId = pinCode.activationOrderId,
sessionId = pinCode.sessionId,
iv = pinCode.iv,
pin = pinCode.encryptedPin,
),
).getOrThrow()
}
}
@ -172,24 +172,42 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
}
}
private suspend fun <T : Any> request(requestBlock: suspend () -> T): T {
private suspend fun <T : Any> request(requestBlock: suspend () -> T): Either<VisaApiError, T> {
return runCatching {
requestBlock()
Either.Right(requestBlock())
}.getOrElse { responseError ->
if (responseError !is ApiResponseError.HttpException ||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
if (responseError is ApiResponseError.HttpException &&
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED &&
responseError.errorBody != null
) {
throw responseError
val errorCode =
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
return Either.Left(VisaApiError.fromBackendError(errorCode))
}
if (responseError !is ApiResponseError.HttpException) {
return Either.Left(VisaApiError.UnknownWithoutCode)
}
val authTokens = visaAuthTokenStorage.get(visaCardId.cardId) ?: error("Auth tokens are not stored")
val newTokens = runCatching {
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
}.getOrElse { throw RefreshTokenExpiredException() }
val newTokens = visaAuthRepository.refreshAccessTokens(authTokens.refreshToken).getOrElse {
return Either.Left(VisaApiError.RefreshTokenExpired)
}
visaAuthTokenStorage.store(visaCardId.cardId, newTokens)
requestBlock()
runCatching {
Either.Right(requestBlock())
}.getOrElse { responseError ->
if (responseError is ApiResponseError.HttpException && responseError.errorBody != null) {
val errorCode =
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code
?: responseError.code.numericCode
Either.Left(VisaApiError.fromBackendError(errorCode))
} else {
Either.Left(VisaApiError.UnknownWithoutCode)
}
}
}
}

View file

@ -1,8 +1,14 @@
package com.tangem.data.visa
import arrow.core.Either
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.visa.TangemVisaApi
import com.tangem.datasource.api.visa.models.request.*
import com.tangem.datasource.api.visa.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaAuthSession
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
@ -14,40 +20,54 @@ import javax.inject.Inject
@Suppress("UnusedPrivateMember")
internal class DefaultVisaAuthRepository @Inject constructor(
@NetworkMoshi private val moshi: Moshi,
private val visaAuthApi: TangemVisaApi,
private val dispatchers: CoroutineDispatcherProvider,
) : VisaAuthRepository {
override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card =
withContext(dispatchers.io) {
val response = visaAuthApi.generateNonceByCardId(
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
override suspend fun getCardAuthChallenge(
cardId: String,
cardPublicKey: String,
): Either<VisaApiError, VisaAuthChallenge.Card> = withContext(dispatchers.io) {
request {
visaAuthApi.generateNonceByCardId(
GenerateNoneByCardIdRequest(
cardId = cardId,
cardPublicKey = cardPublicKey,
),
)
).getOrThrow()
}.map { response ->
VisaAuthChallenge.Card(
challenge = response.result.nonce,
session = VisaAuthSession(response.result.sessionId),
)
}
}
override suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet =
withContext(dispatchers.io) {
val response = visaAuthApi.generateNonceByCardWallet(
override suspend fun getCardWalletAuthChallenge(
cardWalletAddress: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
request {
visaAuthApi.generateNonceByCardWallet(
GenerateNoneByCardWalletRequest(
cardWalletAddress = cardWalletAddress,
),
)
).getOrThrow()
}.map { response ->
VisaAuthChallenge.Wallet(
challenge = response.result.nonce,
session = VisaAuthSession(response.result.sessionId),
)
}
}
override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens =
withContext(dispatchers.io) {
val response = when (signedChallenge) {
override suspend fun getAccessTokens(
signedChallenge: VisaAuthSignedChallenge,
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {
request {
when (signedChallenge) {
is VisaAuthSignedChallenge.ByCardPublicKey -> {
visaAuthApi.getAccessTokenByCardId(
GetAccessTokenByCardIdRequest(
@ -55,7 +75,7 @@ internal class DefaultVisaAuthRepository @Inject constructor(
signature = signedChallenge.signature,
salt = signedChallenge.salt,
),
)
).getOrThrow()
}
is VisaAuthSignedChallenge.ByWallet -> {
visaAuthApi.getAccessTokenByCardWallet(
@ -63,9 +83,10 @@ internal class DefaultVisaAuthRepository @Inject constructor(
sessionId = signedChallenge.challenge.session.sessionId,
signature = signedChallenge.signature,
),
)
).getOrThrow()
}
}
}.map { response ->
VisaAuthTokens(
accessToken = response.result.accessToken,
refreshToken = VisaAuthTokens.RefreshToken(
@ -77,10 +98,13 @@ internal class DefaultVisaAuthRepository @Inject constructor(
),
)
}
}
override suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens =
withContext(dispatchers.io) {
val response = when (refreshToken.authType) {
override suspend fun refreshAccessTokens(
refreshToken: VisaAuthTokens.RefreshToken,
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {
request {
when (refreshToken.authType) {
VisaAuthTokens.RefreshToken.Type.CardId ->
visaAuthApi.refreshCardIdAccessToken(
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
@ -90,10 +114,27 @@ internal class DefaultVisaAuthRepository @Inject constructor(
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
)
}.getOrThrow()
}.map { response ->
VisaAuthTokens(
accessToken = response.result.accessToken,
refreshToken = refreshToken.copy(value = response.result.refreshToken),
)
}
}
private suspend fun <T : Any> request(requestBlock: suspend () -> T): Either<VisaApiError, T> {
return runCatching {
Either.Right(requestBlock())
}.getOrElse { responseError ->
if (responseError is ApiResponseError.HttpException &&
responseError.errorBody != null
) {
val errorCode =
visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode
return Either.Left(VisaApiError.fromBackendError(errorCode))
}
return Either.Left(VisaApiError.UnknownWithoutCode)
}
}
}

View file

@ -1,59 +0,0 @@
package com.tangem.data.visa
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
class MockVisaActivationRepository @AssistedInject constructor(
@Assisted private val visaCardId: VisaCardId,
) : VisaActivationRepository {
override suspend fun getActivationRemoteState(): VisaActivationRemoteState {
return VisaActivationRemoteState.CardWalletSignatureRequired(
activationOrderInfo = VisaActivationOrderInfo(
orderId = "orderId",
customerId = "customerId",
customerWalletAddress = "customerWalletAddress",
cardWalletAddress = null,
),
)
}
override suspend fun getCardWalletAcceptanceData(
request: VisaCardWalletDataToSignRequest,
): VisaDataToSignByCardWallet {
return VisaDataToSignByCardWallet(
request = request,
hashToSign = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
)
}
override suspend fun getCustomerWalletAcceptanceData(
request: VisaCustomerWalletDataToSignRequest,
): VisaDataToSignByCustomerWallet {
return VisaDataToSignByCustomerWallet(
request = request,
CryptoUtils.generateRandomBytes(length = 32).toHexString(),
)
}
override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) {}
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) {}
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {}
override suspend fun getPinCodeRsaEncryptionPublicKey(): String {
return CryptoUtils.generateRandomBytes(length = 32).toHexString()
}
@AssistedFactory
interface Factory : VisaActivationRepository.Factory {
override fun create(cardId: VisaCardId): MockVisaActivationRepository
}
}