Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-22 16:12:30 +05:00
parent 3ffffa73ec
commit 616386c345
18 changed files with 310 additions and 125 deletions

View file

@ -2,8 +2,9 @@ package com.tangem.tap.data
import android.content.Context import android.content.Context
import com.squareup.moshi.Moshi 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.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -11,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.text.encodeToByteArray
private const val DEFAULT_KEY = "tangem_pay_default_key" private const val DEFAULT_KEY = "tangem_pay_default_key"
private const val ORDER_ID_KEY = "tangem_pay_order_id_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 @Singleton
internal class DefaultTangemPayStorage @Inject constructor( internal class DefaultTangemPayStorage @Inject constructor(
@ApplicationContext applicationContext: Context, @ApplicationContext applicationContext: Context,
@NetworkMoshi moshi: Moshi,
private val dispatcherProvider: CoroutineDispatcherProvider, private val dispatcherProvider: CoroutineDispatcherProvider,
) : TangemPayStorage { ) : TangemPayStorage {
@ -29,14 +30,21 @@ internal class DefaultTangemPayStorage @Inject constructor(
name = "tangem_pay_storage", name = "tangem_pay_storage",
) )
} }
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) } 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) = override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) =
withContext(dispatcherProvider.io) { withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens) val json = tokensAdapter.toJson(tokens)
@ -71,10 +79,14 @@ internal class DefaultTangemPayStorage @Inject constructor(
secureStorage.delete(createOrderIdKey(customerWalletAddress)) secureStorage.delete(createOrderIdKey(customerWalletAddress))
} }
override suspend fun clearAll(customerWalletAddress: String) = withContext(dispatcherProvider.io) { override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
secureStorage.delete(createKey(customerWalletAddress)) withContext(dispatcherProvider.io) {
secureStorage.delete(createOrderIdKey(customerWalletAddress)) 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" private fun createKey(address: String): String = "${DEFAULT_KEY}_$address"

View file

@ -7,6 +7,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager 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.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
import dagger.Module import dagger.Module
@ -27,6 +28,7 @@ internal class TangemSdkManagerModule {
cardSdkConfigRepository: CardSdkConfigRepository, cardSdkConfigRepository: CardSdkConfigRepository,
visaCardScanHandler: VisaCardScanHandler, visaCardScanHandler: VisaCardScanHandler,
visaCardActivationTaskFactory: VisaCardActivationTask.Factory, visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
onboardingV2FeatureToggles: OnboardingV2FeatureToggles, onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
): TangemSdkManager { ): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) { return if (BuildConfig.MOCK_DATA_SOURCE) {
@ -37,6 +39,7 @@ internal class TangemSdkManagerModule {
resources = context.resources, resources = context.resources,
visaCardScanHandler = visaCardScanHandler, visaCardScanHandler = visaCardScanHandler,
visaCardActivationTaskFactory = visaCardActivationTaskFactory, visaCardActivationTaskFactory = visaCardActivationTaskFactory,
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
onboardingV2FeatureToggles = onboardingV2FeatureToggles, onboardingV2FeatureToggles = onboardingV2FeatureToggles,
) )
} }

View file

@ -18,13 +18,17 @@ import com.tangem.core.res.getStringSafe
import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey 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.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.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId 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.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse 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.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask 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.VisaCardActivationTask
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
@ -62,6 +67,7 @@ internal class DefaultTangemSdkManager(
private val resources: Resources, private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler, private val visaCardScanHandler: VisaCardScanHandler,
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : TangemSdkManager { ) : TangemSdkManager {
@ -511,6 +517,18 @@ internal class DefaultTangemSdkManager(
) )
} }
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this),
cardId = cardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
}
// endregion // endregion
companion object { companion object {

View file

@ -213,5 +213,11 @@ class MockTangemSdkManager(
error("Not implemented") error("Not implemented")
} }
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
error("Not implemented")
}
// endregion // endregion
} }

View file

@ -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<TangemPayInitialCredentials> {
override fun run(session: CardSession, callback: CompletionCallback<TangemPayInitialCredentials>) {
coroutineScope.launch {
callback(runSuspend(session = session))
}
}
private suspend fun runSuspend(session: CardSession): CompletionResult<TangemPayInitialCredentials> {
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<ExtendedPublicKey> -> 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<VisaSignedDataByCustomerWallet> -> 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<ExtendedPublicKey> {
val deferred = CompletableDeferred<CompletionResult<ExtendedPublicKey>>()
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<VisaSignedDataByCustomerWallet> {
val deferred = CompletableDeferred<CompletionResult<VisaSignedDataByCustomerWallet>>()
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
}
}

View file

@ -16,7 +16,6 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey 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.VisaUtilities
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
@ -59,21 +58,7 @@ class VisaCustomerWalletApproveTask(
session: CardSession, session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>, callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) { ) {
val cardDTO = CardDTO(card) val derivationPath = VisaUtilities.customDerivationPath
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 wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError))

View file

@ -1,9 +1,13 @@
package com.tangem.datasource.local.visa package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
interface TangemPayStorage { interface TangemPayStorage {
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
@ -14,5 +18,5 @@ interface TangemPayStorage {
suspend fun clearOrderId(customerWalletAddress: String) suspend fun clearOrderId(customerWalletAddress: String)
suspend fun clearAll(customerWalletAddress: String) suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
} }

View file

@ -4,9 +4,8 @@ import arrow.core.Either
import arrow.core.raise.either import arrow.core.raise.either
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource 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.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import javax.inject.Inject import javax.inject.Inject
@ -16,29 +15,14 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
private val tangemSdkManager: TangemSdkManager, private val tangemSdkManager: TangemSdkManager,
) : TangemPayAuthDataSource { ) : TangemPayAuthDataSource {
override suspend fun generateNewAuthTokens(address: String, cardId: String): Either<Throwable, VisaAuthTokens> = override suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> {
either { val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)
val challenge = visaAuthRemoteDataSource
.getCustomerWalletAuthChallenge(address)
.mapLeft { IllegalStateException("TangemPay challenge failed. Error code: ${it.errorCode}") }
.bind()
val signed = tangemSdkManager.visaCustomerWalletApprove( return when (initialCredentials) {
VisaDataForApprove( is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error)
customerWalletCardId = cardId, is CompletionResult.Success<TangemPayInitialCredentials> -> Either.Right(initialCredentials.data)
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()
} }
}
override suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens> = either { override suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens> = either {
visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens( visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens(
@ -47,9 +31,4 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
.mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") } .mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") }
.bind() .bind()
} }
}
private fun <T> CompletionResult<T>.toEither(map: (Throwable) -> Throwable) = when (this) {
is CompletionResult.Success -> Either.Right(data)
is CompletionResult.Failure -> Either.Left(map(error))
} }

View file

@ -2,6 +2,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either import arrow.core.Either
import com.tangem.core.error.UniversalError import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest 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.request.OrderRequest
@ -30,9 +31,9 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> { override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
return requestHelper.runWithErrorLogs(TAG) { return requestHelper.runWithErrorLogs(TAG) {
val result = requestHelper.request { val result = tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) .getOrThrow()
}.result .result
result?.status == VALID_STATUS result?.status == VALID_STATUS
} }
} }

View file

