Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-14 11:04:06 +03:00
parent 6111067060
commit 8ca71f0788
25 changed files with 554 additions and 20 deletions

View file

@ -233,6 +233,7 @@ dependencies {
implementation(deps.reownWeb3)
implementation(deps.prettyLogger)
implementation(deps.decompose.ext.compose)
implementation(deps.moshi.adapters)
/** Testing libraries */
testImplementation(deps.test.coroutine)

View file

@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.visa.VisaCardScanHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,18 +16,23 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TangemSdkManagerModule {
internal class TangemSdkManagerModule {
@Provides
@Singleton
fun provideTangemSdkManager(
@ApplicationContext context: Context,
cardSdkConfigRepository: CardSdkConfigRepository,
visaCardScanHandler: VisaCardScanHandler,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
MockTangemSdkManager(resources = context.resources)
} else {
DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources)
DefaultTangemSdkManager(
cardSdkConfigRepository = cardSdkConfigRepository,
resources = context.resources,
visaCardScanHandler = visaCardScanHandler,
)
}
}
}

View file

@ -43,19 +43,18 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask
import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@Suppress("TooManyFunctions", "LargeClass")
class DefaultTangemSdkManager(
internal class DefaultTangemSdkManager(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler,
) : TangemSdkManager {
private val awaitInitializationMutex = Mutex()
@ -127,16 +126,20 @@ class DefaultTangemSdkManager(
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<ScanResponse> {
val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = ScanProductTask(
card = null,
derivationsFinder = derivationsFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this,
),
cardId = cardId,
initialMessage = message,
).also { sendScanResultsToAnalytics(it) }
}
}
override suspend fun createProductWallet(
scanResponse: ScanResponse,

View file

@ -9,6 +9,7 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.deserialization.WalletDataDeserializer
import com.tangem.common.extensions.*
import com.tangem.common.map
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
@ -22,6 +23,7 @@ import com.tangem.domain.common.TapWorkarounds.isVisa
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
@ -35,16 +37,20 @@ import com.tangem.operations.files.ReadFilesTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.collections.set
internal class ScanProductTask(
private val card: Card?,
private val derivationsFinder: DerivationsFinder?,
private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?,
override val allowsRequestAccessCodeFromRepository: Boolean = false,
) : CardSessionRunnable<ScanResponse> {
@ -61,6 +67,16 @@ internal class ScanProductTask(
return
}
if (VisaUtilities.isVisaCard(cardDto)) {
readVisaCard(
session = session,
cardDto = cardDto,
scanWalletProcessor = ScanWalletProcessor(derivationsFinder),
callback = callback,
)
return
}
val commandProcessor = when {
cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(derivationsFinder)
@ -96,6 +112,43 @@ internal class ScanProductTask(
}
return null
}
private fun readVisaCard(
session: CardSession,
cardDto: CardDTO,
scanWalletProcessor: ScanWalletProcessor,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
visaCardScanHandler ?: run {
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
return
}
visaCoroutineScope ?: run {
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
return
}
visaCoroutineScope.launch {
when (val result = visaCardScanHandler.handleVisaCardScan(session = session)) {
is CompletionResult.Success -> {
scanWalletProcessor.proceed(
card = cardDto,
session = session,
) { scanResponseResult ->
callback(
scanResponseResult.map { scanResponse ->
scanResponse.copy(visaCardActivationStatus = result.data)
},
)
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}
private class ScanWalletProcessor(

View file

@ -23,8 +23,12 @@ class FinalizeTwinTask(
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success ->
ScanProductTask(readResult.data, derivationsFinder = null)
.run(session, callback)
ScanProductTask(
readResult.data,
derivationsFinder = null,
visaCardScanHandler = null,
visaCoroutineScope = null,
).run(session, callback)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(readResult.error))
}

View file

@ -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.VisaCardActivationStatus
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
@ -60,6 +61,7 @@ internal object UserWalletsListManagerModule {
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaCardActivationStatus.serializer)
.add(KotlinJsonAdapterFactory())
.build()

View file

@ -0,0 +1,256 @@
package com.tangem.tap.domain.visa
import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.core.CardSession
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.toSignedChallenge
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
internal class VisaCardScanHandler @Inject constructor(
private val visaAuthRepository: VisaAuthRepository,
) {
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Attempting to handle Visa card scan")
val card = session.environment.card ?: run {
Timber.e("Card is null")
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
}
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
val activationInput =
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput)
return CompletionResult.Success(activationStatus)
}
return deriveKey(wallet, session)
}
private suspend fun deriveKey(
wallet: CardWallet,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while first scan")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
)
}
val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath)
val derivationTaskResult = suspendCancellableCoroutine { continuation ->
derivationTask.run(session) { result ->
continuation.resume(result)
}
}
return handleDerivationResponse(derivationTaskResult, session)
}
private suspend fun handleDerivationResponse(
result: CompletionResult<ExtendedPublicKey>,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
return when (result) {
is CompletionResult.Success -> {
Timber.i("Start task for loading challenge for Visa wallet")
handleWalletAuthorization(session)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Started handling authorization using Visa wallet")
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while handling wallet authorization")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
)
}
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
)
}
val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
)
}
Timber.i("Requesting challenge for wallet authorization")
// Will be changed later after backend implementation
val challengeResponse = runCatching {
visaAuthRepository.getCustomerWalletAuthChallenge(
cardId = card.cardId,
walletPublicKey = extendedPublicKey.publicKey.toHexString(),
)
}.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
}
val signChallengeResult = signChallengeWithWallet(
publicKey = wallet.publicKey,
derivationPath = derivationPath,
nonce = challengeResponse.challenge,
session = session,
)
return when (signChallengeResult) {
is CompletionResult.Success -> {
Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
session = session,
signedChallenge = challengeResponse
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
)
}
is CompletionResult.Failure -> {
Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}")
CompletionResult.Failure(signChallengeResult.error)
}
}
}
private suspend fun handleWalletAuthorizationTokens(
session: CardSession,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
}.getOrElse {
Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization(session)
}
Timber.i("Authorized using Wallet public key successfully")
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
private suspend fun handleCardAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign")
val challengeResponse = runCatching {
visaAuthRepository.getCardAuthChallenge(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey.toHexString(),
)
}.getOrElse {
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}")
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
}
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge)
val attestCardKeyResponse = when (signChallengeResult) {
is CompletionResult.Success -> {
Timber.i("Challenged signed.")
signChallengeResult.data
}
is CompletionResult.Failure -> {
Timber.e(
"Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}",
)
return CompletionResult.Failure(signChallengeResult.error)
}
}
@Suppress("UnusedPrivateMember")
val authorizationTokensResponse = runCatching {
visaAuthRepository.getAccessTokens(
signedChallenge = challengeResponse.toSignedChallenge(
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
salt = attestCardKeyResponse.salt.toHexString(),
),
)
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(
TangemSdkError.Underlying(
customMessage = it.message ?: "Unknown error",
),
)
}
TODO() // implement card activation status handling ([REDACTED_TASK_KEY])
}
private suspend fun signChallengeWithWallet(
publicKey: ByteArray,
derivationPath: DerivationPath,
nonce: String,
session: CardSession,
): CompletionResult<SignHashResponse> {
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath)
val result = suspendCancellableCoroutine {
signHashCommand.run(session) { result ->
it.resume(result)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(result.data)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun signChallengeWithCard(
session: CardSession,
challenge: String,
): CompletionResult<AttestCardKeyResponse> {
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray())
val result = suspendCancellableCoroutine { continuation ->
signHashCommand.run(session) { result ->
continuation.resume(result)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(result.data)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.domain.visa
private const val COMMON_DESCRIPTION = "Error occurred. Please contact support."
internal enum class VisaCardScanHandlerError(val errorDescription: String) {
FailedToCreateDerivationPath(COMMON_DESCRIPTION),
FailedToFindWallet(COMMON_DESCRIPTION),
FailedToFindDerivedWalletKey(COMMON_DESCRIPTION),
}

View file

@ -8,6 +8,7 @@ import com.tangem.common.card.WalletData
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.operations.attestation.Attestation
@ -66,7 +67,7 @@ internal class TangemCardTypesResolver(
override fun getBlockchain(): Blockchain {
return when (productType) {
ProductType.Start2Coin -> if (card.isTestCard) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
ProductType.Visa -> Blockchain.PolygonTestnet
ProductType.Visa -> VisaUtilities.visaBlockchain
else -> {
val blockchainName: String = walletData?.blockchain
?: if (productType == ProductType.Note) {

View file

@ -2,6 +2,7 @@ package com.tangem.domain.common
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.CardDTO
import java.util.Locale
@ -23,7 +24,7 @@ object TapWorkarounds {
get() = isStart2CoinIssuer(issuer.name)
val CardDTO.isVisa: Boolean
get() = firmwareVersion.doubleValue in FirmwareVersion.visaRange
get() = VisaUtilities.isVisaCard(this)
val CardDTO.isTestCard: Boolean
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)

View file

@ -0,0 +1,29 @@
package com.tangem.domain.common.visa
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.models.scan.CardDTO
private const val VISA_BATCH_START = "AE"
object VisaUtilities {
val mandatoryCurve = EllipticCurve.Secp256k1
const val tokenId = "tether"
val visaBlockchain = Blockchain.Polygon
val visaDefaultDerivationPath = visaBlockchain.derivationPath(DerivationStyle.V3)
fun isVisaCard(card: CardDTO): Boolean {
return isVisaCard(card.firmwareVersion.doubleValue, card.batchId)
}
fun isVisaCard(firmwareVersion: Double, batchId: String): Boolean {
return FirmwareVersion.visaRange.contains(firmwareVersion) &&
batchId.startsWith(VISA_BATCH_START)
}
}

View file

@ -5,7 +5,10 @@ plugins {
}
dependencies {
api(projects.domain.visa.models)
implementation(deps.tangem.card.core)
implementation(deps.moshi.kotlin)
implementation(deps.moshi.adapters)
implementation(deps.kotlin.serialization)
}

View file

@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
import com.tangem.common.card.WalletData
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.domain.models.scan.serialization.ScanResponseAsStringSerializer
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import kotlinx.serialization.Serializable
@ -20,6 +21,7 @@ data class ScanResponse(
@Json(name = "productType") val productType: ProductType,
@Json(name = "walletData") val walletData: WalletData?,
@Json(name = "secondTwinPublicKey") val secondTwinPublicKey: String? = null,
@Json(name = "visaCardActivationStatus") val visaCardActivationStatus: VisaCardActivationStatus? = null,
@Json(name = "derivedKeys") val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
@Json(name = "primaryCard") val primaryCard: PrimaryCard? = null,
)

View file

@ -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.VisaCardActivationStatus
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@ -22,6 +23,7 @@ internal object ScanResponseAsStringSerializer : KSerializer<ScanResponse> {
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaCardActivationStatus.serializer)
.add(KotlinJsonAdapterFactory())
.build()

View file

@ -9,6 +9,8 @@ android {
}
dependencies {
/** Domain models */
api(projects.domain.visa.models)
/** Project - Domain */
implementation(projects.core.utils)

1
domain/visa/models/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,11 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
implementation(deps.moshi.kotlin)
implementation(deps.moshi.adapters)
implementation(deps.kotlin.serialization)
}

View file

@ -0,0 +1,33 @@
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 VisaActivationInput(
@Json(name = "cardId") val cardId: String,
@Json(name = "cardPublicKey") val cardPublicKey: ByteArray,
@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
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.visa.model
import kotlinx.serialization.Serializable
@Serializable
enum class VisaActivationRemoteState {
CardWalletSignatureRequired,
CustomerWalletSignatureRequired,
PaymentAccountDeploying,
WaitingPinCode,
WaitingForActivationFinishing,
Activated,
BlockedForActivation,
}

View file

@ -0,0 +1,31 @@
package com.tangem.domain.visa.model
sealed class VisaAuthChallenge {
abstract val session: VisaAuthSession
data class Card(
val challenge: String,
override val session: VisaAuthSession,
) : VisaAuthChallenge()
data class Wallet(
val challenge: String,
override val session: VisaAuthSession,
) : VisaAuthChallenge()
}
fun VisaAuthChallenge.Card.toSignedChallenge(signedChallenge: String, salt: String): VisaAuthSignedChallenge {
return VisaAuthSignedChallenge.ByCardPublicKey(
challenge = this,
signature = signedChallenge,
salt = salt,
)
}
fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String): VisaAuthSignedChallenge {
return VisaAuthSignedChallenge.ByWallet(
challenge = this,
signature = signedChallenge,
)
}

View file

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

View file

@ -0,0 +1,15 @@
package com.tangem.domain.visa.model
sealed class VisaAuthSignedChallenge {
data class ByCardPublicKey(
val challenge: VisaAuthChallenge.Card,
val signature: String,
val salt: String,
) : VisaAuthSignedChallenge()
data class ByWallet(
val challenge: VisaAuthChallenge.Wallet,
val signature: String,
) : VisaAuthSignedChallenge()
}

View file

@ -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 VisaAuthTokens(
@Json(name = "accessToken") val accessToken: String,
@Json(name = "refreshToken") val refreshToken: String,
)

View file

@ -0,0 +1,38 @@
package com.tangem.domain.visa.model
import com.squareup.moshi.Json
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import kotlinx.serialization.Serializable
@Serializable
sealed class VisaCardActivationStatus {
@Serializable
data class Activated(
@Json(name = "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,
) : VisaCardActivationStatus()
@Serializable
data class NotStartedActivation(
@Json(name = "activationInput") val activationInput: VisaActivationInput,
) : VisaCardActivationStatus()
@Serializable
data object Blocked : VisaCardActivationStatus()
companion object {
val serializer: 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)
}
}

View file

@ -232,6 +232,7 @@ include(":domain:transaction")
include(":domain:transaction:models")
include(":domain:analytics")
include(":domain:visa")
include(":domain:visa:models")
include(":domain:onboarding")
include(":domain:feedback")
include(":domain:qr-scanning")