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