Updated on 2026-08-14
This commit is contained in:
parent
36aaeca687
commit
f27a28f0b7
18 changed files with 357 additions and 303 deletions
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.domain.tasks.visa
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.blockchain.common.UnmarshalHelper
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -21,7 +20,6 @@ import com.tangem.datasource.local.visa.hasSavedOTP
|
|||
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
|
|
@ -170,28 +168,24 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
signedChallenge: VisaAuthSignedChallenge,
|
||||
cardWalletAddress: String,
|
||||
): Either<TangemError, VisaDataToSignByCardWallet> = either {
|
||||
catch(
|
||||
block = {
|
||||
val tokens = visaAuthRepository.getAccessTokens(signedChallenge)
|
||||
val tokens = visaAuthRepository.getAccessTokens(signedChallenge)
|
||||
.getOrElse { raise(it.tangemError) }
|
||||
|
||||
visaAuthTokenStorage.store(cardId, tokens)
|
||||
visaAuthTokenStorage.store(cardId, tokens)
|
||||
|
||||
val remoteState = visaActivationRepository.getActivationRemoteState()
|
||||
if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) {
|
||||
raise(VisaActivationError.WrongRemoteState.tangemError)
|
||||
}
|
||||
val remoteState = visaActivationRepository.getActivationRemoteState()
|
||||
.getOrElse { raise(it.tangemError) }
|
||||
|
||||
visaActivationRepository.getCardWalletAcceptanceData(
|
||||
VisaCardWalletDataToSignRequest(
|
||||
activationOrderInfo = remoteState.activationOrderInfo,
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
),
|
||||
)
|
||||
},
|
||||
catch = {
|
||||
raise(VisaAuthorizationAPIError.tangemError)
|
||||
},
|
||||
)
|
||||
if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) {
|
||||
return raise(VisaActivationError.WrongRemoteState.tangemError)
|
||||
}
|
||||
|
||||
visaActivationRepository.getCardWalletAcceptanceData(
|
||||
VisaCardWalletDataToSignRequest(
|
||||
activationOrderInfo = remoteState.activationOrderInfo,
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
),
|
||||
).getOrElse { raise(it.tangemError) }
|
||||
}
|
||||
|
||||
private suspend fun SessionContext.createWallet(): CompletionResult<Unit> {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.core.error.ext.tangemError
|
|||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.error.VisaCardScanError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
|
|
@ -82,16 +82,12 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
}
|
||||
|
||||
Timber.i("Requesting challenge for wallet authorization")
|
||||
val challengeResponse = runCatching {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
error("sign and get specific error to switch to card_id flow")
|
||||
visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
|
||||
}.getOrElse {
|
||||
Timber.i("Failed to get Access token for Wallet public key authorization. Authorizing using Card Pub key")
|
||||
return handleCardAuthorization(
|
||||
cardWalletAddress = walletAddress.value,
|
||||
)
|
||||
}
|
||||
|
||||
val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
|
||||
.getOrElse {
|
||||
Timber.i("Failed to get Access token for Wallet public key authorization")
|
||||
return CompletionResult.Failure(it.tangemError)
|
||||
}
|
||||
|
||||
val signChallengeResult = signChallengeWithWallet(
|
||||
publicKey = wallet.publicKey,
|
||||
|
|
@ -118,14 +114,19 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
cardWalletAddress: String,
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
val authorizationTokensResponse = runCatching {
|
||||
visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
|
||||
}.getOrElse {
|
||||
Timber.i("Failed to get Access token for Wallet public key authorization. Authorizing using Card Pub key")
|
||||
return handleCardAuthorization(
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
)
|
||||
}
|
||||
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
|
||||
.getOrElse {
|
||||
Timber.i("Failed to get Access token for Wallet public key authorization.")
|
||||
return if (
|
||||
it is VisaApiError.ProductInstanceIsNotActivated ||
|
||||
it is VisaApiError.ProductInstanceNotFoundActivationRequired
|
||||
) {
|
||||
Timber.i("Proceeding with card authorization.")
|
||||
handleCardAuthorization(cardWalletAddress = cardWalletAddress)
|
||||
} else {
|
||||
CompletionResult.Failure(it.tangemError)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.i("Authorized using Wallet public key successfully")
|
||||
|
||||
|
|
@ -140,14 +141,12 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
|
||||
Timber.i("Requesting authorization challenge to sign")
|
||||
|
||||
val challengeResponse = runCatching {
|
||||
visaAuthRepository.getCardAuthChallenge(
|
||||
cardId = card.cardId,
|
||||
cardPublicKey = card.cardPublicKey.toHexString(),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
|
||||
val challengeResponse = visaAuthRepository.getCardAuthChallenge(
|
||||
cardId = card.cardId,
|
||||
cardPublicKey = card.cardPublicKey.toHexString(),
|
||||
).getOrElse {
|
||||
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.errorCode}")
|
||||
return CompletionResult.Failure(it.tangemError)
|
||||
}
|
||||
|
||||
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
|
||||
|
|
@ -168,16 +167,14 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
val authorizationTokensResponse = runCatching {
|
||||
visaAuthRepository.getAccessTokens(
|
||||
signedChallenge = challengeResponse.toSignedChallenge(
|
||||
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
|
||||
salt = attestCardKeyResponse.salt.toHexString(),
|
||||
),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
|
||||
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(
|
||||
signedChallenge = challengeResponse.toSignedChallenge(
|
||||
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
|
||||
salt = attestCardKeyResponse.salt.toHexString(),
|
||||
),
|
||||
).getOrElse {
|
||||
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}")
|
||||
return CompletionResult.Failure(it.tangemError)
|
||||
}
|
||||
|
||||
visaAuthTokenStorage.store(
|
||||
|
|
@ -185,11 +182,9 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
tokens = authorizationTokensResponse,
|
||||
)
|
||||
|
||||
val activationRemoteState = runCatching {
|
||||
visaActivationRepository.getActivationRemoteState()
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
|
||||
val activationRemoteState = visaActivationRepository.getActivationRemoteState().getOrElse {
|
||||
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}")
|
||||
return CompletionResult.Failure(it.tangemError)
|
||||
}
|
||||
|
||||
val error = when (activationRemoteState) {
|
||||
|
|
|
|||
|
|
@ -25,16 +25,18 @@ interface TangemVisaApi {
|
|||
// region: auth
|
||||
|
||||
@POST("v1/auth/challenge")
|
||||
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse
|
||||
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse<GenerateNonceResponse>
|
||||
|
||||
@POST("v1/auth/challenge")
|
||||
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse
|
||||
suspend fun generateNonceByCardWallet(
|
||||
@Body request: GenerateNoneByCardWalletRequest,
|
||||
): ApiResponse<GenerateNonceResponse>
|
||||
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse
|
||||
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
|
||||
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse
|
||||
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
|
||||
|
||||
@POST("v1/auth/token/refresh")
|
||||
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VisaErrorResponse(
|
||||
@Json(name = "error") val error: Error,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Error(
|
||||
@Json(name = "code") val code: Int,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ dependencies {
|
|||
|
||||
/** Project - Domain */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.error)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import com.tangem.core.error.UniversalError
|
|||
* zzz - Specific error code
|
||||
* If you need to add new subsystem add it to list below incrementing last code.
|
||||
* `Subsystems`:
|
||||
* `001` - Common API
|
||||
* `002` - First card scan error
|
||||
* `003` - Activation
|
||||
* `004` - Authorization API
|
||||
|
|
@ -20,10 +19,6 @@ object VisaError : UniversalError {
|
|||
override val errorCode: Int = 104000000
|
||||
}
|
||||
|
||||
object VisaAPIError : UniversalError {
|
||||
override val errorCode: Int = 104001000
|
||||
}
|
||||
|
||||
enum class VisaCardScanError(
|
||||
override val errorCode: Int,
|
||||
) : UniversalError {
|
||||
|
|
@ -47,8 +42,36 @@ enum class VisaActivationError(
|
|||
FailedRemoteState(104003010),
|
||||
VisaCardForApproval(104003011),
|
||||
CardIdNotMatched(104003011),
|
||||
FailedToSetPinCode(104003012),
|
||||
}
|
||||
|
||||
object VisaAuthorizationAPIError : UniversalError {
|
||||
override val errorCode: Int = 104004000
|
||||
sealed class VisaApiError(
|
||||
override val errorCode: Int,
|
||||
) : UniversalError {
|
||||
// TODO codes may change
|
||||
data object Unspecified : VisaApiError(104100000)
|
||||
data object ProductInstanceNotFoundActivationRequired : VisaApiError(104100100)
|
||||
data object ProductInstanceIsBlocked : VisaApiError(104101000)
|
||||
data object ProductInstanceIsNotActivated : VisaApiError(104101100)
|
||||
data object ProductInstanceIsAlreadyActivated : VisaApiError(104101200)
|
||||
data object CustomerIsBlocked : VisaApiError(104102000)
|
||||
data object UnknownWithoutCode : VisaApiError(104101999)
|
||||
data class Unknown(override val errorCode: Int) : VisaApiError(errorCode)
|
||||
|
||||
data object RefreshTokenExpired : VisaApiError(104004001)
|
||||
|
||||
companion object {
|
||||
fun fromBackendError(backendErrorCode: Int): VisaApiError {
|
||||
val universalErrorCode = 104_000_000 + backendErrorCode
|
||||
return when (universalErrorCode) {
|
||||
104100000 -> Unspecified
|
||||
104100100 -> ProductInstanceNotFoundActivationRequired
|
||||
104101000 -> ProductInstanceIsBlocked
|
||||
104101100 -> ProductInstanceIsNotActivated
|
||||
104101200 -> ProductInstanceIsAlreadyActivated
|
||||
104102000 -> CustomerIsBlocked
|
||||
else -> Unknown(universalErrorCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ package com.tangem.domain.visa
|
|||
|
||||
import android.util.Base64
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.model.VisaEncryptedPinCode
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
|
|
@ -25,7 +27,7 @@ class SetVisaPinCodeUseCase(private val visaActivationRepositoryFactory: VisaAct
|
|||
visaCardId: VisaCardId,
|
||||
activationOrderId: String,
|
||||
pinCode: String,
|
||||
): Either<Throwable, Unit> = Either.catch {
|
||||
): Either<UniversalError, Unit> = runCatching {
|
||||
val visaActivationRepository = visaActivationRepositoryFactory.create(visaCardId)
|
||||
val rsaPublicKey = visaActivationRepository.getPinCodeRsaEncryptionPublicKey()
|
||||
val formattedPin = "24$pinCode${"f".repeat(n = 8)}FF"
|
||||
|
|
@ -48,7 +50,7 @@ class SetVisaPinCodeUseCase(private val visaActivationRepositoryFactory: VisaAct
|
|||
encryptedPin = encryptedPin,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.getOrElse { Either.Left(VisaActivationError.FailedToSetPinCode) }
|
||||
|
||||
private fun generateSessionKey(): Key {
|
||||
val generator = KeyGenerator.getInstance("AES")
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
package com.tangem.domain.visa.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.*
|
||||
|
||||
interface VisaActivationRepository {
|
||||
|
||||
suspend fun getActivationRemoteState(): VisaActivationRemoteState
|
||||
suspend fun getActivationRemoteState(): Either<VisaApiError, VisaActivationRemoteState>
|
||||
|
||||
suspend fun getCardWalletAcceptanceData(request: VisaCardWalletDataToSignRequest): VisaDataToSignByCardWallet
|
||||
suspend fun getCardWalletAcceptanceData(
|
||||
request: VisaCardWalletDataToSignRequest,
|
||||
): Either<VisaApiError, VisaDataToSignByCardWallet>
|
||||
|
||||
suspend fun getCustomerWalletAcceptanceData(
|
||||
request: VisaCustomerWalletDataToSignRequest,
|
||||
): VisaDataToSignByCustomerWallet
|
||||
): Either<VisaApiError, VisaDataToSignByCustomerWallet>
|
||||
|
||||
suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet)
|
||||
suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet): Either<VisaApiError, Unit>
|
||||
|
||||
suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet)
|
||||
suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet): Either<VisaApiError, Unit>
|
||||
|
||||
suspend fun sendPinCode(pinCode: VisaEncryptedPinCode)
|
||||
suspend fun sendPinCode(pinCode: VisaEncryptedPinCode): Either<VisaApiError, Unit>
|
||||
|
||||
suspend fun getPinCodeRsaEncryptionPublicKey(): String
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
package com.tangem.domain.visa.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
|
||||
interface VisaAuthRepository {
|
||||
|
||||
suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card
|
||||
suspend fun getCardAuthChallenge(
|
||||
cardId: String,
|
||||
cardPublicKey: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Card>
|
||||
|
||||
suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet
|
||||
suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): Either<VisaApiError, VisaAuthChallenge.Wallet>
|
||||
|
||||
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens
|
||||
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either<VisaApiError, VisaAuthTokens>
|
||||
|
||||
suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens
|
||||
suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either<VisaApiError, VisaAuthTokens>
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -9,10 +10,9 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.core.error.ext.universalError
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.visa.error.VisaAPIError
|
||||
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
|
||||
|
|
@ -153,14 +153,11 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
loading(true)
|
||||
|
||||
modelScope.launch {
|
||||
val challengeToSign = runCatching {
|
||||
visaAuthRepository.getCardAuthChallenge(
|
||||
cardId = activationInput.cardId,
|
||||
cardPublicKey = activationInput.cardPublicKey,
|
||||
)
|
||||
}.getOrElse {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
|
||||
val challengeToSign = visaAuthRepository.getCardAuthChallenge(
|
||||
cardId = activationInput.cardId,
|
||||
cardPublicKey = activationInput.cardPublicKey,
|
||||
).getOrElse {
|
||||
onError(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
@ -174,23 +171,17 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
|
||||
val resultData = when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(result.error.universalError)
|
||||
analyticsEventsHandler.send(
|
||||
VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError),
|
||||
)
|
||||
onError(result.error.universalError)
|
||||
return@launch
|
||||
}
|
||||
is CompletionResult.Success -> result.data
|
||||
}
|
||||
|
||||
runCatching {
|
||||
visaActivationRepository.activateCard(resultData.signedActivationData)
|
||||
}.onFailure {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAPIError)
|
||||
return@launch
|
||||
}
|
||||
visaActivationRepository.activateCard(resultData.signedActivationData)
|
||||
.onLeft {
|
||||
onError(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val targetAddress =
|
||||
result.data.signedActivationData.dataToSign.request.activationOrderInfo.customerWalletAddress
|
||||
|
|
@ -213,6 +204,12 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onError(error: UniversalError) {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(error)
|
||||
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(error))
|
||||
}
|
||||
|
||||
private fun loading(state: Boolean) {
|
||||
_uiState.update { it.copy(buttonLoading = state) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.approve.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -8,9 +9,9 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.core.error.ext.universalError
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.visa.error.VisaAPIError
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
|
|
@ -67,13 +68,12 @@ internal class OnboardingVisaApproveModel @Inject constructor(
|
|||
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove)
|
||||
|
||||
modelScope.launch {
|
||||
val dataToSign = runCatching {
|
||||
val dataToSign =
|
||||
visaActivationRepository.getCustomerWalletAcceptanceData(params.preparationDataForApprove.request)
|
||||
}.getOrElse {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAPIError)
|
||||
return@launch
|
||||
}
|
||||
.getOrElse {
|
||||
onError(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val result = tangemSdkManager.visaCustomerWalletApprove(
|
||||
visaDataForApprove = VisaDataForApprove(
|
||||
|
|
@ -85,26 +85,28 @@ internal class OnboardingVisaApproveModel @Inject constructor(
|
|||
|
||||
val resultData = when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(result.error.universalError)
|
||||
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
|
||||
onError(result.error.universalError)
|
||||
return@launch
|
||||
}
|
||||
is CompletionResult.Success -> result.data
|
||||
}
|
||||
|
||||
runCatching {
|
||||
visaActivationRepository.approveByCustomerWallet(resultData)
|
||||
}.onFailure {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAPIError)
|
||||
return@launch
|
||||
}
|
||||
visaActivationRepository.approveByCustomerWallet(resultData)
|
||||
.onLeft {
|
||||
onError(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
onDone.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(error: UniversalError) {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(error)
|
||||
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(error))
|
||||
}
|
||||
|
||||
private fun loading(state: Boolean) {
|
||||
_uiState.update { it.copy(approveButtonLoading = state) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
|
|
@ -12,8 +13,10 @@ import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
|||
import com.tangem.datasource.local.visa.VisaOTPStorage
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
|
|
@ -64,9 +67,13 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
private fun runShortPolling() {
|
||||
modelScope.launch {
|
||||
while (true) {
|
||||
val result = runCatching {
|
||||
visaActivationRepository.getActivationRemoteState()
|
||||
}.getOrNull() ?: continue
|
||||
val result = visaActivationRepository.getActivationRemoteState()
|
||||
.getOrElse {
|
||||
uiMessageSender.showErrorDialog(it)
|
||||
delay(timeMillis = 2000)
|
||||
runShortPolling()
|
||||
return@launch
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is VisaActivationRemoteState.CardWalletSignatureRequired,
|
||||
|
|
@ -156,12 +163,11 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
val authTokens = visaAuthTokenStorage.get(params.scanResponse.card.cardId)
|
||||
?: error("Auth tokens are not found. This should not happen.")
|
||||
|
||||
val newTokens = runCatching {
|
||||
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
|
||||
}.getOrElse {
|
||||
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
|
||||
return
|
||||
}
|
||||
val newTokens = visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
|
||||
.getOrElse {
|
||||
uiMessageSender.showErrorDialog(it)
|
||||
return
|
||||
}
|
||||
|
||||
val userWallet = createUserWallet(params.scanResponse, newTokens)
|
||||
userWalletsListManager.save(userWallet)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.visa.model.VisaActivationOrderInfo
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
|
|
@ -15,6 +17,7 @@ import com.tangem.domain.visa.repository.VisaActivationRepository
|
|||
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
|
@ -25,6 +28,7 @@ import javax.inject.Inject
|
|||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class OnboardingVisaOtherWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
visaActivationRepositoryFactory: VisaActivationRepository.Factory,
|
||||
|
|
@ -32,6 +36,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val config = paramsContainer.require<OnboardingVisaOtherWalletComponent.Config>()
|
||||
|
|
@ -50,14 +55,18 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
|
|||
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened)
|
||||
modelScope.launch {
|
||||
while (true) {
|
||||
val result = runCatching {
|
||||
visaActivationRepository.getActivationRemoteState()
|
||||
}.getOrNull()
|
||||
|
||||
if (result is VisaActivationRemoteState.AwaitingPinCode) {
|
||||
onDone.emit(result.activationOrderInfo)
|
||||
break
|
||||
}
|
||||
visaActivationRepository.getActivationRemoteState()
|
||||
.onLeft {
|
||||
uiMessageSender.showErrorDialog(it)
|
||||
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(it))
|
||||
delay(timeMillis = 60_000)
|
||||
}
|
||||
.onRight {
|
||||
if (it is VisaActivationRemoteState.AwaitingPinCode) {
|
||||
onDone.emit(it.activationOrderInfo)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
delay(timeMillis = 2000)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.visa.SetVisaPinCodeUseCase
|
||||
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent
|
||||
|
|
@ -95,7 +94,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
activationOrderId = params.activationOrderInfo.orderId,
|
||||
).onLeft {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
|
||||
uiMessageSender.showErrorDialog(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -7,9 +8,9 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.core.error.ext.universalError
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.visa.error.VisaAPIError
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
|
|
@ -78,13 +79,11 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
|
|||
loading(true)
|
||||
|
||||
modelScope.launch {
|
||||
val dataToSignByCardWallet = runCatching {
|
||||
visaActivationRepository.getCardWalletAcceptanceData(params.dataToSignRequest)
|
||||
}.getOrElse {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAPIError)
|
||||
return@launch
|
||||
}
|
||||
val dataToSignByCardWallet = visaActivationRepository.getCardWalletAcceptanceData(params.dataToSignRequest)
|
||||
.getOrElse {
|
||||
onError(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val result = tangemSdkManager.activateVisaCard(
|
||||
mode = VisaCardActivationTaskMode.SignOnly(dataToSignByCardWallet = dataToSignByCardWallet),
|
||||
|
|
@ -93,21 +92,17 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
|
|||
|
||||
val resultData = when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(result.error.universalError)
|
||||
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
|
||||
onError(result.error.universalError)
|
||||
return@launch
|
||||
}
|
||||
is CompletionResult.Success -> result.data
|
||||
}
|
||||
|
||||
runCatching {
|
||||
visaActivationRepository.activateCard(resultData.signedActivationData)
|
||||
}.onFailure {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAPIError)
|
||||
return@launch
|
||||
}
|
||||
visaActivationRepository.activateCard(resultData.signedActivationData)
|
||||
.onLeft {
|
||||
onError(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val request = result.data.signedActivationData.dataToSign.request
|
||||
val targetAddress = request.activationOrderInfo.customerWalletAddress
|
||||
|
|
@ -132,6 +127,12 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onError(error: UniversalError) {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(error)
|
||||
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(error))
|
||||
}
|
||||
|
||||
private fun loading(state: Boolean) {
|
||||
_uiState.update { it.copy(continueButtonLoading = state) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue