diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 3b07d873a0..c03a98f5ef 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -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 = 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 { diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index e31948a49c..551cffb6e1 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -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 { - 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) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt index 346e7a74fe..74f2e49b69 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt @@ -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 @POST("v1/auth/challenge") - suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse + suspend fun generateNonceByCardWallet( + @Body request: GenerateNoneByCardWalletRequest, + ): ApiResponse @POST("v1/auth/token") - suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse + suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse @POST("v1/auth/token") - suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse + suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse @POST("v1/auth/token/refresh") suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaErrorResponse.kt new file mode 100644 index 0000000000..49a5050d0c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaErrorResponse.kt @@ -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, + ) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index 3f2c1d8a90..40c5d49fe6 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -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 = + 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 = 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 = 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 = 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 request(requestBlock: suspend () -> T): T { + private suspend fun request(requestBlock: suspend () -> T): Either { 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) + } + } } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index daa8bd755d..91f736357e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -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 = 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 = 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 = 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 = 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 request(requestBlock: suspend () -> T): Either { + 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) + } + } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt deleted file mode 100644 index 57bddf8b6c..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index dab90f1ecd..307bb93cbd 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -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) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index 6ddfd6c0d7..2605ba3676 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -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) + } + } + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt index c15d97e074..20f95b2d40 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt @@ -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 = Either.catch { + ): Either = 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") diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt index 339f8c7e9c..d0917b47be 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt @@ -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 - suspend fun getCardWalletAcceptanceData(request: VisaCardWalletDataToSignRequest): VisaDataToSignByCardWallet + suspend fun getCardWalletAcceptanceData( + request: VisaCardWalletDataToSignRequest, + ): Either suspend fun getCustomerWalletAcceptanceData( request: VisaCustomerWalletDataToSignRequest, - ): VisaDataToSignByCustomerWallet + ): Either - suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) + suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet): Either - suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) + suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet): Either - suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) + suspend fun sendPinCode(pinCode: VisaEncryptedPinCode): Either suspend fun getPinCodeRsaEncryptionPublicKey(): String diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index 63c1f2d4bf..09c6138066 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -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 - suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet + suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): Either - suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens + suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either - suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens + suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 6c1dcc74e1..c04c38dd80 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -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) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt index 31f7aa82ed..e6da4d698e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -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) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 790b909fda..fe81644d9d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -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) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 28fca46909..13e71e07f0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -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() @@ -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) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 3deb78230c..eed9850228 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -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 } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt index 7c602c0e14..fc349134d4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt @@ -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) } }