@ -2,9 +2,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either import arrow.core.Either
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.error.UniversalError 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.TangemPayErrorConverter
import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.datasource.api.common.response.ApiResponse 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.datasource.local.visa.TangemPayStorage
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
@ -30,11 +28,9 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val tangemPayStorage: TangemPayStorage, private val tangemPayStorage: TangemPayStorage,
private val authDataSource: TangemPayAuthDataSource, private val authDataSource: TangemPayAuthDataSource,
private val tangemPayWalletsManager: TangemPayWalletsManager, private val tangemPayWalletsManager: TangemPayWalletsManager,
private val walletManagersFacade: WalletManagersFacade,
private val networkFactory: NetworkFactory,
) { ) {
private var customerWalletAddress: String? = null private val customerWalletAddress = MutableStateFlow<String?>(null)
private val refreshTokensMutex = Mutex() private val refreshTokensMutex = Mutex()
private var refreshTokensJob: Deferred<VisaAuthTokens>? = null private var refreshTokensJob: Deferred<VisaAuthTokens>? = null
@ -81,7 +77,9 @@ internal class TangemPayRequestPerformer @Inject constructor(
getTokens: (suspend () -> VisaAuthTokens), getTokens: (suspend () -> VisaAuthTokens),
refreshTokens: (suspend () -> VisaAuthTokens)? = null, refreshTokens: (suspend () -> VisaAuthTokens)? = null,
): T = runCatching { ): T = runCatching {
requestBlock("Bearer ${getTokens().accessToken}").getOrThrow() val tokens = getTokens()
val header = tokens.getAuthHeader()
requestBlock(header).getOrThrow()
}.getOrElse { error -> }.getOrElse { error ->
val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED
if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) { if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) {
@ -116,7 +114,18 @@ internal class TangemPayRequestPerformer @Inject constructor(
return result 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 { private suspend fun getAccessTokens(): VisaAuthTokens {
return getAccessTokensIfSaved() ?: fetchTokens() return getAccessTokensIfSaved() ?: fetchTokens()
@ -126,42 +135,33 @@ internal class TangemPayRequestPerformer @Inject constructor(
return tangemPayStorage.getAuthTokens(getCustomerWalletAddress()) 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 { private suspend fun fetchTokens(): VisaAuthTokens {
val inputData = fetchAuthInputData() val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
val tokens = authDataSource.generateNewAuthTokens(inputData.address, inputData.cardId) val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId)
.getOrNull() ?: error("Cannot fetch tokens") .getOrThrowWithMessage("Can not produce initial data:")
tangemPayStorage.storeAuthTokens(inputData.address, tokens) tangemPayStorage.storeCustomerWalletAddress(
return tokens userWalletId = wallet.walletId,
customerWalletAddress = initialCredentials.customerWalletAddress,
)
tangemPayStorage.storeAuthTokens(
customerWalletAddress = initialCredentials.customerWalletAddress,
tokens = initialCredentials.authTokens,
)
return initialCredentials.authTokens
} }
private suspend fun refreshAuthTokens(): VisaAuthTokens { private suspend fun refreshAuthTokens(): VisaAuthTokens {
val customerWalletAddress = getCustomerWalletAddress() val customerWalletAddress = getCustomerWalletAddress()
val refreshToken = getAccessTokens().refreshToken.value val refreshToken = getAccessTokens().refreshToken.value
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull() ?: error("Cannot refresh tokens") val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrThrowWithMessage("Cannot refresh tokens:")
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens) tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
return tokens return tokens
} }
}
internal data class AuthInputData( private fun <A : Throwable, B> Either<A, B>.getOrThrowWithMessage(message: String): B {
val address: String, return this.fold(
val cardId: String, ifLeft = { error -> throw IllegalStateException("$message ${error.message}") },
) ifRight = { it },
)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.card.common.visa
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.card.FirmwareVersion import com.tangem.common.card.FirmwareVersion
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
private const val VISA_BATCH_START = "AE" private const val VISA_BATCH_START = "AE"
@ -16,6 +17,7 @@ object VisaUtilities {
val visaDefaultDerivationPath val visaDefaultDerivationPath
get() = visaBlockchain.derivationPath(DerivationStyle.V3) get() = visaBlockchain.derivationPath(DerivationStyle.V3)
val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0")
fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style) fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style)

View file

@ -0,0 +1,3 @@
package com.tangem.domain.visa.model
data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: VisaAuthTokens)

View file

@ -1,11 +1,12 @@
package com.tangem.domain.pay.datasource package com.tangem.domain.pay.datasource
import arrow.core.Either import arrow.core.Either
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
interface TangemPayAuthDataSource { interface TangemPayAuthDataSource {
suspend fun generateNewAuthTokens(address: String, cardId: String): Either<Throwable, VisaAuthTokens> suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens> suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens>
} }

View file

@ -22,12 +22,7 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor(
@Composable @Composable
override fun Content(modifier: Modifier) { override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle() val state by model.screenState.collectAsStateWithLifecycle()
TandemPayOnboardingScreen( TandemPayOnboardingScreen(modifier = modifier, state = state)
modifier = modifier,
state = state,
onBackClick = model::back,
onOpenKycClick = model::openKyc,
)
} }
@AssistedFactory @AssistedFactory

View file

@ -12,6 +12,7 @@ import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +28,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>() private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
val screenState: StateFlow<TangemPayOnboardingScreenState> val screenState: StateFlow<TangemPayOnboardingScreenState>
field = MutableStateFlow(TangemPayOnboardingScreenState()) field = MutableStateFlow(getInitialState())
init { init {
modelScope.launch { modelScope.launch {
@ -37,21 +38,27 @@ internal class TangemPayOnboardingModel @Inject constructor(
} }
is TangemPayOnboardingComponent.Params.Deeplink -> { is TangemPayOnboardingComponent.Params.Deeplink -> {
repository.validateDeeplink(params.deeplink) repository.validateDeeplink(params.deeplink)
.onRight { isValid -> if (isValid) checkCustomerInfo() } .onRight { isValid -> if (isValid) showOnboarding() }
.onLeft { back() } .onLeft { back() }
} }
} }
} }
} }
fun openKyc() { private fun openKyc() {
router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) router.replaceAll(AppRoute.Wallet, AppRoute.Kyc)
} }
fun back() { private fun back() {
router.pop() router.pop()
} }
private fun showOnboarding() {
screenState.update {
it.copy(fullScreenLoading = false)
}
}
private suspend fun checkCustomerInfo() { private suspend fun checkCustomerInfo() {
repository.getCustomerInfo() repository.getCustomerInfo()
.onRight { customerInfo -> .onRight { customerInfo ->
@ -68,4 +75,13 @@ internal class TangemPayOnboardingModel @Inject constructor(
} }
.onLeft { back() } .onLeft { back() }
} }
private fun getInitialState(): TangemPayOnboardingScreenState {
return TangemPayOnboardingScreenState(
fullScreenLoading = true,
buttonLoading = false,
onGetCardClick = ::openKyc,
onBackClick = ::back,
)
}
} }

