Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-16 10:07:05 +03:00
parent bfd38fd000
commit ea087aac49
21 changed files with 464 additions and 8 deletions

View file

@ -0,0 +1,42 @@
package com.tangem.tap.data
import android.content.Context
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DefaultVisaOTPStorage @Inject constructor(
@ApplicationContext applicationContext: Context,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaOTPStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
override suspend fun saveOTP(cardId: String, otp: ByteArray) = withContext(dispatcherProvider.io) {
secureStorage.store(otp, VISA_OTP_KEY_PREFIX + cardId)
}
override suspend fun getOTP(cardId: String): ByteArray? = withContext(dispatcherProvider.io) {
secureStorage.get(VISA_OTP_KEY_PREFIX + cardId)
}
override suspend fun removeOTP(cardId: String) = withContext(dispatcherProvider.io) {
secureStorage.delete(VISA_OTP_KEY_PREFIX + cardId)
}
private companion object {
const val VISA_OTP_KEY_PREFIX = "visa_otp_"
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -24,6 +25,7 @@ internal class TangemSdkManagerModule {
@ApplicationContext context: Context, @ApplicationContext context: Context,
cardSdkConfigRepository: CardSdkConfigRepository, cardSdkConfigRepository: CardSdkConfigRepository,
visaCardScanHandler: VisaCardScanHandler, visaCardScanHandler: VisaCardScanHandler,
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
): TangemSdkManager { ): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) { return if (BuildConfig.MOCK_DATA_SOURCE) {
MockTangemSdkManager(resources = context.resources) MockTangemSdkManager(resources = context.resources)
@ -32,6 +34,7 @@ internal class TangemSdkManagerModule {
cardSdkConfigRepository = cardSdkConfigRepository, cardSdkConfigRepository = cardSdkConfigRepository,
resources = context.resources, resources = context.resources,
visaCardScanHandler = visaCardScanHandler, visaCardScanHandler = visaCardScanHandler,
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
) )
} }
} }

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.data package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.tap.data.DefaultVisaAuthTokenStorage import com.tangem.tap.data.DefaultVisaAuthTokenStorage
import com.tangem.tap.data.DefaultVisaOTPStorage
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -15,4 +17,8 @@ internal interface VisaStorageModule {
@Binds @Binds
@Singleton @Singleton
fun bindVisaStorage(impl: DefaultVisaAuthTokenStorage): VisaAuthTokenStorage fun bindVisaStorage(impl: DefaultVisaAuthTokenStorage): VisaAuthTokenStorage
@Binds
@Singleton
fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage
} }

View file

