diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt index 85c60d494d..f752d10dea 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt @@ -1,8 +1,11 @@ package com.tangem.tap.data import android.content.Context +import com.tangem.common.extensions.toByteArray +import com.tangem.common.extensions.toInt import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.visa.VisaOTPStorage +import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -24,19 +27,27 @@ class DefaultVisaOTPStorage @Inject constructor( ), ) - override suspend fun saveOTP(cardId: String, otp: ByteArray) = withContext(dispatcherProvider.io) { - secureStorage.store(otp, VISA_OTP_KEY_PREFIX + cardId) + override suspend fun saveOTP(cardId: String, data: VisaOtpData) = withContext(dispatcherProvider.io) { + secureStorage.store(data.rootOTP, VISA_ROOT_OTP_KEY_PREFIX + cardId) + secureStorage.store(data.counter.toByteArray(), VISA_OTP_COUNTER_KEY_PREFIX + cardId) } - override suspend fun getOTP(cardId: String): ByteArray? = withContext(dispatcherProvider.io) { - secureStorage.get(VISA_OTP_KEY_PREFIX + cardId) + override suspend fun getOTP(cardId: String): VisaOtpData? = withContext(dispatcherProvider.io) { + val rootOTP = secureStorage.get(VISA_ROOT_OTP_KEY_PREFIX + cardId) ?: return@withContext null + val counter = secureStorage.get(VISA_OTP_COUNTER_KEY_PREFIX + cardId)?.toInt() ?: return@withContext null + VisaOtpData( + rootOTP = rootOTP, + counter = counter, + ) } override suspend fun removeOTP(cardId: String) = withContext(dispatcherProvider.io) { - secureStorage.delete(VISA_OTP_KEY_PREFIX + cardId) + secureStorage.delete(VISA_ROOT_OTP_KEY_PREFIX + cardId) + secureStorage.delete(VISA_OTP_COUNTER_KEY_PREFIX + cardId) } private companion object { - const val VISA_OTP_KEY_PREFIX = "visa_otp_" + const val VISA_ROOT_OTP_KEY_PREFIX = "visa_root_otp_" + const val VISA_OTP_COUNTER_KEY_PREFIX = "visa_otp_counter_" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 4cda23bbe9..d0f4cc5985 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -25,8 +25,8 @@ import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask @@ -35,12 +35,12 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.preflightread.PreflightReadFilter -import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse +import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask @@ -480,15 +480,13 @@ internal class DefaultTangemSdkManager( // region Visa-specific override suspend fun activateVisaCard( - accessCode: String, - challengeToSign: VisaAuthChallenge.Card?, + mode: VisaCardActivationTaskMode, activationInput: VisaActivationInput, ): CompletionResult { return coroutineScope { runTaskAsyncReturnOnMain( runnable = visaCardActivationTaskFactory.create( - accessCode = accessCode, - challengeToSign = challengeToSign, + mode = mode, activationInput = activationInput, coroutineScope = this, ), @@ -499,7 +497,7 @@ internal class DefaultTangemSdkManager( override suspend fun visaCustomerWalletApprove( visaDataForApprove: VisaDataForApprove, - ): CompletionResult { + ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = VisaCustomerWalletApproveTask( visaDataForApprove = visaDataForApprove, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index b6291e7a12..bf29d6cfd3 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -18,16 +18,16 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter -import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse +import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.domain.sdk.mocks.MockProvider @Suppress("TooManyFunctions") @@ -203,8 +203,7 @@ class MockTangemSdkManager( // region Visa-specific override suspend fun activateVisaCard( - accessCode: String, - challengeToSign: VisaAuthChallenge.Card?, + mode: VisaCardActivationTaskMode, activationInput: VisaActivationInput, ): CompletionResult { error("Not implemented") @@ -212,7 +211,7 @@ class MockTangemSdkManager( override suspend fun visaCustomerWalletApprove( visaDataForApprove: VisaDataForApprove, - ): CompletionResult { + ): CompletionResult { error("Not implemented") } 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 920c63ac0e..379634ea9c 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 @@ -1,5 +1,9 @@ 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.common.CompletionResult import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable @@ -12,8 +16,10 @@ import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource import com.tangem.crypto.CryptoUtils import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage +import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.datasource.local.visa.hasSavedOTP import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository @@ -25,19 +31,18 @@ import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletTask import com.tangem.sdk.api.visa.VisaCardActivationResponse +import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.* import timber.log.Timber import kotlin.coroutines.resume -import kotlin.jvm.Throws import kotlin.time.measureTimedValue @Suppress("LongParameterList") class VisaCardActivationTask @AssistedInject constructor( - @Assisted private val accessCode: String, - @Assisted private val challengeToSign: VisaAuthChallenge.Card?, + @Assisted private val mode: VisaCardActivationTaskMode, @Assisted private val activationInput: VisaActivationInput, @Assisted private val coroutineScope: CoroutineScope, private val otpStorage: VisaOTPStorage, @@ -65,7 +70,12 @@ class VisaCardActivationTask @AssistedInject constructor( return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message)) } - val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId) + val visaActivationRepository = visaActivationRepositoryFactory.create( + VisaCardId( + cardId = card.cardId, + cardPublicKey = card.cardPublicKey.toHexString(), + ), + ) val context = SessionContext( visaActivationRepository = visaActivationRepository, @@ -74,15 +84,13 @@ class VisaCardActivationTask @AssistedInject constructor( ) val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { - if (challengeToSign != null) { - context.signAuthorizationChallenge(challengeToSign) - } else { - val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() } - .getOrElse { - return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "")) - } - - context.signOrder(activationOrder) + when (mode) { + is VisaCardActivationTaskMode.Full -> { + context.signAuthorizationChallenge(mode.authorizationChallenge) + } + is VisaCardActivationTaskMode.SignOnly -> { + context.signData(mode.dataToSignByCardWallet) + } } } Timber.i("VisaCardActivationTask all time: ${timedResult.duration}") @@ -123,45 +131,50 @@ class VisaCardActivationTask @AssistedInject constructor( signedChallenge: VisaAuthSignedChallenge, ): CompletionResult { return coroutineScope { - val activationOrderDeferred = async { getActivationOrderToSign(signedChallenge) } - val otpTaskDeferred = async { createWallet(session) } + val dataToSignDeferred = async { getDataToSign(signedChallenge) } + val otpTaskDeferred = async { createWallet() } - val order = runCatching { activationOrderDeferred.await() } + val dataToSign = dataToSignDeferred.await() .getOrElse { otpTaskDeferred.cancel() - return@coroutineScope CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "")) + return@coroutineScope CompletionResult.Failure(it) } otpTaskDeferred.await() - signOrder(order) + signData(dataToSign) } } - @Throws(TangemSdkError::class) - private suspend fun SessionContext.getActivationOrderToSign( + private suspend fun SessionContext.getDataToSign( signedChallenge: VisaAuthSignedChallenge, - ): ActivationOrder { - val tokens = runCatching { - visaAuthRepository.getAccessTokens(signedChallenge) - }.getOrElse { - throw TangemSdkError.Underlying( - "Underlying network error: ${it.message ?: ""}", - ) - } + ): Either = either { + catch( + block = { + val tokens = visaAuthRepository.getAccessTokens(signedChallenge) - visaAuthTokenStorage.store(cardId, tokens) + visaAuthTokenStorage.store(cardId, tokens) - return visaActivationRepository.getActivationOrderToSign() + val remoteState = visaActivationRepository.getActivationRemoteState() + if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) { + raise(TangemSdkError.Underlying(VisaActivationError.WrongRemoteState.message)) + } + + visaActivationRepository.getCardWalletAcceptanceData(remoteState.request) + }, + catch = { + raise(TangemSdkError.Underlying("Underlying network error: ${it.message ?: ""}")) + }, + ) } - private suspend fun SessionContext.createWallet(session: CardSession): CompletionResult { + private suspend fun SessionContext.createWallet(): CompletionResult { coroutineScope { ensureActive() } val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) return if (card.wallets.any { it.curve == VisaUtilities.mandatoryCurve }) { - createOTP(session) + createOTP() } else { val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve) @@ -178,7 +191,7 @@ class VisaCardActivationTask @AssistedInject constructor( when (val result = timedResult.value) { is CompletionResult.Success -> { Timber.i("CreateWalletTask success") - createOTP(session) + createOTP() } is CompletionResult.Failure -> { Timber.e("CreateWalletTask failure ${result.error}") @@ -188,46 +201,58 @@ class VisaCardActivationTask @AssistedInject constructor( } } - private suspend fun SessionContext.createOTP(session: CardSession): CompletionResult { + private suspend fun SessionContext.createOTP(): CompletionResult { coroutineScope { ensureActive() } - return if (otpStorage.hasSavedOTP(cardId)) { - CompletionResult.Success(Unit) - } else { - val otpCommand = GenerateOTPCommand() - val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { - suspendCancellableCoroutine { continuation -> - otpCommand.run(session) { otpResult -> - continuation.resume(otpResult) - } + if (otpStorage.hasSavedOTP(cardId)) { + return CompletionResult.Success(Unit) + } + + val otpCommand = GenerateOTPCommand() + val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { + suspendCancellableCoroutine { continuation -> + otpCommand.run(session) { otpResult -> + continuation.resume(otpResult) } } + } - Timber.i("GenerateOTPCommand time: ${timedResult.duration}") + Timber.i("GenerateOTPCommand time: ${timedResult.duration}") - when (val result = timedResult.value) { - is CompletionResult.Success -> { - Timber.i("GenerateOTPCommand success") - otpStorage.saveOTP(cardId, result.data.rootOTP) - CompletionResult.Success(Unit) - } - is CompletionResult.Failure -> { - Timber.e("GenerateOTPCommand failure ${result.error}") - CompletionResult.Failure(result.error) - } + return when (val result = timedResult.value) { + is CompletionResult.Success -> { + Timber.i("GenerateOTPCommand success") + otpStorage.saveOTP( + cardId = cardId, + data = VisaOtpData(result.data.rootOTP, result.data.rootOTPCounter), + ) + CompletionResult.Success(Unit) + } + is CompletionResult.Failure -> { + Timber.e("GenerateOTPCommand failure ${result.error}") + CompletionResult.Failure(result.error) } } } - private suspend fun SessionContext.signOrder(order: ActivationOrder): CompletionResult { + private suspend fun SessionContext.signData( + dataToSign: VisaDataToSignByCardWallet, + ): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val derivedPublicKey = wallet.derivedKeys[VisaUtilities.visaDefaultDerivationPath] + ?: return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message)) + + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnVisaCurve(derivedPublicKey.publicKey) + .getOrElse { return CompletionResult.Failure(TangemSdkError.Underlying(it.message)) } + .value + val task = SignHashCommand( - hash = order.hash.hexToBytes(), + hash = dataToSign.hashToSign.hexToBytes(), walletPublicKey = wallet.publicKey, derivationPath = VisaUtilities.visaDefaultDerivationPath, ) @@ -245,8 +270,9 @@ class VisaCardActivationTask @AssistedInject constructor( return when (val result = timedResult.value) { is CompletionResult.Success -> { Timber.i("SignHashCommand success") - handleSignedOrder( - activationOrder = order, + handleSignedData( + dataToSign = dataToSign, + walletAddress = walletAddress, response = result.data, ) } @@ -257,26 +283,29 @@ class VisaCardActivationTask @AssistedInject constructor( } } - private suspend fun SessionContext.handleSignedOrder( - activationOrder: ActivationOrder, + private suspend fun SessionContext.handleSignedData( + dataToSign: VisaDataToSignByCardWallet, + walletAddress: String, response: SignHashResponse, ): CompletionResult { - val signedOrder = SignedActivationOrder( - activationOrder = activationOrder, + val otp = otpStorage.getOTP(cardId) ?: run { + createOTP() + otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(TangemSdkError.Underlying("OTP not found")) + } + + val signedActivationData = dataToSign.sign( + cardWalletAddress = walletAddress, + rootOTP = otp.rootOTP.toHexString(), + otpCounter = otp.counter, signature = response.signature.toHexString(), ) - val otp = otpStorage.getOTP(cardId) ?: return CompletionResult.Failure( - TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message), - ) - return setupAccessCode().map { - val card = - session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val card = session.environment.card + ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) VisaCardActivationResponse( - signedActivationOrder = signedOrder, - rootOTP = VisaRootOTP(otp.toHexString()), + signedActivationData = signedActivationData, newCardDTO = CardDTO(card), ) } @@ -285,13 +314,13 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.setupAccessCode(): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - if (card.isAccessCodeSet) { + if (card.isAccessCodeSet || mode !is VisaCardActivationTaskMode.Full) { return CompletionResult.Success(Unit) } Timber.i("Setting access code") - val task = SetUserCodeCommand.changeAccessCode(accessCode) + val task = SetUserCodeCommand.changeAccessCode(mode.accessCode) val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { suspendCancellableCoroutine { continuation -> @@ -318,8 +347,7 @@ class VisaCardActivationTask @AssistedInject constructor( @AssistedFactory interface Factory { fun create( - accessCode: String, - challengeToSign: VisaAuthChallenge.Card?, + mode: VisaCardActivationTaskMode, activationInput: VisaActivationInput, coroutineScope: CoroutineScope, ): VisaCardActivationTask diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 541f367b08..89fb5b4965 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -9,6 +9,7 @@ import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.common.util.derivationStyleProvider @@ -18,16 +19,17 @@ import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDe import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.model.VisaActivationError import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.sign import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand -import com.tangem.operations.sign.SignHashResponse class VisaCustomerWalletApproveTask( private val visaDataForApprove: VisaDataForApprove, -) : CardSessionRunnable { +) : CardSessionRunnable { - override fun run(session: CardSession, callback: CompletionCallback) { + override fun run(session: CardSession, callback: CompletionCallback) { val card = session.environment.card ?: run { callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) return @@ -54,7 +56,11 @@ class VisaCustomerWalletApproveTask( } } - private fun proceedApprove(card: Card, session: CardSession, callback: CompletionCallback) { + private fun proceedApprove( + card: Card, + session: CardSession, + callback: CompletionCallback, + ) { val cardDTO = CardDTO(card) val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run { @@ -108,7 +114,7 @@ class VisaCustomerWalletApproveTask( extendedPublicKey: ExtendedPublicKey, derivationPath: DerivationPath, session: CardSession, - callback: CompletionCallback, + callback: CompletionCallback, ) { val validationResult = VisaWalletPublicKeyUtility.validateExtendedPublicKey( targetAddress = visaDataForApprove.targetAddress, @@ -131,7 +137,7 @@ class VisaCustomerWalletApproveTask( private fun proceedApproveWithLegacyCard( card: Card, session: CardSession, - callback: CompletionCallback, + callback: CompletionCallback, ) { val publicKey = findKeyWithoutDerivation( targetAddress = visaDataForApprove.targetAddress, @@ -153,10 +159,10 @@ class VisaCustomerWalletApproveTask( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, session: CardSession, - callback: CompletionCallback, + callback: CompletionCallback, ) { val signTask = SignHashCommand( - hash = visaDataForApprove.approveHash.hexToBytes(), + hash = visaDataForApprove.dataToSign.hashToSign.hexToBytes(), walletPublicKey = targetWalletPublicKey, derivationPath = derivationPath, ) @@ -165,9 +171,12 @@ class VisaCustomerWalletApproveTask( when (result) { is CompletionResult.Success -> { scanCard( - signHashResponse = result.data, session = session, callback = callback, + signedData = visaDataForApprove.dataToSign.sign( + signature = result.data.signature.toHexString(), + customerWalletAddress = visaDataForApprove.targetAddress, + ), ) } is CompletionResult.Failure -> { @@ -178,15 +187,15 @@ class VisaCustomerWalletApproveTask( } private fun scanCard( - signHashResponse: SignHashResponse, + signedData: VisaSignedDataByCustomerWallet, session: CardSession, - callback: CompletionCallback, + callback: CompletionCallback, ) { val scanTask = ScanTask() scanTask.run(session) { result -> when (result) { is CompletionResult.Success -> { - callback(CompletionResult.Success(signHashResponse)) + callback(CompletionResult.Success(signedData)) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index c0d95f3122..3951929b3c 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -8,6 +8,7 @@ import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.models.scan.serialization.* +import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage @@ -61,8 +62,9 @@ internal object UserWalletsListManagerModule { .add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(VisaActivationRemoteState.jsonAdapter) .add(VisaCardActivationStatus.jsonAdapter) - .add(KotlinJsonAdapterFactory()) + .addLast(KotlinJsonAdapterFactory()) .build() val secureStorage = AndroidSecureStorage( 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 42e9b20d4a..1ef93cd25d 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 @@ -1,5 +1,6 @@ package com.tangem.tap.domain.visa +import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.card.CardWallet import com.tangem.common.core.CardSession @@ -10,6 +11,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository @@ -43,7 +45,12 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) } - val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId) + val visaActivationRepository = visaActivationRepositoryFactory.create( + VisaCardId( + cardId = card.cardId, + cardPublicKey = card.cardPublicKey.toHexString(), + ), + ) val context = SessionContext( visaActivationRepository = visaActivationRepository, @@ -53,7 +60,7 @@ internal class VisaCardScanHandler @Inject constructor( val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { val activationInput = - VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet) + VisaActivationInput(card.cardId, card.cardPublicKey.toHexString(), card.isAccessCodeSet) val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput) return CompletionResult.Success(activationStatus) } @@ -119,14 +126,21 @@ internal class VisaCardScanHandler @Inject constructor( ) } + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnVisaCurve(extendedPublicKey.publicKey) + .getOrElse { + return CompletionResult.Failure( + TangemSdkError.Underlying("Cannot generate address on Visa curve"), + ) + } + Timber.i("Requesting challenge for wallet authorization") val challengeResponse = runCatching { - visaAuthRepository.getCustomerWalletAuthChallenge( - cardId = card.cardId, - walletPublicKey = extendedPublicKey.publicKey.toHexString(), - ) + visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) }.getOrElse { - return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error")) + Timber.i( + "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", + ) + return handleCardAuthorization() } val signChallengeResult = signChallengeWithWallet( @@ -162,11 +176,6 @@ internal class VisaCardScanHandler @Inject constructor( return handleCardAuthorization() } - visaAuthTokenStorage.store( - cardId = cardId, - tokens = authorizationTokensResponse, - ) - Timber.i("Authorized using Wallet public key successfully") return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse)) @@ -240,7 +249,7 @@ internal class VisaCardScanHandler @Inject constructor( val activationInput = VisaActivationInput( cardId = card.cardId, - cardPublicKey = card.cardPublicKey, + cardPublicKey = card.cardPublicKey.toHexString(), isAccessCodeSet = card.isAccessCodeSet, ) diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt index 921fce0ccc..0a793b0222 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt @@ -2,6 +2,7 @@ package com.tangem.tap.network.auth import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.visa.model.getAuthHeader import com.tangem.domain.wallets.legacy.UserWalletsListManager import javax.inject.Inject @@ -13,8 +14,6 @@ internal class DefaultVisaAuthProvider @Inject constructor( val card = userWalletsListManager.userWalletsSync.firstOrNull { it.cardId == cardId } val status = card?.scanResponse?.visaCardActivationStatus as? VisaCardActivationStatus.Activated ?: return "Error in the app!" - val accessToken = status.visaAuthTokens.accessToken - - return "Bearer $accessToken" + return status.visaAuthTokens.getAuthHeader() } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt index 1dd6c425a5..cfd97051d7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt @@ -15,7 +15,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory object MoshiConverter { val networkMoshi: Moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) + .addLast(KotlinJsonAdapterFactory()) .add(BigDecimalAdapter()) .add(TangemSdkAdapter.ByteArrayAdapter()) .build() diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt index cbc7c36835..9c1f78bb43 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt @@ -1,8 +1,11 @@ package com.tangem.datasource.api.common.config import com.tangem.utils.ProviderSuspend +import com.tangem.utils.version.AppVersionProvider -internal class TangemVisa : ApiConfig() { +internal class TangemVisa( + private val appVersionProvider: AppVersionProvider, +) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD @@ -16,5 +19,8 @@ internal class TangemVisa : ApiConfig() { headers = createHeaders(), ) - private fun createHeaders() = mapOf>() + private fun createHeaders() = mapOf( + "version" to ProviderSuspend { appVersionProvider.versionName }, + "platform" to ProviderSuspend { "Android" }, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt index 5a26524206..e0a9068c02 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt @@ -1,8 +1,11 @@ package com.tangem.datasource.api.common.config import com.tangem.utils.ProviderSuspend +import com.tangem.utils.version.AppVersionProvider -internal class TangemVisaAuth : ApiConfig() { +internal class TangemVisaAuth( + private val appVersionProvider: AppVersionProvider, +) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE @@ -16,5 +19,8 @@ internal class TangemVisaAuth : ApiConfig() { headers = createHeaders(), ) - private fun createHeaders() = mapOf>() + private fun createHeaders() = mapOf( + "version" to ProviderSuspend { appVersionProvider.versionName }, + "platform" to ProviderSuspend { "Android" }, + ) } \ No newline at end of file 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 57cb30dab0..8050897cbf 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 @@ -1,21 +1,67 @@ package com.tangem.datasource.api.visa +import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.utils.ReadTimeout +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.response.CardActivationRemoteStateResponse +import com.tangem.datasource.api.visa.models.response.CardWalletDataToSignResponse +import com.tangem.datasource.api.visa.models.response.CustomerWalletDataToSignResponse +import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query import java.util.concurrent.TimeUnit interface TangemVisaApi { - @GET("activation-status") + @GET("product_instance/activation_status") suspend fun getRemoteActivationStatus( @Header("Authorization") authHeader: String, - ): CardActivationRemoteStateResponse + @Query("customer_id") customerId: String, + @Query("product_instance_id") productInstanceId: String, + @Query("card_id") cardId: String, + @Query("card_public_key") cardPublicKey: String, + ): ApiResponse @ReadTimeout(duration = 20, TimeUnit.MINUTES) - @GET("activation-status") + @GET("product_instance/activation_status") suspend fun getRemoteActivationStatusLongPoll( @Header("Authorization") authHeader: String, - ): CardActivationRemoteStateResponse + @Query("customer_id") customerId: String, + @Query("product_instance_id") productInstanceId: String, + @Query("card_id") cardId: String, + @Query("card_public_key") cardPublicKey: String, + ): ApiResponse + + @GET("product_instance/card_wallet_acceptance") + suspend fun getCardWalletAcceptance( + @Header("Authorization") authHeader: String, + @Query("customer_id") customerId: String, + @Query("product_instance_id") productInstanceId: String, + @Query("activation_order") activationOrderId: String, + @Query("customer_wallet_address") customerWalletAddress: String, + ): ApiResponse + + @GET("product_instance/customer_wallet_acceptance") + suspend fun getCustomerWalletAcceptance( + @Header("Authorization") authHeader: String, + @Query("customer_id") customerId: String, + @Query("product_instance_id") productInstanceId: String, + @Query("activation_order") activationOrderId: String, + @Query("card_wallet_address") cardWalletAddress: String, + ): ApiResponse + + @POST("product_instance/activation_by_card_wallet") + suspend fun activateByCardWallet( + @Header("Authorization") authHeader: String, + @Body body: ActivationByCardWalletRequest, + ): ApiResponse + + @POST("product_instance/activation_by_customer_wallet") + suspend fun activateByCustomerWallet( + @Header("Authorization") authHeader: String, + @Body body: ActivationByCustomerWalletRequest, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt index 858390cf7c..1e200c63bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt @@ -3,45 +3,29 @@ package com.tangem.datasource.api.visa import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse import com.tangem.datasource.api.visa.models.response.JWTResponse import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded -import retrofit2.http.Headers import retrofit2.http.POST +import retrofit2.http.Query interface TangemVisaAuthApi { - @FormUrlEncoded - @Headers("Content-Type: application/x-www-form-urlencoded") - @POST("auth/clients/mobile-app-android/nonce-challenge") + @POST("auth/card_wallet") suspend fun generateNonceByWalletAddress( - @Field("customer_id") customerId: String? = null, - @Field("customer_wallet_address") customerWalletAddress: String, + @Query("card_wallet_address") cardWalletAddress: String, ): GenerateNonceResponse - @FormUrlEncoded - @Headers("Content-Type: application/x-www-form-urlencoded") - @POST("auth/clients/mobile-app-android/nonce-challenge") + @POST("auth/card_id") suspend fun generateNonceByCard( - @Field("card_id") cardId: String, - @Field("card_public_key") cardPublicKey: String, + @Query("card_id") cardId: String, + @Query("card_public_key") cardPublicKey: String, ): GenerateNonceResponse - @FormUrlEncoded - @Headers("Content-Type: application/x-www-form-urlencoded") - @POST("auth/protocol/openid-connect/token") + @POST("auth/get_token") suspend fun getAccessToken( - @Field("client_id") clientId: String = "mobile-app-android", - @Field("grant_type") grantType: String = "password", - @Field("session_id") sessionId: String, - @Field("signature") signature: String, - @Field("salt") salt: String?, + @Query("session_id") sessionId: String, + @Query("signature") signature: String, + @Query("salt") salt: String?, ): JWTResponse - @FormUrlEncoded - @Headers("Content-Type: application/x-www-form-urlencoded") - @POST("auth/protocol/openid-connect/token") - suspend fun refreshAccessToken( - @Field("client_id") clientId: String = "mobile-app-android", - @Field("grant_type") grantType: String = "refresh_token", - @Field("refresh_token") refreshToken: String, - ): JWTResponse + @POST("auth/refresh_token") + suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): JWTResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt new file mode 100644 index 0000000000..d19f87f592 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt @@ -0,0 +1,40 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ActivationByCardWalletRequest( + @Json(name = "customer_id") val customerId: String, + @Json(name = "product_instance_id") val productInstanceId: String, + @Json(name = "activation_order_id") val activationOrderId: String, + @Json(name = "data") val data: Data, +) { + @JsonClass(generateAdapter = true) + data class Data( + @Json(name = "card_wallet") val cardWallet: CardWallet, + @Json(name = "otp") val otp: Otp, + ) + + @JsonClass(generateAdapter = true) + data class CardWallet( + @Json(name = "address") val address: String, + @Json(name = "card_wallet_confirmation") val cardWalletConfirmation: CardWalletConfirmation?, + @Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String, + ) + + @JsonClass(generateAdapter = true) + data class CardWalletConfirmation( + @Json(name = "challenge") val challenge: String, + @Json(name = "wallet_salt") val walletSalt: String, + @Json(name = "wallet_signature") val walletSignature: String, + @Json(name = "card_salt") val cardSalt: String, + @Json(name = "card_signature") val cardSignature: String, + ) + + @JsonClass(generateAdapter = true) + data class Otp( + @Json(name = "root_otp") val rootOtp: String, + @Json(name = "counter") val counter: Int, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt new file mode 100644 index 0000000000..47bdbc4132 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ActivationByCustomerWalletRequest( + @Json(name = "customer_id") val customerId: String, + @Json(name = "product_instance_id") val productInstanceId: String, + @Json(name = "activation_order_id") val activationOrderId: String, + @Json(name = "data") val data: Data, +) { + @JsonClass(generateAdapter = true) + data class Data( + @Json(name = "customer_wallet") val customerWallet: CustomerWallet, + ) + + @JsonClass(generateAdapter = true) + data class CustomerWallet( + @Json(name = "address") val address: String, + @Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt index 1b4c053a91..e0ee6cc4f0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt @@ -5,5 +5,12 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CardActivationRemoteStateResponse( - @Json(name = "state") val state: String, -) \ No newline at end of file + @Json(name = "activation_status") val status: String, + @Json(name = "activation_order") val activationOrder: ActivationOrder?, +) { + @JsonClass(generateAdapter = true) + data class ActivationOrder( + @Json(name = "id") val id: String, + @Json(name = "customer_wallet_address") val customerWalletAddress: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardWalletDataToSignResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardWalletDataToSignResponse.kt new file mode 100644 index 0000000000..288ab32e39 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardWalletDataToSignResponse.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 CardWalletDataToSignResponse( + @Json(name = "dataForCardWallet") val dataForCardWallet: Data, +) { + @JsonClass(generateAdapter = true) + data class Data( + @Json(name = "hash") val hash: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CustomerWalletDataToSignResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CustomerWalletDataToSignResponse.kt new file mode 100644 index 0000000000..e4575a1587 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CustomerWalletDataToSignResponse.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 CustomerWalletDataToSignResponse( + @Json(name = "data_for_customer_wallet") val dataForCardWallet: Data, +) { + @JsonClass(generateAdapter = true) + data class Data( + @Json(name = "hash") val hash: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 4f6517e5ff..20d50bcd28 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -42,9 +42,10 @@ internal object ApiConfigsModule { @Provides @IntoSet - fun provideTangemAuthVisaConfig(): ApiConfig = TangemVisaAuth() + fun provideTangemAuthVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = + TangemVisaAuth(appVersionProvider) @Provides @IntoSet - fun provideTangemVisaConfig(): ApiConfig = TangemVisa() + fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 0294c9d76a..f3012c9510 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.adapter.LocalDateAdapter import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.domain.models.scan.serialization.* +import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus import dagger.Module import dagger.Provides @@ -35,8 +36,9 @@ class MoshiModule { .add(BigDecimalAdapter()) .add(LocalDateAdapter()) .add(DateTimeAdapter()) + .add(VisaActivationRemoteState.jsonAdapter) .add(VisaCardActivationStatus.jsonAdapter) - .add(KotlinJsonAdapterFactory()) + .addLast(KotlinJsonAdapterFactory()) .addStakeKitEnumFallbackAdapters() .build() } @@ -59,10 +61,11 @@ class MoshiModule { val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters() return Moshi.Builder().apply { + add(VisaActivationRemoteState.jsonAdapter) add(VisaCardActivationStatus.jsonAdapter) adapters.forEach { this.add(it) } typedAdapters.forEach { add(it.key, it.value) } - add(KotlinJsonAdapterFactory()) + addLast(KotlinJsonAdapterFactory()) }.build() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt index 8395982e69..c82e128e47 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt @@ -2,11 +2,16 @@ package com.tangem.datasource.local.visa interface VisaOTPStorage { - suspend fun saveOTP(cardId: String, otp: ByteArray) + suspend fun saveOTP(cardId: String, data: VisaOtpData) - suspend fun getOTP(cardId: String): ByteArray? + suspend fun getOTP(cardId: String): VisaOtpData? suspend fun removeOTP(cardId: String) } +class VisaOtpData( + val rootOTP: ByteArray, + val counter: Int, +) + suspend fun VisaOTPStorage.hasSavedOTP(cardId: String): Boolean = getOTP(cardId) != null \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 0b65dbcbf0..a58ab9efa6 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -36,8 +36,8 @@ private val API_CONFIGS = setOf( Express(configManager, expressAuthProvider, appVersionProvider), TangemTech(appVersionProvider, appAuthProvider), StakeKit(stakeKitAuthProvider), - TangemVisaAuth(), - TangemVisa(), + TangemVisaAuth(appVersionProvider), + TangemVisa(appVersionProvider), ) /** @@ -188,6 +188,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { expected = ApiEnvironmentConfig( environment = ApiEnvironment.STAGE, baseUrl = "https://api-s.tangem.org/", + headers = mapOf( + "version" to ProviderSuspend { VERSION_NAME }, + "platform" to ProviderSuspend { "Android" }, + ), ), ) } @@ -198,7 +202,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { expected = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://bff.tangem.com/", - headers = mapOf(), + headers = mapOf( + "version" to ProviderSuspend { VERSION_NAME }, + "platform" to ProviderSuspend { "Android" }, + ), ), ) } 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 4384fc3d0e..f96179c10c 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,49 +1,195 @@ package com.tangem.data.visa -import com.tangem.common.extensions.toHexString -import com.tangem.crypto.CryptoUtils +import com.tangem.data.visa.converter.AccessCodeDataConverter import com.tangem.data.visa.converter.VisaActivationStatusConverter -import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider +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.domain.visa.model.ActivationOrder -import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.visa.exception.RefreshTokenExpiredException +import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.withContext -@Suppress("UnusedPrivateMember") +@Suppress("LongParameterList") internal class DefaultVisaActivationRepository @AssistedInject constructor( - @Assisted private val cardId: String, + @Assisted private val visaCardId: VisaCardId, private val visaApi: TangemVisaApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val visaActivationStatusConverter: VisaActivationStatusConverter, - private val visaAuthProvider: TangemVisaAuthProvider, + private val visaAuthTokenStorage: VisaAuthTokenStorage, + private val accessCodeDataConverter: AccessCodeDataConverter, + private val visaAuthRepository: VisaAuthRepository, ) : VisaActivationRepository { override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { - // visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(visaAuthProvider.getAuthHeader(cardId))) - VisaActivationRemoteState.CardWalletSignatureRequired // mock - // TODO implement refreshing access token if it's expired + val result = request { + val authTokens = + checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } + val accessCodeData = accessCodeDataConverter.convert(authTokens) + + visaApi.getRemoteActivationStatus( + authHeader = authTokens.getAuthHeader(), + customerId = accessCodeData.customerId, + productInstanceId = accessCodeData.productInstanceId, + cardId = visaCardId.cardId, + cardPublicKey = visaCardId.cardPublicKey, + ).getOrThrow() + } + + visaActivationStatusConverter.convert(result) } override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { - // visaActivationStatusConverter.convert( - // visaApi.getRemoteActivationStatusLongPoll(visaAuthProvider.getAuthHeader(cardId)), - // ) + val result = request { + val authTokens = + checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } + val accessCodeData = accessCodeDataConverter.convert(authTokens) - VisaActivationRemoteState.WaitingPinCode + visaApi.getRemoteActivationStatusLongPoll( + authHeader = authTokens.getAuthHeader(), + customerId = accessCodeData.customerId, + productInstanceId = accessCodeData.productInstanceId, + cardId = visaCardId.cardId, + cardPublicKey = visaCardId.cardPublicKey, + ).getOrThrow() + } + + visaActivationStatusConverter.convert(result) } - override suspend fun getActivationOrderToSign(): ActivationOrder = withContext(dispatcherProvider.io) { - ActivationOrder(CryptoUtils.generateRandomBytes(length = 32).toHexString()) + override suspend fun getCardWalletAcceptanceData( + request: VisaCardWalletDataToSignRequest, + ): VisaDataToSignByCardWallet = withContext(dispatcherProvider.io) { + val result = request { + val authTokens = + checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } + val accessCodeData = accessCodeDataConverter.convert(authTokens) + + visaApi.getCardWalletAcceptance( + authHeader = authTokens.getAuthHeader(), + customerId = accessCodeData.customerId, + productInstanceId = accessCodeData.productInstanceId, + activationOrderId = request.orderId, + customerWalletAddress = request.customerWalletAddress, + ).getOrThrow() + } + + VisaDataToSignByCardWallet( + request = request, + hashToSign = result.dataForCardWallet.hash, + ) + } + + override suspend fun getCustomerWalletAcceptanceData( + request: VisaCustomerWalletDataToSignRequest, + ): VisaDataToSignByCustomerWallet = withContext(dispatcherProvider.io) { + val result = request { + val authTokens = + checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } + val accessCodeData = accessCodeDataConverter.convert(authTokens) + + visaApi.getCustomerWalletAcceptance( + authHeader = authTokens.getAuthHeader(), + customerId = accessCodeData.customerId, + productInstanceId = accessCodeData.productInstanceId, + activationOrderId = request.orderId, + cardWalletAddress = request.cardWalletAddress, + ).getOrThrow() + } + + VisaDataToSignByCustomerWallet( + request = request, + hashToSign = result.dataForCardWallet.hash, + ) + } + + override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) { + withContext(dispatcherProvider.io) { + request { + val authTokens = + checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } + val accessCodeData = accessCodeDataConverter.convert(authTokens) + + visaApi.activateByCardWallet( + authHeader = authTokens.getAuthHeader(), + body = ActivationByCardWalletRequest( + customerId = accessCodeData.customerId, + productInstanceId = accessCodeData.productInstanceId, + activationOrderId = signedData.dataToSign.request.orderId, + data = ActivationByCardWalletRequest.Data( + cardWallet = ActivationByCardWalletRequest.CardWallet( + address = signedData.cardWalletAddress, + cardWalletConfirmation = null, // for second iteration + deployAcceptanceSignature = signedData.signature, + ), + otp = ActivationByCardWalletRequest.Otp( + rootOtp = signedData.rootOTP, + counter = signedData.otpCounter, + ), + ), + ), + ) + } + } + } + + override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) { + withContext(dispatcherProvider.io) { + request { + val authTokens = + checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } + val accessCodeData = accessCodeDataConverter.convert(authTokens) + + visaApi.activateByCustomerWallet( + authHeader = authTokens.getAuthHeader(), + body = ActivationByCustomerWalletRequest( + customerId = accessCodeData.customerId, + productInstanceId = accessCodeData.productInstanceId, + activationOrderId = signedData.dataToSign.request.orderId, + data = ActivationByCustomerWalletRequest.Data( + customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( + address = signedData.customerWalletAddress, + deployAcceptanceSignature = signedData.signature, + ), + ), + ), + ) + } + } + } + + private suspend fun request(requestBlock: suspend () -> T): T { + return runCatching { + requestBlock() + }.getOrElse { responseError -> + if (responseError !is ApiResponseError.HttpException || + responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED + ) { + throw responseError + } + + val authTokens = visaAuthTokenStorage.get(visaCardId.cardId) ?: error("Auth tokens are not stored") + val newTokens = runCatching { + visaAuthRepository.refreshAccessTokens(authTokens.refreshToken) + }.getOrElse { throw RefreshTokenExpiredException() } + + visaAuthTokenStorage.store(visaCardId.cardId, newTokens) + + requestBlock() + } } @AssistedFactory interface Factory : VisaActivationRepository.Factory { - override fun create(cardId: String): DefaultVisaActivationRepository + override fun create(cardId: VisaCardId): DefaultVisaActivationRepository } } \ No newline at end of file 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 5174396612..9e5d4a5122 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 @@ -36,24 +36,22 @@ internal class DefaultVisaAuthRepository @Inject constructor( ) } - override suspend fun getCustomerWalletAuthChallenge( - cardId: String, - walletPublicKey: String, - ): VisaAuthChallenge.Wallet = withContext(dispatchers.io) { - // val response = visaAuthApi.generateNonceByWalletAddress( - // customerId = cardId, - // customerWalletAddress = walletPublicKey, - // ) - // - // VisaAuthChallenge.Wallet( - // challenge = response.nonce, - // session = VisaAuthSession(response.sessionId), - // ) - VisaAuthChallenge.Wallet( - challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(), - session = VisaAuthSession("session"), - ) - } + override suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet = + withContext(dispatchers.io) { + // val response = visaAuthApi.generateNonceByWalletAddress( + // customerId = cardId, + // customerWalletAddress = walletPublicKey, + // ) + // + // VisaAuthChallenge.Wallet( + // challenge = response.nonce, + // session = VisaAuthSession(response.sessionId), + // ) + VisaAuthChallenge.Wallet( + challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(), + session = VisaAuthSession("session"), + ) + } override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens = withContext(dispatchers.io) { 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 new file mode 100644 index 0000000000..1da6cd3be2 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt @@ -0,0 +1,50 @@ +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.PaymentAccountDeploying + } + + override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState { + return VisaActivationRemoteState.WaitingPinCode + } + + 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) {} + + @AssistedFactory + interface Factory : VisaActivationRepository.Factory { + override fun create(cardId: VisaCardId): MockVisaActivationRepository + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/AccessCodeDataConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/AccessCodeDataConverter.kt new file mode 100644 index 0000000000..f4676e3ed7 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/AccessCodeDataConverter.kt @@ -0,0 +1,28 @@ +package com.tangem.data.visa.converter + +import com.squareup.moshi.Moshi +import com.tangem.data.visa.model.AccessCodeData +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.utils.converter.Converter +import okio.ByteString.Companion.decodeBase64 +import javax.inject.Inject +import javax.inject.Singleton + +private const val JWT_PAYLOAD_INDEX = 1 + +@Singleton +internal class AccessCodeDataConverter @Inject constructor( + @NetworkMoshi private val moshi: Moshi, +) : Converter { + + private val adapter = moshi.adapter(AccessCodeData::class.java) + + override fun convert(value: VisaAuthTokens): AccessCodeData { + val payloadBase64 = value.accessToken.split(".").getOrNull(JWT_PAYLOAD_INDEX) + val decodedString = payloadBase64?.decodeBase64()?.utf8() + val data = decodedString?.let { adapter.fromJson(it) } + requireNotNull(data) { "Invalid access token" } + return data + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index f1582ade9e..5cfa0296bb 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,8 +1,8 @@ package com.tangem.data.visa.di -import com.tangem.data.visa.DefaultVisaActivationRepository import com.tangem.data.visa.DefaultVisaAuthRepository import com.tangem.data.visa.DummyVisaRepository +import com.tangem.data.visa.MockVisaActivationRepository import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.visa.repository.VisaRepository @@ -36,9 +36,16 @@ internal interface VisaDataBindsModule { @Singleton fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository + // @Binds + // @Singleton + // fun bindVisaActivationRepositoryFactory( + // repository: DefaultVisaActivationRepository.Factory, + // ): VisaActivationRepository.Factory + + // Mocked @Binds @Singleton fun bindVisaActivationRepositoryFactory( - repository: DefaultVisaActivationRepository.Factory, + repository: MockVisaActivationRepository.Factory, ): VisaActivationRepository.Factory } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/model/AccessCodeData.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/model/AccessCodeData.kt new file mode 100644 index 0000000000..47c868a466 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/model/AccessCodeData.kt @@ -0,0 +1,10 @@ +package com.tangem.data.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AccessCodeData( + @Json(name = "pid") val productInstanceId: String, + @Json(name = "sub") val customerId: String, +) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt index 60a095ca16..70af83d8d9 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO @@ -28,6 +29,21 @@ object VisaWalletPublicKeyUtility { wallet.publicKey } + fun generateAddressOnVisaCurve(walletPublicKey: ByteArray): Either = either { + val addresses = catch( + block = { + VisaUtilities.visaBlockchain.makeAddresses( + walletPublicKey = walletPublicKey, + pairPublicKey = null, + curve = VisaUtilities.mandatoryCurve, + ) + }, + catch = { raise(Error.FailedToCreateAddress) }, + ) + + addresses.firstOrNull { it.type == AddressType.Default } ?: raise(Error.FailedToCreateAddress) + } + private fun findWalletOnVisaCurve(card: CardDTO): Either = either { card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: raise(Error.MissingWalletOnTargetCurve) } @@ -40,21 +56,6 @@ object VisaWalletPublicKeyUtility { } } - private fun generateAddressOnVisaCurve(walletPublicKey: ByteArray): Either = either { - val addresses = catch( - block = { - VisaUtilities.visaBlockchain.makeAddresses( - walletPublicKey = walletPublicKey, - pairPublicKey = null, - curve = VisaUtilities.mandatoryCurve, - ) - }, - catch = { raise(Error.FailedToCreateAddress) }, - ) - - addresses.firstOrNull() ?: raise(Error.FailedToCreateAddress) - } - enum class Error(val message: String) { AddressNotMatched("ValidationError: Address not matched"), FailedToCreateAddress("ValidationError: Failed to create address"), diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt index 7f22e52275..62431daf4f 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.TangemSdkAdapter import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind @@ -23,8 +24,9 @@ internal object ScanResponseAsStringSerializer : KSerializer { .add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(VisaActivationRemoteState.jsonAdapter) .add(VisaCardActivationStatus.jsonAdapter) - .add(KotlinJsonAdapterFactory()) + .addLast(KotlinJsonAdapterFactory()) .build() override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ScanResponse", PrimitiveKind.STRING) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt deleted file mode 100644 index a5924d0da4..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.domain.visa.model - -data class ActivationOrder( - val hash: String, -) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt deleted file mode 100644 index 9c16d85bfc..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.visa.model - -data class SignedActivationOrder( - val activationOrder: ActivationOrder, - val signature: String, -) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt index f115a703f5..ad826a2747 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt @@ -4,6 +4,7 @@ enum class VisaActivationError(val message: String) { BlockedForActivation("Card is blocked for activation"), InvalidActivationState("Invalid activation state"), WrongCard("Wrong card tapped"), + WrongRemoteState("Wrong remote state"), MissingWallet("Missing wallet"), MissingRootOTP("Missing root OTP"), } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt index 5749e737d8..491cdcb437 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt @@ -8,26 +8,6 @@ import kotlinx.serialization.Serializable @JsonClass(generateAdapter = true) data class VisaActivationInput( @Json(name = "cardId") val cardId: String, - @Json(name = "cardPublicKey") val cardPublicKey: ByteArray, + @Json(name = "cardPublicKey") val cardPublicKey: String, @Json(name = "isAccessCodeSet") val isAccessCodeSet: Boolean, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as VisaActivationInput - - if (cardId != other.cardId) return false - if (!cardPublicKey.contentEquals(other.cardPublicKey)) return false - if (isAccessCodeSet != other.isAccessCodeSet) return false - - return true - } - - override fun hashCode(): Int { - var result = cardId.hashCode() - result = 31 * result + cardPublicKey.contentHashCode() - result = 31 * result + isAccessCodeSet.hashCode() - return result - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt index 3b64c35fde..5b2c197c56 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt @@ -1,9 +1,51 @@ package com.tangem.domain.visa.model +import com.squareup.moshi.* import kotlinx.serialization.Serializable @Serializable -enum class VisaActivationRemoteState { +sealed class VisaActivationRemoteState { + + @Serializable + data class CardWalletSignatureRequired( + val request: VisaCardWalletDataToSignRequest, + ) : VisaActivationRemoteState() + + @Serializable + data class CustomerWalletSignatureRequired( + val request: VisaCardWalletDataToSignRequest, + ) : VisaActivationRemoteState() + + @Serializable + data object PaymentAccountDeploying : VisaActivationRemoteState() + + @Serializable + data object WaitingPinCode : VisaActivationRemoteState() + + @Serializable + data object WaitingForActivationFinishing : VisaActivationRemoteState() + + @Serializable + data object Activated : VisaActivationRemoteState() + + @Serializable + data object BlockedForActivation : VisaActivationRemoteState() + + companion object { + val jsonAdapter: VisaActivationRemoteState_JsonAdapter = VisaActivationRemoteState_JsonAdapter() + } +} + +@Suppress("ClassNaming") +class VisaActivationRemoteState_Json( + @Json(name = "type") val type: VisaActivationRemoteState_Type, + @Json(name = "request") val request: VisaCardWalletDataToSignRequest? = null, +) + +@Suppress("ClassNaming") +// do not rename +@JsonClass(generateAdapter = false) +enum class VisaActivationRemoteState_Type { CardWalletSignatureRequired, CustomerWalletSignatureRequired, PaymentAccountDeploying, @@ -11,4 +53,51 @@ enum class VisaActivationRemoteState { WaitingForActivationFinishing, Activated, BlockedForActivation, +} + +@Suppress("ClassNaming") +class VisaActivationRemoteState_JsonAdapter { + + @FromJson + fun fromJson(value: VisaActivationRemoteState_Json): VisaActivationRemoteState { + return when (value.type) { + VisaActivationRemoteState_Type.CardWalletSignatureRequired -> + VisaActivationRemoteState.CardWalletSignatureRequired(value.request!!) + VisaActivationRemoteState_Type.CustomerWalletSignatureRequired -> + VisaActivationRemoteState.CustomerWalletSignatureRequired(value.request!!) + VisaActivationRemoteState_Type.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying + VisaActivationRemoteState_Type.WaitingPinCode -> VisaActivationRemoteState.WaitingPinCode + VisaActivationRemoteState_Type.WaitingForActivationFinishing -> + VisaActivationRemoteState.WaitingForActivationFinishing + VisaActivationRemoteState_Type.Activated -> VisaActivationRemoteState.Activated + VisaActivationRemoteState_Type.BlockedForActivation -> VisaActivationRemoteState.BlockedForActivation + } + } + + @ToJson + fun toJson(value: VisaActivationRemoteState): VisaActivationRemoteState_Json { + return when (value) { + is VisaActivationRemoteState.CardWalletSignatureRequired -> + VisaActivationRemoteState_Json( + type = VisaActivationRemoteState_Type.CardWalletSignatureRequired, + request = value.request, + ) + is VisaActivationRemoteState.CustomerWalletSignatureRequired -> + VisaActivationRemoteState_Json( + type = VisaActivationRemoteState_Type.CustomerWalletSignatureRequired, + request = value.request, + ) + is VisaActivationRemoteState.PaymentAccountDeploying -> + VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.PaymentAccountDeploying) + is VisaActivationRemoteState.WaitingPinCode -> + VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingPinCode) + is VisaActivationRemoteState.WaitingForActivationFinishing -> + VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingForActivationFinishing) + is VisaActivationRemoteState.Activated -> VisaActivationRemoteState_Json( + VisaActivationRemoteState_Type.Activated, + ) + is VisaActivationRemoteState.BlockedForActivation -> + VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.BlockedForActivation) + } + } } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt index 7131b2533f..b39a31f25b 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -14,4 +14,8 @@ data class VisaAuthTokens( @Serializable @JvmInline value class RefreshToken(val value: String) +} + +fun VisaAuthTokens.getAuthHeader(): String { + return "Bearer $accessToken" } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt index dc9e44c563..833bbd8ccd 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt @@ -1,7 +1,9 @@ package com.tangem.domain.visa.model +import com.squareup.moshi.FromJson import com.squareup.moshi.Json -import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import com.squareup.moshi.JsonClass +import com.squareup.moshi.ToJson import kotlinx.serialization.Serializable @Serializable @@ -9,19 +11,19 @@ sealed class VisaCardActivationStatus { @Serializable data class Activated( - @Json(name = "visaAuthTokens") val visaAuthTokens: VisaAuthTokens, + val visaAuthTokens: VisaAuthTokens, ) : VisaCardActivationStatus() @Serializable data class ActivationStarted( - @Json(name = "activationInput") val activationInput: VisaActivationInput, - @Json(name = "authTokens") val authTokens: VisaAuthTokens, - @Json(name = "remoteState") val remoteState: VisaActivationRemoteState, + val activationInput: VisaActivationInput, + val authTokens: VisaAuthTokens, + val remoteState: VisaActivationRemoteState, ) : VisaCardActivationStatus() @Serializable data class NotStartedActivation( - @Json(name = "activationInput") val activationInput: VisaActivationInput, + val activationInput: VisaActivationInput, ) : VisaCardActivationStatus() @Serializable @@ -31,11 +33,70 @@ sealed class VisaCardActivationStatus { data object RefreshTokenExpired : VisaCardActivationStatus() companion object { - val jsonAdapter: PolymorphicJsonAdapterFactory - get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type") - .withSubtype(Activated::class.java, "Activated") - .withSubtype(ActivationStarted::class.java, "ActivationStarted") - .withSubtype(NotStartedActivation::class.java, "NotStartedActivation") - .withDefaultValue(Blocked) + val jsonAdapter: VisaCardActivationStatus_JsonAdapter = VisaCardActivationStatus_JsonAdapter() + } +} + +@Suppress("ClassNaming") +class VisaCardActivationStatus_Json( + @Json(name = "type") val type: VisaCardActivationStatus_Type, + @Json(name = "activationInput") val activationInput: VisaActivationInput? = null, + @Json(name = "authTokens") val authTokens: VisaAuthTokens? = null, + @Json(name = "remoteState") val remoteState: VisaActivationRemoteState? = null, +) + +// do not rename instances +@Suppress("ClassNaming") +@JsonClass(generateAdapter = false) +enum class VisaCardActivationStatus_Type { + Activated, + ActivationStarted, + NotStartedActivation, + Blocked, + RefreshTokenExpired, +} + +@Suppress("ClassNaming") +class VisaCardActivationStatus_JsonAdapter { + + @FromJson + fun fromJson(value: VisaCardActivationStatus_Json): VisaCardActivationStatus { + return when (value.type) { + VisaCardActivationStatus_Type.Activated -> VisaCardActivationStatus.Activated(value.authTokens!!) + VisaCardActivationStatus_Type.ActivationStarted -> VisaCardActivationStatus.ActivationStarted( + value.activationInput!!, + value.authTokens!!, + value.remoteState!!, + ) + VisaCardActivationStatus_Type.NotStartedActivation -> VisaCardActivationStatus.NotStartedActivation( + value.activationInput!!, + ) + VisaCardActivationStatus_Type.Blocked -> VisaCardActivationStatus.Blocked + VisaCardActivationStatus_Type.RefreshTokenExpired -> VisaCardActivationStatus.RefreshTokenExpired + } + } + + @ToJson + fun toJson(value: VisaCardActivationStatus): VisaCardActivationStatus_Json { + return when (value) { + is VisaCardActivationStatus.Activated -> VisaCardActivationStatus_Json( + VisaCardActivationStatus_Type.Activated, + authTokens = value.visaAuthTokens, + ) + is VisaCardActivationStatus.ActivationStarted -> VisaCardActivationStatus_Json( + VisaCardActivationStatus_Type.ActivationStarted, + value.activationInput, + value.authTokens, + value.remoteState, + ) + is VisaCardActivationStatus.NotStartedActivation -> VisaCardActivationStatus_Json( + VisaCardActivationStatus_Type.NotStartedActivation, + activationInput = value.activationInput, + ) + is VisaCardActivationStatus.Blocked -> VisaCardActivationStatus_Json(VisaCardActivationStatus_Type.Blocked) + is VisaCardActivationStatus.RefreshTokenExpired -> VisaCardActivationStatus_Json( + VisaCardActivationStatus_Type.RefreshTokenExpired, + ) + } } } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardId.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardId.kt new file mode 100644 index 0000000000..846ec43e7f --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardId.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class VisaCardId( + val cardId: String, + val cardPublicKey: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardWalletDataToSignRequest.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardWalletDataToSignRequest.kt new file mode 100644 index 0000000000..a704090318 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardWalletDataToSignRequest.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + +@Serializable +@JsonClass(generateAdapter = true) +data class VisaCardWalletDataToSignRequest( + @Json(name = "orderId") val orderId: String, + @Json(name = "customer_id") val customerId: String, + @Json(name = "customer_wallet_address") val customerWalletAddress: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCustomerWalletDataToSignRequest.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCustomerWalletDataToSignRequest.kt new file mode 100644 index 0000000000..0103e34df4 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCustomerWalletDataToSignRequest.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + +@Serializable +@JsonClass(generateAdapter = true) +data class VisaCustomerWalletDataToSignRequest( + @Json(name = "orderId") val orderId: String, + @Json(name = "card_wallet_address") val cardWalletAddress: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt index 93ec3d29d8..1751abe394 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt @@ -1,11 +1,10 @@ package com.tangem.domain.visa.model -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class VisaDataForApprove( - @SerialName("customerWalletCardId") val customerWalletCardId: String?, - @SerialName("targetAddress") val targetAddress: String, - @SerialName("approveHash") val approveHash: String, + val customerWalletCardId: String?, + val targetAddress: String, + val dataToSign: VisaDataToSignByCustomerWallet, ) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCardWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCardWallet.kt new file mode 100644 index 0000000000..420aa0ea53 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCardWallet.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.visa.model + +data class VisaDataToSignByCardWallet( + val request: VisaCardWalletDataToSignRequest, + val hashToSign: String, +) + +fun VisaDataToSignByCardWallet.sign(cardWalletAddress: String, rootOTP: String, otpCounter: Int, signature: String) = + VisaSignedActivationDataByCardWallet( + dataToSign = this, + cardWalletAddress = cardWalletAddress, + rootOTP = rootOTP, + otpCounter = otpCounter, + signature = signature, + ) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt new file mode 100644 index 0000000000..17de705186 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.visa.model + +import kotlinx.serialization.Serializable + +@Serializable +data class VisaDataToSignByCustomerWallet( + val request: VisaCustomerWalletDataToSignRequest, + val hashToSign: String, +) + +fun VisaDataToSignByCustomerWallet.sign(signature: String, customerWalletAddress: String) = + VisaSignedDataByCustomerWallet( + dataToSign = this, + customerWalletAddress = customerWalletAddress, + signature = signature, + ) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt deleted file mode 100644 index 310ee01021..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.tangem.domain.visa.model - -@JvmInline -value class VisaRootOTP(val value: String) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedActivationDataByCardWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedActivationDataByCardWallet.kt new file mode 100644 index 0000000000..5b37c067ec --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedActivationDataByCardWallet.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.visa.model + +class VisaSignedActivationDataByCardWallet( + val dataToSign: VisaDataToSignByCardWallet, + val cardWalletAddress: String, + val rootOTP: String, + val otpCounter: Int, + val signature: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedDataByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedDataByCustomerWallet.kt new file mode 100644 index 0000000000..c32efcce99 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedDataByCustomerWallet.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.visa.model + +data class VisaSignedDataByCustomerWallet( + val dataToSign: VisaDataToSignByCustomerWallet, + val customerWalletAddress: String, + val signature: String, +) \ No newline at end of file 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 4dd591568f..35043bcac1 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,7 +1,6 @@ package com.tangem.domain.visa.repository -import com.tangem.domain.visa.model.ActivationOrder -import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.model.* interface VisaActivationRepository { @@ -9,9 +8,17 @@ interface VisaActivationRepository { suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState - suspend fun getActivationOrderToSign(): ActivationOrder + suspend fun getCardWalletAcceptanceData(request: VisaCardWalletDataToSignRequest): VisaDataToSignByCardWallet + + suspend fun getCustomerWalletAcceptanceData( + request: VisaCustomerWalletDataToSignRequest, + ): VisaDataToSignByCustomerWallet + + suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) + + suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) interface Factory { - fun create(cardId: String): VisaActivationRepository + fun create(cardId: VisaCardId): VisaActivationRepository } } \ No newline at end of file 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 1182e20b73..63c1f2d4bf 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 @@ -8,7 +8,7 @@ interface VisaAuthRepository { suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card - suspend fun getCustomerWalletAuthChallenge(cardId: String, walletPublicKey: String): VisaAuthChallenge.Wallet + suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index da668753a6..3fe2267ca4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -154,6 +154,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( appComponentContext = factoryContext, config = OnboardingVisaApproveComponent.Config( visaDataForApprove = route.visaDataForApprove, + scanResponse = model.currentScanResponse.value, ), params = OnboardingVisaApproveComponent.Params( childParams = childParams, 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 0778d05cbe..91a0d88b81 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 @@ -10,12 +10,16 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.visa.model.VisaCardId +import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM import com.tangem.sdk.api.TangemSdkManager +import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -28,6 +32,7 @@ import javax.inject.Inject @ComponentScoped internal class OnboardingVisaAccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, + visaActivationRepositoryFactory: VisaActivationRepository.Factory, override val dispatchers: CoroutineDispatcherProvider, @Suppress("UnusedPrivateMember") private val tangemSdkManager: TangemSdkManager, @@ -36,11 +41,18 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( ) : Model() { private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require() + private val visaActivationRepository = visaActivationRepositoryFactory.create( + VisaCardId( + cardId = params.scanResponse.card.cardId, + cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(), + ), + ) private val activationStatus = params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation ?: error("Visa activation status is not set or incorrect for this step") private val _uiState = MutableStateFlow(getInitialState()) + val uiState = _uiState.asStateFlow() val onBack = MutableSharedFlow() val onDone = MutableSharedFlow() @@ -126,46 +138,65 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( val challengeToSign = runCatching { visaAuthRepository.getCardAuthChallenge( cardId = activationStatus.activationInput.cardId, - cardPublicKey = activationStatus.activationInput.cardPublicKey.toHexString(), + cardPublicKey = activationStatus.activationInput.cardPublicKey, ) }.getOrElse { loading(false) - // show alert + // TODO show alert return@launch } val result = tangemSdkManager.activateVisaCard( - accessCode = accessCode, - challengeToSign = challengeToSign, + mode = VisaCardActivationTaskMode.Full( + accessCode = accessCode, + authorizationChallenge = challengeToSign, + ), activationInput = activationStatus.activationInput, - ) - // - when (result) { - is CompletionResult.Success -> { - // TODO load approve data from backend - val targetAddress = "x9F65354e595284956599F2892fA4A4a87653D6E6" - val foundCardId = tryToFindExistingWalletCardId(targetAddress) + ) as? CompletionResult.Success ?: run { + loading(false) + // TODO show alert + return@launch + } - modelScope.launch { - onDone.emit( - OnboardingVisaAccessCodeComponent.DoneEvent( - visaDataForApprove = VisaDataForApprove( - targetAddress = targetAddress, - approveHash = "48b55c482123a10ad9022f9f4c5dd95c", - customerWalletCardId = foundCardId, - ), - walletFound = foundCardId != null, - newScanResponse = params.scanResponse.copy( - card = result.data.newCardDTO, - ), - ), - ) - } - } - is CompletionResult.Failure -> { - loading(false) - // show alert - } + runCatching { + visaActivationRepository.activateCard(result.data.signedActivationData) + }.onFailure { + loading(false) + // TODO show alert + return@launch + } + + // load data to sign by customer wallet for the next step + val dataToSign = runCatching { + visaActivationRepository.getCustomerWalletAcceptanceData( + VisaCustomerWalletDataToSignRequest( + orderId = result.data.signedActivationData.dataToSign.request.orderId, + cardWalletAddress = result.data.signedActivationData.cardWalletAddress, + ), + ) + }.getOrElse { + loading(false) + // TODO show alert + return@launch + } + + val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress + val foundCardId = tryToFindExistingWalletCardId(targetAddress) + + modelScope.launch { + onDone.emit( + OnboardingVisaAccessCodeComponent.DoneEvent( + visaDataForApprove = VisaDataForApprove( + targetAddress = targetAddress, + customerWalletCardId = foundCardId, + dataToSign = dataToSign, + ), + walletFound = foundCardId != null, + newScanResponse = params.scanResponse.copy( + card = result.data.newCardDTO, + ), + ), + ) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt index 60141475ad..758b6af4af 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.approve.model.OnboardingVisaApproveModel @@ -43,6 +44,7 @@ internal class OnboardingVisaApproveComponent( data class Config( val visaDataForApprove: VisaDataForApprove, + val scanResponse: ScanResponse, ) data class Params( 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 a79fe4aa78..ee97c99fc5 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 @@ -2,9 +2,12 @@ package com.tangem.features.onboarding.v2.visa.impl.child.approve.model import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult +import com.tangem.common.extensions.toHexString import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.visa.model.VisaCardId +import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM import com.tangem.sdk.api.TangemSdkManager @@ -20,11 +23,19 @@ import javax.inject.Inject @ComponentScoped internal class OnboardingVisaApproveModel @Inject constructor( paramsContainer: ParamsContainer, + visaActivationRepositoryFactory: VisaActivationRepository.Factory, override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, ) : Model() { private val params = paramsContainer.require() + private val visaActivationRepository = visaActivationRepositoryFactory.create( + VisaCardId( + cardId = params.scanResponse.card.cardId, + cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(), + ), + ) + private val _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() @@ -42,20 +53,21 @@ internal class OnboardingVisaApproveModel @Inject constructor( modelScope.launch { val result = tangemSdkManager.visaCustomerWalletApprove( visaDataForApprove = params.visaDataForApprove, - ) - - when (result) { - is CompletionResult.Success -> { - // TODO make backend call - - onDone.emit(Unit) - } - is CompletionResult.Failure -> { - loading(false) - // TODO show dialog - return@launch - } + ) as? CompletionResult.Success ?: run { + loading(false) + // TODO show dialog + return@launch } + + runCatching { + visaActivationRepository.approveByCustomerWallet(result.data) + }.onFailure { + loading(false) + // TODO show dialog + return@launch + } + + onDone.emit(Unit) } } 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 94748a9a1a..bb6efae8f1 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,10 +1,12 @@ package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model import androidx.compose.runtime.Stable +import com.tangem.common.extensions.toHexString import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -17,12 +19,17 @@ import javax.inject.Inject @ComponentScoped internal class OnboardingVisaInProgressModel @Inject constructor( paramsContainer: ParamsContainer, + visaActivationRepositoryFactory: VisaActivationRepository.Factory, override val dispatchers: CoroutineDispatcherProvider, - private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, ) : Model() { private val params = paramsContainer.require() - private val visaActivationRepository = visaActivationRepositoryFactory.create(params.scanResponse.card.cardId) + private val visaActivationRepository = visaActivationRepositoryFactory.create( + VisaCardId( + cardId = params.scanResponse.card.cardId, + cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(), + ), + ) val onDone = MutableSharedFlow() init { 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 51036ad803..7b1b3d44bf 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 @@ -1,12 +1,14 @@ package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model import androidx.compose.runtime.Stable +import com.tangem.common.extensions.toHexString import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.model.VisaCardId 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 @@ -22,15 +24,19 @@ import javax.inject.Inject @ComponentScoped internal class OnboardingVisaOtherWalletModel @Inject constructor( paramsContainer: ParamsContainer, + visaActivationRepositoryFactory: VisaActivationRepository.Factory, override val dispatchers: CoroutineDispatcherProvider, private val urlOpener: UrlOpener, private val shareManager: ShareManager, - private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, ) : Model() { - @Suppress("UnusedPrivateMember") private val config = paramsContainer.require() - private val visaActivationRepository = visaActivationRepositoryFactory.create(config.scanResponse.card.cardId) + private val visaActivationRepository = visaActivationRepositoryFactory.create( + VisaCardId( + cardId = config.scanResponse.card.cardId, + cardPublicKey = config.scanResponse.card.cardPublicKey.toHexString(), + ), + ) private val _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() @@ -61,10 +67,10 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( } private fun onShareClicked() { - shareManager.shareText("https://tangem.com/${config.visaDataForApprove.approveHash}") + shareManager.shareText("https://tangem.com/") } private fun onOpenInBrowserClicked() { - urlOpener.openUrl("https://tangem.com/${config.visaDataForApprove.approveHash}") + urlOpener.openUrl("https://tangem.com/") } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt index f1eab1d0d0..f746570a63 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt @@ -52,12 +52,13 @@ internal class OnboardingVisaModel @Inject constructor( fun navigateFromWelcome(route: OnboardingVisaRoute.Welcome) { if (route.isWelcomeBack) { val scanResponse = _currentScanResponse.value - val activationStatus = scanResponse.visaCardActivationStatus as? VisaCardActivationStatus - .ActivationStarted ?: error("Activation status is not correct for welcome back route") + val activationStatus = scanResponse.visaCardActivationStatus + as? VisaCardActivationStatus.ActivationStarted + ?: error("Activation status is not correct for welcome back route") when (activationStatus.remoteState) { - VisaActivationRemoteState.CardWalletSignatureRequired, - VisaActivationRemoteState.CustomerWalletSignatureRequired, + is VisaActivationRemoteState.CardWalletSignatureRequired, + is VisaActivationRemoteState.CustomerWalletSignatureRequired, -> OnboardingVisaRoute.AccessCode VisaActivationRemoteState.WaitingPinCode -> OnboardingVisaRoute.PinCode VisaActivationRemoteState.WaitingForActivationFinishing -> OnboardingVisaRoute.InProgress diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 754f3ea551..2defabccc2 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -16,14 +16,14 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter -import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.visa.VisaCardActivationResponse +import com.tangem.sdk.api.visa.VisaCardActivationTaskMode @Suppress("TooManyFunctions") interface TangemSdkManager { @@ -150,12 +150,13 @@ interface TangemSdkManager { // region Visa-specific suspend fun activateVisaCard( - accessCode: String, - challengeToSign: VisaAuthChallenge.Card?, + mode: VisaCardActivationTaskMode, activationInput: VisaActivationInput, ): CompletionResult - suspend fun visaCustomerWalletApprove(visaDataForApprove: VisaDataForApprove): CompletionResult + suspend fun visaCustomerWalletApprove( + visaDataForApprove: VisaDataForApprove, + ): CompletionResult // endregion } \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationResponse.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationResponse.kt index 50da653b5e..3aef6c73ff 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationResponse.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationResponse.kt @@ -1,11 +1,9 @@ package com.tangem.sdk.api.visa import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.visa.model.SignedActivationOrder -import com.tangem.domain.visa.model.VisaRootOTP +import com.tangem.domain.visa.model.VisaSignedActivationDataByCardWallet data class VisaCardActivationResponse( - val signedActivationOrder: SignedActivationOrder, - val rootOTP: VisaRootOTP, + val signedActivationData: VisaSignedActivationDataByCardWallet, val newCardDTO: CardDTO, ) \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationTaskMode.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationTaskMode.kt new file mode 100644 index 0000000000..7699f52cf7 --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationTaskMode.kt @@ -0,0 +1,20 @@ +package com.tangem.sdk.api.visa + +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaDataToSignByCardWallet + +sealed class VisaCardActivationTaskMode { + /** + * Full activation process with getting remote activation status. + */ + data class Full( + val accessCode: String, + val authorizationChallenge: VisaAuthChallenge.Card, + ) : VisaCardActivationTaskMode() + + /** + * Activation process with only sign data by card wallet. + * This is used when activation process was interrupted and we need to finish it. + */ + data class SignOnly(val dataToSignByCardWallet: VisaDataToSignByCardWallet) : VisaCardActivationTaskMode() +} \ No newline at end of file