View file

@ -9,24 +9,21 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview 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.R
import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.WindowInsetsZero
@Composable @Composable
internal fun TandemPayOnboardingScreen( internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) {
state: TangemPayOnboardingScreenState,
onOpenKycClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold( Scaffold(
modifier = modifier.systemBarsPadding(), modifier = modifier.systemBarsPadding(),
topBar = { topBar = {
AppBarWithBackButton( AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(), modifier = Modifier.statusBarsPadding(),
onBackClick = onBackClick, onBackClick = state.onBackClick,
iconRes = R.drawable.ic_back_24, iconRes = R.drawable.ic_back_24,
) )
}, },
@ -37,7 +34,7 @@ internal fun TandemPayOnboardingScreen(
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize(), .fillMaxSize(),
state = state, state = state,
onButtonClick = onOpenKycClick, onButtonClick = state.onGetCardClick,
) )
}, },
) )
@ -46,8 +43,35 @@ internal fun TandemPayOnboardingScreen(
@Preview(showBackground = true) @Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
private fun PreviewDarkTheme() { private fun PreviewDarkTheme(
@PreviewParameter(TangemPayOnboardingScreenStateProvider::class)
state: TangemPayOnboardingScreenState,
) {
TangemThemePreview { TangemThemePreview {
TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {}, {}) TandemPayOnboardingScreen(state = state, modifier = Modifier.fillMaxSize())
} }
} }
private class TangemPayOnboardingScreenStateProvider :
CollectionPreviewParameterProvider<TangemPayOnboardingScreenState>(
listOf(
TangemPayOnboardingScreenState(
fullScreenLoading = true,
buttonLoading = false,
onGetCardClick = {},
onBackClick = {},
),
TangemPayOnboardingScreenState(
fullScreenLoading = false,
buttonLoading = false,
onGetCardClick = {},
onBackClick = {},
),
TangemPayOnboardingScreenState(
fullScreenLoading = false,
buttonLoading = true,
onGetCardClick = {},
onBackClick = {},
),
),
)

View file

@ -4,6 +4,8 @@ import javax.annotation.concurrent.Immutable
@Immutable @Immutable
internal data class TangemPayOnboardingScreenState( internal data class TangemPayOnboardingScreenState(
val fullScreenLoading: Boolean = true, val fullScreenLoading: Boolean,
val buttonLoading: Boolean = false, val buttonLoading: Boolean,
val onGetCardClick: () -> Unit,
val onBackClick: () -> Unit,
) )

View file

@ -16,7 +16,10 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId 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.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletResponse
@ -156,5 +159,6 @@ interface TangemSdkManager {
visaDataForApprove: VisaDataForApprove, visaDataForApprove: VisaDataForApprove,
): CompletionResult<VisaSignedDataByCustomerWallet> ): CompletionResult<VisaSignedDataByCustomerWallet>
suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult<TangemPayInitialCredentials>
// endregion // endregion
} }