diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 042bf40240..48d6fc0098 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -269,8 +269,8 @@ dependencies { debugImplementation(projects.features.kyc.impl) internalImplementation(projects.features.kyc.impl) mockedImplementation(projects.features.kyc.impl) - releaseImplementation(projects.features.kyc.mock) - externalImplementation(projects.features.kyc.mock) + releaseImplementation(projects.features.kyc.impl) + externalImplementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) implementation(projects.features.createWalletSelection.api) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index ce88071554..e615d85d62 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -2,8 +2,9 @@ package com.tangem.tap.data import android.content.Context import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -11,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton -import kotlin.text.encodeToByteArray private const val DEFAULT_KEY = "tangem_pay_default_key" private const val ORDER_ID_KEY = "tangem_pay_order_id_key" @@ -19,6 +19,7 @@ private const val ORDER_ID_KEY = "tangem_pay_order_id_key" @Singleton internal class DefaultTangemPayStorage @Inject constructor( @ApplicationContext applicationContext: Context, + @NetworkMoshi moshi: Moshi, private val dispatcherProvider: CoroutineDispatcherProvider, ) : TangemPayStorage { @@ -29,14 +30,21 @@ internal class DefaultTangemPayStorage @Inject constructor( name = "tangem_pay_storage", ) } - private val moshi by lazy { - Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - } private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) } + override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { + withContext(dispatcherProvider.io) { + secureStorage.store(key = createCustomerAddressKey(userWalletId), value = customerWalletAddress) + } + } + + override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? { + return withContext(dispatcherProvider.io) { + secureStorage.getAsString(createCustomerAddressKey(userWalletId)) + } + } + override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) { val json = tokensAdapter.toJson(tokens) @@ -71,10 +79,14 @@ internal class DefaultTangemPayStorage @Inject constructor( secureStorage.delete(createOrderIdKey(customerWalletAddress)) } - override suspend fun clearAll(customerWalletAddress: String) = withContext(dispatcherProvider.io) { - secureStorage.delete(createKey(customerWalletAddress)) - secureStorage.delete(createOrderIdKey(customerWalletAddress)) - } + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = + withContext(dispatcherProvider.io) { + secureStorage.delete(createCustomerAddressKey(userWalletId)) + secureStorage.delete(createKey(customerWalletAddress)) + secureStorage.delete(createOrderIdKey(customerWalletAddress)) + } + + private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue private fun createKey(address: String): String = "${DEFAULT_KEY}_$address" diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index d741fc1a41..0487d0fad7 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -7,6 +7,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.visa.VisaCardScanHandler import dagger.Module @@ -27,6 +28,7 @@ internal class TangemSdkManagerModule { cardSdkConfigRepository: CardSdkConfigRepository, visaCardScanHandler: VisaCardScanHandler, visaCardActivationTaskFactory: VisaCardActivationTask.Factory, + tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { @@ -37,6 +39,7 @@ internal class TangemSdkManagerModule { resources = context.resources, visaCardScanHandler = visaCardScanHandler, visaCardActivationTaskFactory = visaCardActivationTaskFactory, + tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory, onboardingV2FeatureToggles = onboardingV2FeatureToggles, ) } 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 09a9039974..32f0e5b105 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 @@ -18,13 +18,17 @@ import com.tangem.core.res.getStringSafe import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.sign +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse @@ -44,6 +48,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask @@ -62,6 +67,7 @@ internal class DefaultTangemSdkManager( private val resources: Resources, private val visaCardScanHandler: VisaCardScanHandler, private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, + private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ) : TangemSdkManager { @@ -511,6 +517,18 @@ internal class DefaultTangemSdkManager( ) } + override suspend fun tangemPayProduceInitialCredentials( + cardId: String, + ): CompletionResult { + return coroutineScope { + runTaskAsyncReturnOnMain( + runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this), + cardId = cardId, + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + ) + } + } + // endregion companion object { 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 1568e7dfc8..d86ebd543b 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 @@ -213,5 +213,11 @@ class MockTangemSdkManager( error("Not implemented") } + override suspend fun tangemPayProduceInitialCredentials( + cardId: String, + ): CompletionResult { + error("Not implemented") + } + // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt new file mode 100644 index 0000000000..000ae515ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -0,0 +1,130 @@ +package com.tangem.tap.domain.tasks.visa + +import arrow.core.getOrElse +import com.tangem.common.CompletionResult +import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.common.core.TangemSdkError +import com.tangem.core.error.ext.tangemError +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.card.common.visa.VisaUtilities +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.sign +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( + @Assisted private val coroutineScope: CoroutineScope, + private val dispatchersProvider: CoroutineDispatcherProvider, + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, +) : CardSessionRunnable { + + override fun run(session: CardSession, callback: CompletionCallback) { + coroutineScope.launch { + callback(runSuspend(session = session)) + } + } + + private suspend fun runSuspend(session: CardSession): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + ?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError) + + val derivationResult = runDerivationTask(session, wallet) + val address = when (derivationResult) { + is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error) + is CompletionResult.Success -> generateAddressFromExtendedKey(derivationResult.data) + } + + val challenge = withContext(dispatchersProvider.io) { + visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(address) + }.getOrElse { return CompletionResult.Failure(it.tangemError) } + + val dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge) + val approveResult = runVisaCustomerWalletApproveTask( + session = session, + cardId = card.cardId, + targetAddress = address, + dataToSign = dataToSign, + ) + val signedData = when (approveResult) { + is CompletionResult.Failure<*> -> return CompletionResult.Failure(approveResult.error) + is CompletionResult.Success -> approveResult.data + } + + val authTokens = withContext(dispatchersProvider.io) { + visaAuthRemoteDataSource.getTokenWithCustomerWallet( + sessionId = challenge.session.sessionId, + signature = signedData.signature, + nonce = signedData.dataToSign.hashToSign, + ) + }.getOrNull() ?: return CompletionResult.Failure(VisaActivationError.FailedRemoteState.tangemError) + + return CompletionResult.Success( + data = TangemPayInitialCredentials( + customerWalletAddress = address, + authTokens = authTokens, + ), + ) + } + + private suspend fun runDerivationTask( + session: CardSession, + wallet: CardWallet, + ): CompletionResult { + val deferred = CompletableDeferred>() + val derivationTask = DeriveWalletPublicKeyTask( + walletPublicKey = wallet.publicKey, + derivationPath = VisaUtilities.customDerivationPath, + ) + + derivationTask.run(session = session, callback = deferred::complete) + return deferred.await() + } + + private suspend fun runVisaCustomerWalletApproveTask( + session: CardSession, + cardId: String, + targetAddress: String, + dataToSign: VisaDataToSignByCustomerWallet, + ): CompletionResult { + val deferred = CompletableDeferred>() + val task = VisaCustomerWalletApproveTask( + visaDataForApprove = VisaCustomerWalletApproveTask.Input( + cardId = cardId, + targetAddress = targetAddress, + hashToSign = dataToSign.hashToSign, + sign = dataToSign::sign, + ), + ) + task.run(session = session, callback = deferred::complete) + return deferred.await() + } + + private fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String { + val derivationData = VisaUtilities.visaBlockchain.makeAddressesFromExtendedPublicKey( + extendedPublicKey = extendedPublicKey, + cachedIndex = null, + ) + return derivationData.address + } + + @AssistedFactory + interface Factory { + fun create(coroutineScope: CoroutineScope): TangemPayGenerateAddressAndSignChallengeTask + } +} \ No newline at end of file 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 64fc98c01d..3439d72093 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 @@ -16,7 +16,6 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation @@ -59,21 +58,7 @@ class VisaCustomerWalletApproveTask( session: CardSession, callback: CompletionCallback, ) { - val cardDTO = CardDTO(card) - - val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run { - proceedApproveWithLegacyCard( - card = card, - session = session, - callback = callback, - ) - return - } - - val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run { - callback(CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError)) - return - } + val derivationPath = VisaUtilities.customDerivationPath val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 73c8739caa..675625e1a8 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -33,7 +33,7 @@ }, { "name": "TANGEM_PAY_ENABLED", - "version": "undefined" + "version": "5.30.0" }, { "name": "NEW_TOKEN_RECEIVE_ENABLED", diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index bf8bf7fa78..3d9558f45e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -1,9 +1,13 @@ package com.tangem.datasource.local.visa +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.visa.model.VisaAuthTokens interface TangemPayStorage { + suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) + suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? + suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? @@ -14,5 +18,5 @@ interface TangemPayStorage { suspend fun clearOrderId(customerWalletAddress: String) - suspend fun clearAll(customerWalletAddress: String) + suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt index 450fc7c70d..bca25d2bb9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -4,9 +4,8 @@ import arrow.core.Either import arrow.core.raise.either import com.tangem.common.CompletionResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.sdk.api.TangemSdkManager import javax.inject.Inject @@ -16,29 +15,14 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( private val tangemSdkManager: TangemSdkManager, ) : TangemPayAuthDataSource { - override suspend fun generateNewAuthTokens(address: String, cardId: String): Either = - either { - val challenge = visaAuthRemoteDataSource - .getCustomerWalletAuthChallenge(address) - .mapLeft { IllegalStateException("TangemPay challenge failed. Error code: ${it.errorCode}") } - .bind() + override suspend fun produceInitialCredentials(cardId: String): Either { + val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId) - val signed = tangemSdkManager.visaCustomerWalletApprove( - VisaDataForApprove( - customerWalletCardId = cardId, - targetAddress = address, - dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge), - ), - ).toEither { IllegalStateException("TangemPay signing failed: $it") }.bind() - - visaAuthRemoteDataSource.getTokenWithCustomerWallet( - sessionId = challenge.session.sessionId, - signature = signed.signature, - nonce = signed.dataToSign.hashToSign, - ) - .mapLeft { IllegalStateException("TangemPay token fetch failed. Error code: ${it.errorCode}") } - .bind() + return when (initialCredentials) { + is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error) + is CompletionResult.Success -> Either.Right(initialCredentials.data) } + } override suspend fun refreshAuthTokens(refreshToken: String): Either = either { visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens( @@ -47,9 +31,4 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( .mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") } .bind() } -} - -private fun CompletionResult.toEither(map: (Throwable) -> Throwable) = when (this) { - is CompletionResult.Success -> Either.Right(data) - is CompletionResult.Failure -> Either.Left(map(error)) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index c6222c62c7..40c402edf8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.pay.DataForReceiveFactory import com.tangem.domain.pay.repository.CardDetailsRepository import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository @@ -52,6 +53,14 @@ internal interface TangemPayDataModule { return TangemPayMainScreenCustomerInfoUseCase(repository = repository) } + @Provides + @Singleton + fun provideProduceTangemPayInitialDataUseCase( + repository: OnboardingRepository, + ): ProduceTangemPayInitialDataUseCase { + return ProduceTangemPayInitialDataUseCase(repository = repository) + } + @Provides @Singleton fun provideTangemPayIssueOrderUseCase(repository: OnboardingRepository): TangemPayIssueOrderUseCase { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index dbe40d9c20..2f5582be20 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -2,11 +2,14 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError +import com.tangem.data.pay.util.TangemPayWalletsManager +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest import com.tangem.datasource.api.pay.models.request.OrderRequest import com.tangem.datasource.api.pay.models.response.CustomerMeResponse import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo.ProductInstance @@ -26,17 +29,44 @@ internal class DefaultOnboardingRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, private val tangemPayStorage: TangemPayStorage, + private val authDataSource: TangemPayAuthDataSource, + private val tangemPayWalletsManager: TangemPayWalletsManager, ) : OnboardingRepository { override suspend fun validateDeeplink(link: String): Either { return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.request { - tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) - }.result + val result = tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) + .getOrThrow() + .result result?.status == VALID_STATUS } } + override suspend fun isTangemPayInitialDataProduced(): Boolean { + val walletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId + val customerWalletAddress = tangemPayStorage.getCustomerWalletAddress(walletId) ?: return false + tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return false + + return true + } + + override suspend fun produceInitialData() { + val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay() + val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId) + .fold( + ifLeft = { error -> error("Can not produce initial data: ${error.message}") }, + ifRight = { it }, + ) + tangemPayStorage.storeCustomerWalletAddress( + userWalletId = wallet.walletId, + customerWalletAddress = initialCredentials.customerWalletAddress, + ) + tangemPayStorage.storeAuthTokens( + customerWalletAddress = initialCredentials.customerWalletAddress, + tokens = initialCredentials.authTokens, + ) + } + override suspend fun getCustomerInfo(): Either { return requestHelper.runWithErrorLogs(TAG) { val result = requestHelper.request { authHeader -> @@ -84,7 +114,7 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun createOrder(): Either = withContext(dispatcherProvider.io) { requestHelper.runWithErrorLogs(TAG) { val walletAddress = requestHelper.getCustomerWalletAddress() - val result = requestHelper.requestWithPersistedToken { authHeader -> + val result = requestHelper.request { authHeader -> tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress)) }.result ?: error("Create order result is null") @@ -128,7 +158,7 @@ internal class DefaultOnboardingRepository @Inject constructor( } private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo { - val result = requestHelper.requestWithPersistedToken { authHeader -> + val result = requestHelper.request { authHeader -> tangemPayApi.getCustomerMe(authHeader) }.result return getCustomerInfo(result) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index c52ec65d24..cd24329183 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -2,9 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.squareup.moshi.Moshi -import com.tangem.blockchain.common.Blockchain import com.tangem.core.error.UniversalError -import com.tangem.data.common.network.NetworkFactory import com.tangem.data.pay.util.TangemPayErrorConverter import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.datasource.api.common.response.ApiResponse @@ -14,10 +12,10 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.visa.model.VisaAuthTokens -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.domain.visa.model.getAuthHeader import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -30,11 +28,9 @@ internal class TangemPayRequestPerformer @Inject constructor( private val tangemPayStorage: TangemPayStorage, private val authDataSource: TangemPayAuthDataSource, private val tangemPayWalletsManager: TangemPayWalletsManager, - private val walletManagersFacade: WalletManagersFacade, - private val networkFactory: NetworkFactory, ) { - private var customerWalletAddress: String? = null + private val customerWalletAddress = MutableStateFlow(null) private val refreshTokensMutex = Mutex() private var refreshTokensJob: Deferred? = null @@ -67,21 +63,14 @@ internal class TangemPayRequestPerformer @Inject constructor( ) } - suspend fun requestWithPersistedToken(requestBlock: suspend (header: String) -> ApiResponse): T = - withContext(dispatchers.io) { - performRequest( - requestBlock = requestBlock, - getTokens = { getAccessTokensIfSaved() ?: error("Cannot get saved access tokens") }, - refreshTokens = ::refreshAuthTokens, - ) - } - private suspend fun performRequest( requestBlock: suspend (header: String) -> ApiResponse, getTokens: (suspend () -> VisaAuthTokens), refreshTokens: (suspend () -> VisaAuthTokens)? = null, ): T = runCatching { - requestBlock("Bearer ${getTokens().accessToken}").getOrThrow() + val tokens = getTokens() + val header = tokens.getAuthHeader() + requestBlock(header).getOrThrow() }.getOrElse { error -> val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) { @@ -116,52 +105,34 @@ internal class TangemPayRequestPerformer @Inject constructor( return result } - suspend fun getCustomerWalletAddress(): String = customerWalletAddress ?: fetchAuthInputData().address + suspend fun getCustomerWalletAddress(): String { + val existingAddress = customerWalletAddress.value + if (existingAddress != null) { + return existingAddress + } + val storedAddress = tangemPayStorage.getCustomerWalletAddress( + userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId, + ) ?: error("Can not find customer address") + + customerWalletAddress.value = storedAddress + return storedAddress + } private suspend fun getAccessTokens(): VisaAuthTokens { - return getAccessTokensIfSaved() ?: fetchTokens() - } - - private suspend fun getAccessTokensIfSaved(): VisaAuthTokens? { - return tangemPayStorage.getAuthTokens(getCustomerWalletAddress()) - } - - private suspend fun fetchAuthInputData(): AuthInputData { - val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay() - - val network = networkFactory.create( - blockchain = Blockchain.Polygon, - extraDerivationPath = null, - derivationStyleProvider = wallet.derivationStyleProvider, - canHandleTokens = true, - ) ?: error("Cannot create network") - - val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network) - ?: error("Cannot get polygon address") - - customerWalletAddress = address - - return AuthInputData(address, wallet.cardId) - } - - private suspend fun fetchTokens(): VisaAuthTokens { - val inputData = fetchAuthInputData() - val tokens = authDataSource.generateNewAuthTokens(inputData.address, inputData.cardId) - .getOrNull() ?: error("Cannot fetch tokens") - tangemPayStorage.storeAuthTokens(inputData.address, tokens) + val walletAddress = getCustomerWalletAddress() + val tokens = tangemPayStorage.getAuthTokens(walletAddress) ?: error("Auth tokens are not stored") return tokens } private suspend fun refreshAuthTokens(): VisaAuthTokens { val customerWalletAddress = getCustomerWalletAddress() val refreshToken = getAccessTokens().refreshToken.value - val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull() ?: error("Cannot refresh tokens") + val tokens = authDataSource.refreshAuthTokens(refreshToken) + .fold( + ifLeft = { error -> error("Cannot refresh tokens: ${error.message}") }, + ifRight = { it }, + ) tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens) return tokens } -} - -internal data class AuthInputData( - val address: String, - val cardId: String, -) \ No newline at end of file +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt index 101ec1b9a4..fe41ac4f05 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt @@ -3,6 +3,7 @@ package com.tangem.domain.card.common.visa import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.card.FirmwareVersion +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.CardDTO private const val VISA_BATCH_START = "AE" @@ -16,6 +17,7 @@ object VisaUtilities { val visaDefaultDerivationPath get() = visaBlockchain.derivationPath(DerivationStyle.V3) + val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0") fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayInitialCredentials.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayInitialCredentials.kt new file mode 100644 index 0000000000..40a1132513 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayInitialCredentials.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.visa.model + +data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: VisaAuthTokens) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt index 05725f0183..69ec635f4a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt @@ -1,11 +1,12 @@ package com.tangem.domain.pay.datasource import arrow.core.Either +import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaAuthTokens interface TangemPayAuthDataSource { - suspend fun generateNewAuthTokens(address: String, cardId: String): Either + suspend fun produceInitialCredentials(cardId: String): Either suspend fun refreshAuthTokens(refreshToken: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 5c93a97e19..88d98ab106 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -9,6 +9,10 @@ interface OnboardingRepository { suspend fun validateDeeplink(link: String): Either + suspend fun isTangemPayInitialDataProduced(): Boolean + + suspend fun produceInitialData() + suspend fun getCustomerInfo(): Either suspend fun createOrder(): Either diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt new file mode 100644 index 0000000000..bddeb01eaf --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Either +import arrow.core.Either.Companion.catch +import com.tangem.domain.pay.repository.OnboardingRepository + +class ProduceTangemPayInitialDataUseCase( + private val repository: OnboardingRepository, +) { + + suspend operator fun invoke(): Either { + return catch { + val isDataProduced = repository.isTangemPayInitialDataProduced() + if (isDataProduced) { + return@catch Unit + } else { + repository.produceInitialData() + } + } + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index 0b2af70dd3..a118c239b7 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -39,4 +39,7 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.timber) } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt index 01effd1f8b..4ee5eb136c 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt @@ -21,13 +21,8 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val state by model.screenState.collectAsStateWithLifecycle() - TandemPayOnboardingScreen( - modifier = modifier, - state = state, - onBackClick = model::back, - onOpenKycClick = model::openKyc, - ) + val state by model.uiState.collectAsStateWithLifecycle() + TandemPayOnboardingScreen(modifier = modifier, state = state) } @AssistedFactory diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index 4b41785218..aedb40e989 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -7,12 +7,15 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @@ -22,12 +25,12 @@ internal class TangemPayOnboardingModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val repository: OnboardingRepository, + private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase, ) : Model() { private val params = paramsContainer.require() - - val screenState: StateFlow - field = MutableStateFlow(TangemPayOnboardingScreenState()) + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) init { modelScope.launch { @@ -37,19 +40,15 @@ internal class TangemPayOnboardingModel @Inject constructor( } is TangemPayOnboardingComponent.Params.Deeplink -> { repository.validateDeeplink(params.deeplink) - .onRight { isValid -> if (isValid) checkCustomerInfo() } + .onRight { isValid -> if (isValid) showOnboarding() } .onLeft { back() } } } } } - fun openKyc() { - router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) - } - - fun back() { - router.pop() + private fun showOnboarding() { + uiState.update { it.copy(fullScreenLoading = false) } } private suspend fun checkCustomerInfo() { @@ -59,7 +58,7 @@ internal class TangemPayOnboardingModel @Inject constructor( !customerInfo.isKycApproved -> { when (params) { is TangemPayOnboardingComponent.Params.Deeplink -> - screenState.value = screenState.value.copy(fullScreenLoading = false) + uiState.value = uiState.value.copy(fullScreenLoading = false) else -> openKyc() } } @@ -68,4 +67,48 @@ internal class TangemPayOnboardingModel @Inject constructor( } .onLeft { back() } } + + private fun onGetCardClick() { + uiState.update { it.copy(buttonLoading = true) } + modelScope.launch { + val result = produceInitialDataUseCase() + if (result.isLeft()) { + Timber.e("Error producing initial data: ${result.leftOrNull()?.message}") + uiState.update { it.copy(buttonLoading = false) } + return@launch + } + + repository.getCustomerInfo() + .fold( + ifLeft = { + Timber.e("Error getCustomerInfo: ${it.errorCode}") + uiState.update { it.copy(buttonLoading = false) } + }, + ifRight = { customerInfo -> + if (customerInfo.isKycApproved) { + back() + } else { + openKyc() + } + }, + ) + } + } + + private fun openKyc() { + router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) + } + + private fun back() { + router.pop() + } + + private fun getInitialState(): TangemPayOnboardingScreenState { + return TangemPayOnboardingScreenState( + fullScreenLoading = true, + buttonLoading = false, + onGetCardClick = ::onGetCardClick, + onBackClick = ::back, + ) + } } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt index 8f2605df2e..54f4140025 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt @@ -9,24 +9,21 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero @Composable -internal fun TandemPayOnboardingScreen( - state: TangemPayOnboardingScreenState, - onOpenKycClick: () -> Unit, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) { Scaffold( modifier = modifier.systemBarsPadding(), topBar = { AppBarWithBackButton( modifier = Modifier.statusBarsPadding(), - onBackClick = onBackClick, + onBackClick = state.onBackClick, iconRes = R.drawable.ic_back_24, ) }, @@ -37,7 +34,7 @@ internal fun TandemPayOnboardingScreen( .padding(paddingValues) .fillMaxSize(), state = state, - onButtonClick = onOpenKycClick, + onButtonClick = state.onGetCardClick, ) }, ) @@ -46,8 +43,35 @@ internal fun TandemPayOnboardingScreen( @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewDarkTheme() { +private fun PreviewDarkTheme( + @PreviewParameter(TangemPayOnboardingScreenStateProvider::class) + state: TangemPayOnboardingScreenState, +) { TangemThemePreview { - TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {}, {}) + TandemPayOnboardingScreen(state = state, modifier = Modifier.fillMaxSize()) } -} \ No newline at end of file +} + +private class TangemPayOnboardingScreenStateProvider : + CollectionPreviewParameterProvider( + listOf( + TangemPayOnboardingScreenState( + fullScreenLoading = true, + buttonLoading = false, + onGetCardClick = {}, + onBackClick = {}, + ), + TangemPayOnboardingScreenState( + fullScreenLoading = false, + buttonLoading = false, + onGetCardClick = {}, + onBackClick = {}, + ), + TangemPayOnboardingScreenState( + fullScreenLoading = false, + buttonLoading = true, + onGetCardClick = {}, + onBackClick = {}, + ), + ), + ) \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt index 8745cf3db9..9cf4deb4d6 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt @@ -4,6 +4,8 @@ import javax.annotation.concurrent.Immutable @Immutable internal data class TangemPayOnboardingScreenState( - val fullScreenLoading: Boolean = true, - val buttonLoading: Boolean = false, + val fullScreenLoading: Boolean, + val buttonLoading: Boolean, + val onGetCardClick: () -> Unit, + val onBackClick: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 8099f6dd13..5d318bb9ee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* @@ -93,6 +94,7 @@ internal class WalletModel @Inject constructor( private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, + private val tangemPayOnboardingRepository: OnboardingRepository, private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -344,16 +346,24 @@ internal class WalletModel @Inject constructor( * and every minute while user stays on the main screen */ screenLifecycleProvider.isBackgroundState.onEach { inBackground -> + // fast exit + if (!tangemPayFeatureToggles.isTangemPayEnabled) return@onEach + updateTangemPayJobHolder.cancel() - if (!inBackground && tangemPayFeatureToggles.isTangemPayEnabled) { - modelScope.launch { + + modelScope.launch { + // fast exit + val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced() + if (!initialDataProduced) return@launch + + if (!inBackground) { refreshTangemPayInfo() while (isActive) { delay(TANGEM_PAY_UPDATE_INTERVAL) refreshTangemPayInfo() } - }.saveIn(updateTangemPayJobHolder) - } + } + }.saveIn(updateTangemPayJobHolder) }.launchIn(modelScope) } 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 12aeb18278..25bf3febfa 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,7 +16,10 @@ 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.models.wallet.UserWalletId -import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse @@ -156,5 +159,6 @@ interface TangemSdkManager { visaDataForApprove: VisaDataForApprove, ): CompletionResult + suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult // endregion } \ No newline at end of file