diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3886d9f58b..8cf12cc08e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index 4754c5ec8e..d943172df9 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -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, + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 164992b425..d7f1cd4f90 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -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,15 +126,19 @@ class DefaultTangemSdkManager( allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header)) - return runTaskAsyncReturnOnMain( - runnable = ScanProductTask( - card = null, - derivationsFinder = derivationsFinder, - allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, - ), - cardId = cardId, - initialMessage = message, - ).also { sendScanResultsToAnalytics(it) } + 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( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 2fcfc2411a..7044b12c7f 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -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 { @@ -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) -> 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( diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 4e3364feba..0c725f3fbb 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -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)) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 2885696de1..baff7e0289 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt new file mode 100644 index 0000000000..efea30664c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -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 { + 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 { + 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, + session: CardSession, + ): CompletionResult { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt new file mode 100644 index 0000000000..d507ac7b14 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt @@ -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), +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 85f6165a23..bcf606539a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -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) { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index 601ecc526b..b369611972 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -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) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt new file mode 100644 index 0000000000..32d4282b20 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt @@ -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) + } +} \ No newline at end of file diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index bf25c0f8b6..717c52e49c 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt index 6a3b017398..98954e2c5c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt @@ -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 = mapOf(), @Json(name = "primaryCard") val primaryCard: PrimaryCard? = null, ) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt index b3cb3d847e..3d8a7b4229 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt @@ -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 { .add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(VisaCardActivationStatus.serializer) .add(KotlinJsonAdapterFactory()) .build() diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index d5f66c4889..96fb032e84 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -9,6 +9,8 @@ android { } dependencies { + /** Domain models */ + api(projects.domain.visa.models) /** Project - Domain */ implementation(projects.core.utils) diff --git a/domain/visa/models/.gitignore b/domain/visa/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/visa/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/visa/models/build.gradle.kts b/domain/visa/models/build.gradle.kts new file mode 100644 index 0000000000..96a8bc4639 --- /dev/null +++ b/domain/visa/models/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt new file mode 100644 index 0000000000..5749e737d8 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationInput.kt @@ -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 + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt new file mode 100644 index 0000000000..3b64c35fde --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt @@ -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, +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt new file mode 100644 index 0000000000..536b7207dc --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt @@ -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, + ) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt new file mode 100644 index 0000000000..67437218f5 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.visa.model + +@JvmInline +value class VisaAuthSession(val sessionId: String) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt new file mode 100644 index 0000000000..a16916a887 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt @@ -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() +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt new file mode 100644 index 0000000000..0341e9ad8a --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -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, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt new file mode 100644 index 0000000000..40921403dd --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt @@ -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 + get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type") + .withSubtype(Activated::class.java, "Activated") + .withSubtype(ActivationStarted::class.java, "ActivationStarted") + .withSubtype(NotStartedActivation::class.java, "NotStartedActivation") + .withDefaultValue(Blocked) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d5fda70b8a..5e944733b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")