@ -24,6 +24,9 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.ScanTask import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
@ -40,6 +43,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask import com.tangem.tap.domain.twins.FinalizeTwinTask
@ -55,6 +59,7 @@ internal class DefaultTangemSdkManager(
private val cardSdkConfigRepository: CardSdkConfigRepository, private val cardSdkConfigRepository: CardSdkConfigRepository,
private val resources: Resources, private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler, private val visaCardScanHandler: VisaCardScanHandler,
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
) : TangemSdkManager { ) : TangemSdkManager {
private val awaitInitializationMutex = Mutex() private val awaitInitializationMutex = Mutex()
@ -466,6 +471,28 @@ internal class DefaultTangemSdkManager(
// endregion // endregion
// region Visa-specific
override suspend fun activateVisaCard(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse> {
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = visaCardActivationTaskFactory.create(
accessCode = accessCode,
challengeToSign = challengeToSign,
activationInput = activationInput,
coroutineScope = this,
),
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
}
// endregion
companion object { companion object {
private const val MAX_INITIALIZE_ATTEMPTS = 10 private const val MAX_INITIALIZE_ATTEMPTS = 10
} }

View file

@ -17,6 +17,9 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.preflightread.PreflightReadFilter
@ -194,4 +197,14 @@ class MockTangemSdkManager(
} }
// endregion // endregion
// region Visa-specific
override suspend fun activateVisaCard(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse> {
error("Not implemented")
}
} }

View file

@ -0,0 +1,268 @@
package com.tangem.tap.domain.tasks.visa
import com.reown.util.hexToBytes
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletTask
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.jvm.Throws
@Suppress("LongParameterList")
class VisaCardActivationTask @AssistedInject constructor(
@Assisted private val accessCode: String,
@Assisted private val challengeToSign: VisaAuthChallenge.Card?,
@Assisted private val activationInput: VisaActivationInput,
@Assisted private val coroutineScope: CoroutineScope,
private val otpStorage: VisaOTPStorage,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository,
) : CardSessionRunnable<VisaCardActivationResponse> {
override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) {
coroutineScope.launch {
callback(runSuspend(session))
}
}
private suspend fun runSuspend(session: CardSession): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
if (card.cardId != activationInput.cardId) {
return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message))
}
return if (challengeToSign != null) {
signAuthorizationChallenge(session, challengeToSign)
} else {
val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
signOrder(session, activationOrder)
}
}
private suspend fun signAuthorizationChallenge(
session: CardSession,
challengeToSign: VisaAuthChallenge.Card,
): CompletionResult<VisaCardActivationResponse> {
val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes())
val result = suspendCancellableCoroutine { continuation ->
attestationCommand.run(session = session) { attestationResponse ->
continuation.resume(attestationResponse)
}
}
return when (result) {
is CompletionResult.Success -> {
processSignedAuthorizationChallenge(
session = session,
signedChallenge = challengeToSign.toSignedChallenge(
signedChallenge = result.data.cardSignature.toHexString(),
salt = result.data.salt.toHexString(),
),
)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun processSignedAuthorizationChallenge(
session: CardSession,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> {
return coroutineScope {
val activationOrderDeferred = async { getActivationOrderToSign(signedChallenge) }
val otpTaskDeferred = async { createWallet(session) }
val order = runCatching { activationOrderDeferred.await() }
.getOrElse {
otpTaskDeferred.cancel()
return@coroutineScope CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
otpTaskDeferred.await()
signOrder(session, order)
}
}
@Throws(TangemSdkError::class)
private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder {
val tokens = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge)
}.getOrElse {
throw TangemSdkError.Underlying(
"Underlying network error: ${it.message ?: ""}",
)
}
visaAuthTokenStorage.store(tokens)
return visaActivationRepository.getActivationOrderToSign()
}
private suspend fun createWallet(session: CardSession): 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)
} else {
val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve)
val result = suspendCancellableCoroutine { continuation ->
createWalletTask.run(session) { createWalletResult ->
continuation.resume(createWalletResult)
}
}
when (result) {
is CompletionResult.Success -> {
createOTP(session)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
}
private suspend fun createOTP(session: CardSession): CompletionResult<Unit> {
coroutineScope { ensureActive() }
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val otp = otpStorage.getOTP(card.cardId)
return if (otp != null) {
CompletionResult.Success(Unit)
} else {
val otpCommand = GenerateOTPCommand()
val result = suspendCancellableCoroutine { continuation ->
otpCommand.run(session) { otpResult ->
continuation.resume(otpResult)
}
}
when (result) {
is CompletionResult.Success -> {
otpStorage.saveOTP(card.cardId, result.data.rootOTP)
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
}
private suspend fun signOrder(
session: CardSession,
order: ActivationOrder,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey)
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { signResult ->
continuation.resume(signResult)
}
}
return when (result) {
is CompletionResult.Success -> {
handleSignedOrder(
session = session,
activationOrder = order,
response = result.data,
)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun handleSignedOrder(
session: CardSession,
activationOrder: ActivationOrder,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val signedOrder = SignedActivationOrder(
activationOrder = activationOrder,
signature = response.signature.toHexString(),
)
val otp = otpStorage.getOTP(card.cardId) ?: return CompletionResult.Failure(
TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message),
)
val activationResponse = VisaCardActivationResponse(
signedActivationOrder = signedOrder,
rootOTP = VisaRootOTP(otp.toHexString()),
)
return setupAccessCode(session).map { activationResponse }
}
private suspend fun setupAccessCode(session: CardSession): CompletionResult<Unit> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
if (card.isAccessCodeSet) {
return CompletionResult.Success(Unit)
}
val task = SetUserCodeCommand.changeAccessCode(accessCode)
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { setAccessCodeResult ->
continuation.resume(setAccessCodeResult)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
@AssistedFactory
interface Factory {
fun create(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
coroutineScope: CoroutineScope,
): VisaCardActivationTask
}
}

View file

@ -61,7 +61,7 @@ internal object UserWalletsListManagerModule {
.add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaCardActivationStatus.serializer) .add(VisaCardActivationStatus.jsonAdapter)
.add(KotlinJsonAdapterFactory()) .add(KotlinJsonAdapterFactory())
.build() .build()

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters
import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaCardActivationStatus
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -34,6 +35,7 @@ class MoshiModule {
.add(BigDecimalAdapter()) .add(BigDecimalAdapter())
.add(LocalDateAdapter()) .add(LocalDateAdapter())
.add(DateTimeAdapter()) .add(DateTimeAdapter())
.add(VisaCardActivationStatus.jsonAdapter)
.add(KotlinJsonAdapterFactory()) .add(KotlinJsonAdapterFactory())
.addStakeKitEnumFallbackAdapters() .addStakeKitEnumFallbackAdapters()
.build() .build()
@ -42,8 +44,26 @@ class MoshiModule {
@Provides @Provides
@Singleton @Singleton
@SdkMoshi @SdkMoshi
fun provideSdkMoshi(sdkMoshiJsonConverter: MoshiJsonConverter): Moshi { fun provideSdkMoshi(): Moshi {
return sdkMoshiJsonConverter.moshi val adapters = MoshiJsonConverter.getTangemSdkAdapters() +
listOf(
BigDecimalAdapter(),
WalletDerivedKeysMapAdapter(),
ScanResponseDerivedKeysMapAdapter(),
ByteArrayKeyAdapter(),
ExtendedPublicKeysMapAdapter(),
CardBackupStatusAdapter(),
DerivationPathAdapterWithMigration(),
)
val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters()
return Moshi.Builder().apply {
add(VisaCardActivationStatus.jsonAdapter)
adapters.forEach { this.add(it) }
typedAdapters.forEach { add(it.key, it.value) }
add(KotlinJsonAdapterFactory())
}.build()
} }
@Provides @Provides

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.visa
interface VisaOTPStorage {
suspend fun saveOTP(cardId: String, otp: ByteArray)
suspend fun getOTP(cardId: String): ByteArray?
suspend fun removeOTP(cardId: String)
}
suspend fun VisaOTPStorage.hasSavedOTP(cardId: String): Boolean = getOTP(cardId) != null

View file

@ -2,6 +2,7 @@ package com.tangem.data.visa
import com.tangem.data.visa.converter.VisaActivationStatusConverter import com.tangem.data.visa.converter.VisaActivationStatusConverter
import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.datasource.api.visa.TangemVisaApi
import com.tangem.domain.visa.model.ActivationOrder
import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -20,4 +21,8 @@ internal class DefaultVisaActivationRepository @Inject constructor(
visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus()) visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus())
// TODO implement refreshing access token if it's expired // TODO implement refreshing access token if it's expired
} }
override suspend fun getActivationOrderToSign(): ActivationOrder {
return ActivationOrder("TODO implement")
}
} }

View file

@ -23,7 +23,7 @@ internal object ScanResponseAsStringSerializer : KSerializer<ScanResponse> {
.add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaCardActivationStatus.serializer) .add(VisaCardActivationStatus.jsonAdapter)
.add(KotlinJsonAdapterFactory()) .add(KotlinJsonAdapterFactory())
.build() .build()

View file

@ -0,0 +1,5 @@
package com.tangem.domain.visa.model
data class ActivationOrder(
val hash: String,
)

View file

@ -0,0 +1,6 @@
package com.tangem.domain.visa.model
data class SignedActivationOrder(
val activationOrder: ActivationOrder,
val signature: String,
)

View file

@ -3,4 +3,6 @@ package com.tangem.domain.visa.model
enum class VisaActivationError(val message: String) { enum class VisaActivationError(val message: String) {
BlockedForActivation("Card is blocked for activation"), BlockedForActivation("Card is blocked for activation"),
InvalidActivationState("Invalid activation state"), InvalidActivationState("Invalid activation state"),
WrongCard("Wrong card tapped"),
MissingRootOTP("Missing root OTP"),
} }

View file

@ -0,0 +1,6 @@
package com.tangem.domain.visa.model
data class VisaCardActivationResponse(
val signedActivationOrder: SignedActivationOrder,
val rootOTP: VisaRootOTP,
)

View file

@ -28,7 +28,7 @@ sealed class VisaCardActivationStatus {
data object Blocked : VisaCardActivationStatus() data object Blocked : VisaCardActivationStatus()
companion object { companion object {
val serializer: PolymorphicJsonAdapterFactory<VisaCardActivationStatus> val jsonAdapter: PolymorphicJsonAdapterFactory<VisaCardActivationStatus>
get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type") get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type")
.withSubtype(Activated::class.java, "Activated") .withSubtype(Activated::class.java, "Activated")
.withSubtype(ActivationStarted::class.java, "ActivationStarted") .withSubtype(ActivationStarted::class.java, "ActivationStarted")

View file

@ -0,0 +1,4 @@
package com.tangem.domain.visa.model
@JvmInline
value class VisaRootOTP(val value: String)

View file

@ -1,8 +1,11 @@
package com.tangem.domain.visa.repository 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.VisaActivationRemoteState
interface VisaActivationRepository { interface VisaActivationRepository {
suspend fun getActivationRemoteState(): VisaActivationRemoteState suspend fun getActivationRemoteState(): VisaActivationRemoteState
suspend fun getActivationOrderToSign(): ActivationOrder
} }

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -13,6 +14,7 @@ import javax.inject.Inject
@ComponentScoped @ComponentScoped
internal class OnboardingVisaAccessCodeModel @Inject constructor( internal class OnboardingVisaAccessCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
) : Model() { ) : Model() {
private val _uiState = MutableStateFlow(getInitialState()) private val _uiState = MutableStateFlow(getInitialState())
@ -44,7 +46,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
_uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) } _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) }
} }
OnboardingVisaAccessCodeUM.Step.ReEnter -> { OnboardingVisaAccessCodeUM.Step.ReEnter -> {
startActivationProcess() startActivationProcess(accessCode = uiState.value.accessCodeFirst.text)
} }
} }
} }
@ -67,8 +69,26 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
return true return true
} }
private fun startActivationProcess() { private fun startActivationProcess(accessCode: String) {
TODO() modelScope.launch {
@Suppress("UnusedPrivateMember")
val result = tangemSdkManager.activateVisaCard(
accessCode = accessCode,
challengeToSign = null,
activationInput = TODO(),
)
// when (result) {
// is CompletionResult.Success -> {
// val response = result.data
// TODO()
// }
// is CompletionResult.Failure -> {
// // show alert
// TODO()
// }
// }
}
} }
private companion object { private companion object {

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.domain.card) implementation(projects.domain.card)
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.visa.models)
implementation(projects.core.res) implementation(projects.core.res)

View file

@ -15,6 +15,9 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.preflightread.PreflightReadFilter
@ -141,4 +144,14 @@ interface TangemSdkManager {
fun clearProductType() fun clearProductType()
// endregion // endregion
// region Visa-specific
suspend fun activateVisaCard(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse>
// endregion
} }