diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt index 5adc5fd942..a6128ddfe6 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt @@ -48,5 +48,9 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor( ?.let(tokensAdapter::fromJson) } + override fun remove(cardId: String) { + secureStorage.delete(createKey(cardId)) + } + private fun createKey(cardId: String): String = "visa_auth_tokens_$cardId" } \ No newline at end of file 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 709be0263d..9bd9bf2e48 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,7 +1,6 @@ package com.tangem.tap.domain.tasks.visa import com.tangem.common.CompletionResult -import com.tangem.common.card.Card import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback @@ -9,8 +8,10 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.common.map +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.hasSavedOTP import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository @@ -25,6 +26,7 @@ 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 @@ -42,7 +44,7 @@ class VisaCardActivationTask @AssistedInject constructor( private class SessionContext( val visaActivationRepository: VisaActivationRepository, - val card: Card, + val cardId: String, val session: CardSession, ) @@ -63,7 +65,7 @@ class VisaCardActivationTask @AssistedInject constructor( val context = SessionContext( visaActivationRepository = visaActivationRepository, - card = card, + cardId = card.cardId, session = session, ) @@ -82,7 +84,7 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.signAuthorizationChallenge( challengeToSign: VisaAuthChallenge.Card, ): CompletionResult { - val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes()) + val attestationCommand = AttestCardKeyCommand(challenge = CryptoUtils.generateRandomBytes(length = 16)) val result = suspendCancellableCoroutine { continuation -> attestationCommand.run(session = session) { attestationResponse -> continuation.resume(attestationResponse) @@ -91,6 +93,7 @@ class VisaCardActivationTask @AssistedInject constructor( return when (result) { is CompletionResult.Success -> { + Timber.tag("ASDASD").e("AttestCardKeyCommand success") processSignedAuthorizationChallenge( signedChallenge = challengeToSign.toSignedChallenge( signedChallenge = result.data.cardSignature.toHexString(), @@ -99,6 +102,7 @@ class VisaCardActivationTask @AssistedInject constructor( ) } is CompletionResult.Failure -> { + Timber.tag("ASDASD").e("AttestCardKeyCommand failure ${result.error}") CompletionResult.Failure(result.error) } } @@ -135,12 +139,12 @@ class VisaCardActivationTask @AssistedInject constructor( ) } - visaAuthTokenStorage.store(card.cardId, tokens) + visaAuthTokenStorage.store(cardId, tokens) return visaActivationRepository.getActivationOrderToSign() } - private suspend fun createWallet(session: CardSession): CompletionResult { + private suspend fun SessionContext.createWallet(session: CardSession): CompletionResult { coroutineScope { ensureActive() } val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) @@ -157,22 +161,21 @@ class VisaCardActivationTask @AssistedInject constructor( when (result) { is CompletionResult.Success -> { + Timber.tag("ASDASD").e("CreateWalletTask success") createOTP(session) } is CompletionResult.Failure -> { + Timber.tag("ASDASD").e("CreateWalletTask failure ${result.error}") CompletionResult.Failure(result.error) } } } } - private suspend fun createOTP(session: CardSession): CompletionResult { + private suspend fun SessionContext.createOTP(session: CardSession): CompletionResult { coroutineScope { ensureActive() } - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - - val otp = otpStorage.getOTP(card.cardId) - return if (otp != null) { + return if (otpStorage.hasSavedOTP(cardId)) { CompletionResult.Success(Unit) } else { val otpCommand = GenerateOTPCommand() @@ -184,10 +187,12 @@ class VisaCardActivationTask @AssistedInject constructor( when (result) { is CompletionResult.Success -> { - otpStorage.saveOTP(card.cardId, result.data.rootOTP) + Timber.tag("ASDASD").e("GenerateOTPCommand success") + otpStorage.saveOTP(cardId, result.data.rootOTP) CompletionResult.Success(Unit) } is CompletionResult.Failure -> { + Timber.tag("ASDASD").e("GenerateOTPCommand failure ${result.error}") CompletionResult.Failure(result.error) } } @@ -195,9 +200,18 @@ class VisaCardActivationTask @AssistedInject constructor( } private suspend fun SessionContext.signOrder(order: ActivationOrder): CompletionResult { + val card = + session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = - card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey) + card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } + ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val task = SignHashCommand( + hash = order.hash.hexToBytes(), + walletPublicKey = wallet.publicKey, + derivationPath = VisaUtilities.visaDefaultDerivationPath, + ) + val result = suspendCancellableCoroutine { continuation -> task.run(session) { signResult -> continuation.resume(signResult) @@ -206,12 +220,14 @@ class VisaCardActivationTask @AssistedInject constructor( return when (result) { is CompletionResult.Success -> { + Timber.tag("ASDASD").e("SignHashCommand success") handleSignedOrder( activationOrder = order, response = result.data, ) } is CompletionResult.Failure -> { + Timber.tag("ASDASD").e("SignHashCommand failure ${result.error}") CompletionResult.Failure(result.error) } } @@ -226,7 +242,7 @@ class VisaCardActivationTask @AssistedInject constructor( signature = response.signature.toHexString(), ) - val otp = otpStorage.getOTP(card.cardId) ?: return CompletionResult.Failure( + val otp = otpStorage.getOTP(cardId) ?: return CompletionResult.Failure( TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message), ) @@ -239,10 +255,14 @@ class VisaCardActivationTask @AssistedInject constructor( } private suspend fun SessionContext.setupAccessCode(session: CardSession): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + if (card.isAccessCodeSet) { return CompletionResult.Success(Unit) } + Timber.tag("ASDASD").e("Setting access code: $accessCode") + val task = SetUserCodeCommand.changeAccessCode(accessCode) val result = suspendCancellableCoroutine { continuation -> task.run(session) { setAccessCodeResult -> @@ -252,9 +272,11 @@ class VisaCardActivationTask @AssistedInject constructor( return when (result) { is CompletionResult.Success -> { + Timber.tag("ASDASD").e("SetUserCodeCommand success") CompletionResult.Success(Unit) } is CompletionResult.Failure -> { + Timber.tag("ASDASD").e("SetUserCodeCommand failure ${result.error}") CompletionResult.Failure(result.error) } } 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 a86f371d93..d1e883bcdd 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 @@ -31,7 +31,7 @@ internal class VisaCardScanHandler @Inject constructor( private class SessionContext( val visaActivationRepository: VisaActivationRepository, - val card: Card, + val cardId: String, val session: CardSession, ) @@ -47,7 +47,7 @@ internal class VisaCardScanHandler @Inject constructor( val context = SessionContext( visaActivationRepository = visaActivationRepository, - card = card, + cardId = card.cardId, session = session, ) @@ -103,6 +103,8 @@ internal class VisaCardScanHandler @Inject constructor( ) } + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { Timber.e("Failed to find extended public key while handling wallet authorization") return CompletionResult.Failure( @@ -118,7 +120,6 @@ internal class VisaCardScanHandler @Inject constructor( } Timber.i("Requesting challenge for wallet authorization") - // Will be changed later after backend implementation val challengeResponse = runCatching { visaAuthRepository.getCustomerWalletAuthChallenge( cardId = card.cardId, @@ -152,8 +153,6 @@ internal class VisaCardScanHandler @Inject constructor( private suspend fun SessionContext.handleWalletAuthorizationTokens( signedChallenge: VisaAuthSignedChallenge, ): CompletionResult { - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val authorizationTokensResponse = runCatching { visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) }.getOrElse { @@ -164,7 +163,7 @@ internal class VisaCardScanHandler @Inject constructor( } visaAuthTokenStorage.store( - cardId = card.cardId, + cardId = cardId, tokens = authorizationTokensResponse, ) @@ -266,14 +265,7 @@ internal class VisaCardScanHandler @Inject constructor( } } - return when (result) { - is CompletionResult.Success -> { - CompletionResult.Success(result.data) - } - is CompletionResult.Failure -> { - CompletionResult.Failure(result.error) - } - } + return result } private suspend fun SessionContext.signChallengeWithCard( 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 3e0b010bc8..57cb30dab0 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,8 +1,10 @@ package com.tangem.datasource.api.visa +import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse import retrofit2.http.GET import retrofit2.http.Header +import java.util.concurrent.TimeUnit interface TangemVisaApi { @@ -10,4 +12,10 @@ interface TangemVisaApi { suspend fun getRemoteActivationStatus( @Header("Authorization") authHeader: String, ): CardActivationRemoteStateResponse + + @ReadTimeout(duration = 20, TimeUnit.MINUTES) + @GET("activation-status") + suspend fun getRemoteActivationStatusLongPoll( + @Header("Authorization") authHeader: String, + ): CardActivationRemoteStateResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index ae75ab68d8..353f66d62e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -187,7 +187,7 @@ internal object NetworkModule { clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), - ) + ).applyTimeoutAnnotations() }, ) } @@ -208,7 +208,7 @@ internal object NetworkModule { clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), - ) + ).applyTimeoutAnnotations() }, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt index cceb0afdde..a91c2705a1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt @@ -7,4 +7,6 @@ interface VisaAuthTokenStorage { suspend fun store(cardId: String, tokens: VisaAuthTokens) suspend fun get(cardId: String): VisaAuthTokens? + + fun remove(cardId: String) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index a4dd795246..c3b1cfb588 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -44,17 +44,13 @@ internal fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builde val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java) val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java) - chain - .apply { - connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } - } - .apply { - readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } - } - .apply { - writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } - } - .proceed(request) + chain.run { + connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this + }.run { + readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this + }.run { + writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this + }.proceed(request) }, ) } 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 b11958c4f5..b4cf7f4d8f 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,6 +1,9 @@ package com.tangem.data.visa +import com.tangem.common.extensions.toHexString +import com.tangem.crypto.CryptoUtils import com.tangem.data.visa.converter.VisaActivationStatusConverter +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.domain.visa.model.ActivationOrder import com.tangem.domain.visa.model.VisaActivationRemoteState @@ -16,16 +19,26 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( private val visaApi: TangemVisaApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val visaActivationStatusConverter: VisaActivationStatusConverter, + private val visaAuthProvider: TangemVisaAuthProvider, ) : VisaActivationRepository { override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { - visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(cardId)) + // visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(visaAuthProvider.getAuthHeader(cardId))) VisaActivationRemoteState.CardWalletSignatureRequired // mock // TODO implement refreshing access token if it's expired } - override suspend fun getActivationOrderToSign(): ActivationOrder { - return ActivationOrder("TODO implement") + override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState = + withContext(dispatcherProvider.io) { + // visaActivationStatusConverter.convert( + // visaApi.getRemoteActivationStatusLongPoll(visaAuthProvider.getAuthHeader(cardId)), + // ) + + VisaActivationRemoteState.WaitingPinCode + } + + override suspend fun getActivationOrderToSign(): ActivationOrder = withContext(dispatcherProvider.io) { + ActivationOrder(CryptoUtils.generateRandomBytes(32).toHexString()) } @AssistedFactory 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 a0fb323d81..042d5d5518 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -1,5 +1,7 @@ package com.tangem.data.visa +import com.tangem.common.extensions.toHexString +import com.tangem.crypto.CryptoUtils import com.tangem.datasource.api.visa.TangemVisaAuthApi import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSession @@ -17,14 +19,19 @@ internal class DefaultVisaAuthRepository @Inject constructor( override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card = withContext(dispatchers.io) { - val response = visaAuthApi.generateNonceByCard( - cardId = cardId, - cardPublicKey = cardPublicKey, - ) + // val response = visaAuthApi.generateNonceByCard( + // cardId = cardId, + // cardPublicKey = cardPublicKey, + // ) + // + // VisaAuthChallenge.Card( + // challenge = response.nonce, + // session = VisaAuthSession(response.sessionId), + // ) VisaAuthChallenge.Card( - challenge = response.nonce, - session = VisaAuthSession(response.sessionId), + challenge = CryptoUtils.generateRandomBytes(16).toHexString(), + session = VisaAuthSession("session"), ) } @@ -32,39 +39,47 @@ internal class DefaultVisaAuthRepository @Inject constructor( cardId: String, walletPublicKey: String, ): VisaAuthChallenge.Wallet = withContext(dispatchers.io) { - val response = visaAuthApi.generateNonceByWalletAddress( - customerId = cardId, - customerWalletAddress = walletPublicKey, - ) - + // val response = visaAuthApi.generateNonceByWalletAddress( + // customerId = cardId, + // customerWalletAddress = walletPublicKey, + // ) + // + // VisaAuthChallenge.Wallet( + // challenge = response.nonce, + // session = VisaAuthSession(response.sessionId), + // ) VisaAuthChallenge.Wallet( - challenge = response.nonce, - session = VisaAuthSession(response.sessionId), + challenge = CryptoUtils.generateRandomBytes(32).toHexString(), + session = VisaAuthSession("session"), ) } override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens = withContext(dispatchers.io) { - val response = when (signedChallenge) { - is VisaAuthSignedChallenge.ByCardPublicKey -> { - visaAuthApi.getAccessToken( - sessionId = signedChallenge.challenge.session.sessionId, - signature = signedChallenge.signature, - salt = signedChallenge.salt, - ) - } - is VisaAuthSignedChallenge.ByWallet -> { - visaAuthApi.getAccessToken( - sessionId = signedChallenge.challenge.session.sessionId, - signature = signedChallenge.signature, - salt = null, - ) - } - } - + // val response = when (signedChallenge) { + // is VisaAuthSignedChallenge.ByCardPublicKey -> { + // visaAuthApi.getAccessToken( + // sessionId = signedChallenge.challenge.session.sessionId, + // signature = signedChallenge.signature, + // salt = signedChallenge.salt, + // ) + // } + // is VisaAuthSignedChallenge.ByWallet -> { + // visaAuthApi.getAccessToken( + // sessionId = signedChallenge.challenge.session.sessionId, + // signature = signedChallenge.signature, + // salt = null, + // ) + // } + // } + // + // VisaAuthTokens( + // accessToken = response.accessToken, + // refreshToken = response.refreshToken, + // ) VisaAuthTokens( - accessToken = response.accessToken, - refreshToken = response.refreshToken, + accessToken = "accessToken", + refreshToken = "refreshToken", ) } } \ 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 810e422951..4dd591568f 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 @@ -7,6 +7,8 @@ interface VisaActivationRepository { suspend fun getActivationRemoteState(): VisaActivationRemoteState + suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState + suspend fun getActivationOrderToSign(): ActivationOrder interface Factory { 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 a174ae69e0..22ab0da110 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 @@ -151,6 +151,9 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( ) OnboardingVisaRoute.InProgress -> OnboardingVisaInProgressComponent( appComponentContext = factoryContext, + config = OnboardingVisaInProgressComponent.Config( + scanResponse = params.scanResponse, + ), params = OnboardingVisaInProgressComponent.Params( childParams = childParams, onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) }, @@ -159,15 +162,19 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( is OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent( appComponentContext = factoryContext, config = OnboardingVisaOtherWalletComponent.Config( + scanResponse = params.scanResponse, visaDataForApprove = route.visaDataForApprove, ), params = OnboardingVisaOtherWalletComponent.Params( childParams = childParams, - onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) }, + onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) }, ), ) OnboardingVisaRoute.PinCode -> OnboardingVisaPinCodeComponent( appComponentContext = factoryContext, + config = OnboardingVisaPinCodeComponent.Config( + scanResponse = params.scanResponse, + ), params = OnboardingVisaPinCodeComponent.Params( childParams = childParams, onDone = { params.onDone() }, 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 1186c22057..b621996c87 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 @@ -17,6 +17,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.math.tan @Stable @ComponentScoped @@ -39,6 +40,8 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( val onDone = MutableSharedFlow() fun onBack() { + if (uiState.value.buttonLoading) return + when (uiState.value.step) { OnboardingVisaAccessCodeUM.Step.Enter -> modelScope.launch { onBack.emit(Unit) } OnboardingVisaAccessCodeUM.Step.ReEnter -> @@ -141,7 +144,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( OnboardingVisaAccessCodeComponent.DoneEvent( visaDataForApprove = VisaDataForApprove( targetAddress = "x9F65354e595284956599F2892fA4A4a87653D6E6", - approveHash = "approve hash", + approveHash = "48b55c482123a10ad9022f9f4c5dd95c", ), walletFound = false, // TODO ), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt index f8d10a865f..6e8787f75a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier 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.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model.OnboardingVisaInProgressModel import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.ui.OnboardingVisaInProgress @@ -15,10 +16,11 @@ import kotlinx.coroutines.flow.onEach internal class OnboardingVisaInProgressComponent( appComponentContext: AppComponentContext, + config: Config, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaInProgressModel = getOrCreateModel() + private val model: OnboardingVisaInProgressModel = getOrCreateModel(config) init { model.onDone @@ -35,6 +37,10 @@ internal class OnboardingVisaInProgressComponent( ) } + data class Config( + val scanResponse: ScanResponse, + ) + data class Params( val childParams: DefaultOnboardingVisaComponent.ChildParams, val onDone: () -> 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 9639c4ddc8..94748a9a1a 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 @@ -3,6 +3,10 @@ package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model import androidx.compose.runtime.Stable 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.repository.VisaActivationRepository +import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -12,16 +16,29 @@ import javax.inject.Inject @Stable @ComponentScoped internal class OnboardingVisaInProgressModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, ) : Model() { + private val params = paramsContainer.require() + private val visaActivationRepository = visaActivationRepositoryFactory.create(params.scanResponse.card.cardId) val onDone = MutableSharedFlow() init { modelScope.launch { - // TODO check state - delay(timeMillis = 2000) - onDone.emit(Unit) + while (true) { + val result = runCatching { + visaActivationRepository.getActivationRemoteStateLongPoll() + }.getOrNull() + + if (result == VisaActivationRemoteState.WaitingPinCode) { + onDone.emit(Unit) + break + } + + delay(timeMillis = 1000) + } } } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt index 3442f47224..a9d2297b5f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.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.otherwallet.model.OnboardingVisaOtherWalletModel @@ -43,6 +44,7 @@ internal class OnboardingVisaOtherWalletComponent( } data class Config( + val scanResponse: ScanResponse, val visaDataForApprove: VisaDataForApprove, ) 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 7e5ece076a..51036ad803 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 @@ -4,12 +4,18 @@ import androidx.compose.runtime.Stable 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.repository.VisaActivationRepository import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @Stable @@ -17,15 +23,36 @@ import javax.inject.Inject internal class OnboardingVisaOtherWalletModel @Inject constructor( paramsContainer: ParamsContainer, 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 _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() val onDone = MutableSharedFlow() + init { + modelScope.launch { + while (true) { + val result = runCatching { + visaActivationRepository.getActivationRemoteStateLongPoll() + }.getOrNull() + + if (result == VisaActivationRemoteState.WaitingPinCode) { + onDone.emit(Unit) + break + } + + delay(timeMillis = 1000) + } + } + } + private fun getInitialState(): OnboardingVisaOtherWalletUM { return OnboardingVisaOtherWalletUM( onShareClick = ::onShareClicked, @@ -34,10 +61,10 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( } private fun onShareClicked() { - // TODO + shareManager.shareText("https://tangem.com/${config.visaDataForApprove.approveHash}") } private fun onOpenInBrowserClicked() { - // TODO + urlOpener.openUrl("https://tangem.com/${config.visaDataForApprove.approveHash}") } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt index 05aeba0384..edf60844f7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt @@ -10,6 +10,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect +import com.tangem.domain.models.scan.ScanResponse import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.pincode.model.OnboardingVisaPinCodeModel import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.OnboardingVisaPinCode @@ -17,10 +18,11 @@ import kotlinx.coroutines.launch internal class OnboardingVisaPinCodeComponent( appComponentContext: AppComponentContext, + config: Config, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaPinCodeModel = getOrCreateModel() + private val model: OnboardingVisaPinCodeModel = getOrCreateModel(config) init { componentScope.launch { @@ -42,6 +44,10 @@ internal class OnboardingVisaPinCodeComponent( ) } + data class Config( + val scanResponse: ScanResponse, + ) + data class Params( val childParams: DefaultOnboardingVisaComponent.ChildParams, val onDone: () -> Unit, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 19f65cfd96..38f42fad17 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -3,6 +3,15 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow @@ -10,14 +19,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @Stable @ComponentScoped internal class OnboardingVisaPinCodeModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletsListManager: UserWalletsListManager, + private val authTokenStorage: VisaAuthTokenStorage, + private val otpStorage: VisaAuthTokenStorage, ) : Model() { + private val params = paramsContainer.require() private val _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() @@ -31,21 +47,61 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } private fun onPinCodeChange(pin: String) { - if (pin.all { it.isDigit() }) _uiState.update { it.copy(pinCode = pin) } + if (pin.all { it.isDigit() }) { + _uiState.update { + it.copy( + pinCode = pin, + submitButtonEnabled = checkPinCode(pin), + ) + } + } } private fun onSubmitClick() { if (checkPinCode(_uiState.value.pinCode).not()) return - // TODO + modelScope.launch { + loading(true) - modelScope.launch { onDone.emit(Unit) } + // TODO + // make backend call + saveWallet() + + loading(false) + } } private fun checkPinCode(pin: String): Boolean { return pin.length == PIN_CODE_LENGTH } + private fun loading(state: Boolean) { + _uiState.update { it.copy(submitButtonLoading = state) } + } + + private suspend fun saveWallet() { + val userWallet = createUserWallet(params.scanResponse) + userWalletsListManager.save(userWallet) + authTokenStorage.remove(params.scanResponse.card.cardId) + otpStorage.remove(params.scanResponse.card.cardId) + onDone.emit(Unit) + } + + private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet = withContext(dispatchers.io) { + val newActivationStatus = VisaCardActivationStatus.Activated( + visaAuthTokens = authTokenStorage.get(scanResponse.card.cardId) + ?: error("Impossible state. Wrong feature implementation"), + ) + + requireNotNull( + value = UserWalletBuilder( + scanResponse.copy(visaCardActivationStatus = newActivationStatus), + generateWalletNameUseCase, + ).build(), + lazyMessage = { "User wallet not created" }, + ) + } + private companion object { const val PIN_CODE_LENGTH = 4 } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt index cf47762749..6db2601978 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -28,9 +28,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM @@ -72,13 +74,17 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod PinCodeSection(state) } - PrimaryButton( + NavigationPrimaryButton( modifier = Modifier .imePadding() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Submit", - onClick = state.onSubmitClick, + primaryButton = NavigationButton( + textReference = TextReference.Str("Submit"), + onClick = state.onSubmitClick, + showProgress = state.submitButtonLoading, + isEnabled = state.submitButtonEnabled, + ), ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt index a8d26f44a3..9b17f93c5a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt @@ -3,5 +3,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state internal data class OnboardingVisaPinCodeUM( val pinCode: String = "", val onPinCodeChange: (String) -> Unit = {}, + val submitButtonLoading: Boolean = false, + val submitButtonEnabled: Boolean = true, val onSubmitClick: () -> Unit = {}, ) \ No newline at end of file