Updated on 2026-08-14
This commit is contained in:
commit
bf0cb06cb3
297 changed files with 4771 additions and 2376 deletions
|
|
@ -33,22 +33,24 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
|
|||
|
||||
private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java)
|
||||
|
||||
override suspend fun store(tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
|
||||
override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
|
||||
val json = tokensAdapter.toJson(tokens)
|
||||
|
||||
secureStorage.store(
|
||||
json.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
VISA_AUTH_TOKENS_KEY,
|
||||
createKey(cardId),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun get(): VisaAuthTokens? = withContext(dispatcherProvider.io) {
|
||||
secureStorage.get(VISA_AUTH_TOKENS_KEY)
|
||||
override suspend fun get(cardId: String): VisaAuthTokens? = withContext(dispatcherProvider.io) {
|
||||
secureStorage.get(createKey(cardId))
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(tokensAdapter::fromJson)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val VISA_AUTH_TOKENS_KEY = "visa_auth_tokens"
|
||||
override fun remove(cardId: String) {
|
||||
secureStorage.delete(createKey(cardId))
|
||||
}
|
||||
|
||||
private fun createKey(cardId: String): String = "visa_auth_tokens_$cardId"
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.onramp.*
|
||||
import com.tangem.domain.onramp.repositories.HotCryptoRepository
|
||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
||||
|
|
@ -229,4 +230,10 @@ internal object OnrampDomainModule {
|
|||
): FetchOnrampCountriesUseCase {
|
||||
return FetchOnrampCountriesUseCase(onrampRepository, onrampErrorResolver)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetHotCryptoTokensUseCase(hotCryptoRepository: HotCryptoRepository): GetHotCryptoUseCase {
|
||||
return GetHotCryptoUseCase(hotCryptoRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,12 +19,6 @@ internal object PromoDomainModule {
|
|||
return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowRingPromoUseCase(promoRepository: PromoRepository): ShouldShowRingPromoUseCase {
|
||||
return ShouldShowRingPromoUseCase(promoRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ 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.VisaCardActivationResponse
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.operations.ScanTask
|
||||
|
|
@ -35,16 +35,19 @@ 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.tap.derivationsFinder
|
||||
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
|
||||
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
|
||||
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
|
||||
import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
|
||||
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
|
|
@ -494,6 +497,17 @@ internal class DefaultTangemSdkManager(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun visaCustomerWalletApprove(
|
||||
visaDataForApprove: VisaDataForApprove,
|
||||
): CompletionResult<SignHashResponse> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
runnable = VisaCustomerWalletApproveTask(
|
||||
visaDataForApprove = visaDataForApprove,
|
||||
),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ 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.VisaCardActivationResponse
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
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.tap.domain.sdk.mocks.MockProvider
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
@ -207,4 +209,12 @@ class MockTangemSdkManager(
|
|||
): CompletionResult<VisaCardActivationResponse> {
|
||||
error("Not implemented")
|
||||
}
|
||||
|
||||
override suspend fun visaCustomerWalletApprove(
|
||||
visaDataForApprove: VisaDataForApprove,
|
||||
): CompletionResult<SignHashResponse> {
|
||||
error("Not implemented")
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -311,15 +311,12 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
|
||||
private fun getWalletProductType(card: CardDTO): ProductType {
|
||||
if (RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX)) {
|
||||
return ProductType.Ring
|
||||
}
|
||||
return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
|
||||
card.settings.isKeysImportAllowed
|
||||
) {
|
||||
ProductType.Wallet2
|
||||
} else {
|
||||
ProductType.Wallet
|
||||
return when {
|
||||
card.isVisa -> ProductType.Visa
|
||||
RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX) -> ProductType.Ring
|
||||
card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
|
||||
card.settings.isKeysImportAllowed -> ProductType.Wallet2
|
||||
else -> ProductType.Wallet
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
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.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.map
|
||||
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.hasSavedOTP
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
|
|
@ -20,12 +24,15 @@ 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 com.tangem.sdk.api.visa.VisaCardActivationResponse
|
||||
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(
|
||||
|
|
@ -36,9 +43,15 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
private val otpStorage: VisaOTPStorage,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaActivationRepository: VisaActivationRepository,
|
||||
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
|
||||
) : CardSessionRunnable<VisaCardActivationResponse> {
|
||||
|
||||
private class SessionContext(
|
||||
val visaActivationRepository: VisaActivationRepository,
|
||||
val cardId: String,
|
||||
val session: CardSession,
|
||||
)
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) {
|
||||
coroutineScope.launch {
|
||||
callback(runSuspend(session))
|
||||
|
|
@ -52,33 +65,47 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
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 ?: ""))
|
||||
}
|
||||
val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId)
|
||||
|
||||
signOrder(session, activationOrder)
|
||||
}
|
||||
}
|
||||
val context = SessionContext(
|
||||
visaActivationRepository = visaActivationRepository,
|
||||
cardId = card.cardId,
|
||||
session = session,
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
Timber.i("VisaCardActivationTask all time: ${timedResult.duration}")
|
||||
return timedResult.value
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
private suspend fun SessionContext.signAuthorizationChallenge(
|
||||
challengeToSign: VisaAuthChallenge.Card,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
val attestationCommand = AttestCardKeyCommand(challenge = CryptoUtils.generateRandomBytes(length = 16))
|
||||
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
attestationCommand.run(session = session) { attestationResponse ->
|
||||
continuation.resume(attestationResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.i("AttestCardKeyCommand time: ${timedResult.duration}")
|
||||
|
||||
return when (val result = timedResult.value) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("AttestCardKeyCommand success")
|
||||
processSignedAuthorizationChallenge(
|
||||
session = session,
|
||||
signedChallenge = challengeToSign.toSignedChallenge(
|
||||
signedChallenge = result.data.cardSignature.toHexString(),
|
||||
salt = result.data.salt.toHexString(),
|
||||
|
|
@ -86,13 +113,13 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e("AttestCardKeyCommand failure ${result.error}")
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processSignedAuthorizationChallenge(
|
||||
session: CardSession,
|
||||
private suspend fun SessionContext.processSignedAuthorizationChallenge(
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
return coroutineScope {
|
||||
|
|
@ -107,12 +134,14 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
|
||||
otpTaskDeferred.await()
|
||||
|
||||
signOrder(session, order)
|
||||
signOrder(order)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(TangemSdkError::class)
|
||||
private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder {
|
||||
private suspend fun SessionContext.getActivationOrderToSign(
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): ActivationOrder {
|
||||
val tokens = runCatching {
|
||||
visaAuthRepository.getAccessTokens(signedChallenge)
|
||||
}.getOrElse {
|
||||
|
|
@ -121,12 +150,12 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
visaAuthTokenStorage.store(tokens)
|
||||
visaAuthTokenStorage.store(cardId, tokens)
|
||||
|
||||
return visaActivationRepository.getActivationOrderToSign()
|
||||
}
|
||||
|
||||
private suspend fun createWallet(session: CardSession): CompletionResult<Unit> {
|
||||
private suspend fun SessionContext.createWallet(session: CardSession): CompletionResult<Unit> {
|
||||
coroutineScope { ensureActive() }
|
||||
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
|
@ -135,122 +164,152 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
createOTP(session)
|
||||
} else {
|
||||
val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve)
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
createWalletTask.run(session) { createWalletResult ->
|
||||
continuation.resume(createWalletResult)
|
||||
|
||||
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
createWalletTask.run(session) { createWalletResult ->
|
||||
continuation.resume(createWalletResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
Timber.i("CreateWalletTask time: ${timedResult.duration}")
|
||||
|
||||
when (val result = timedResult.value) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("CreateWalletTask success")
|
||||
createOTP(session)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e("CreateWalletTask failure ${result.error}")
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createOTP(session: CardSession): CompletionResult<Unit> {
|
||||
private suspend fun SessionContext.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) {
|
||||
return if (otpStorage.hasSavedOTP(cardId)) {
|
||||
CompletionResult.Success(Unit)
|
||||
} else {
|
||||
val otpCommand = GenerateOTPCommand()
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
otpCommand.run(session) { otpResult ->
|
||||
continuation.resume(otpResult)
|
||||
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
otpCommand.run(session) { otpResult ->
|
||||
continuation.resume(otpResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
Timber.i("GenerateOTPCommand time: ${timedResult.duration}")
|
||||
|
||||
when (val result = timedResult.value) {
|
||||
is CompletionResult.Success -> {
|
||||
otpStorage.saveOTP(card.cardId, result.data.rootOTP)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signOrder(
|
||||
session: CardSession,
|
||||
order: ActivationOrder,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
private suspend fun SessionContext.signOrder(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)
|
||||
card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve }
|
||||
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val task = SignHashCommand(
|
||||
hash = order.hash.hexToBytes(),
|
||||
walletPublicKey = wallet.publicKey,
|
||||
derivationPath = VisaUtilities.visaDefaultDerivationPath,
|
||||
)
|
||||
|
||||
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
task.run(session) { signResult ->
|
||||
continuation.resume(signResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
Timber.i("SignHashCommand time: ${timedResult.duration}")
|
||||
|
||||
return when (val result = timedResult.value) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("SignHashCommand success")
|
||||
handleSignedOrder(
|
||||
session = session,
|
||||
activationOrder = order,
|
||||
response = result.data,
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e("SignHashCommand failure ${result.error}")
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSignedOrder(
|
||||
session: CardSession,
|
||||
private suspend fun SessionContext.handleSignedOrder(
|
||||
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(
|
||||
val otp = otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message),
|
||||
)
|
||||
|
||||
val activationResponse = VisaCardActivationResponse(
|
||||
signedActivationOrder = signedOrder,
|
||||
rootOTP = VisaRootOTP(otp.toHexString()),
|
||||
)
|
||||
return setupAccessCode().map {
|
||||
val card =
|
||||
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
return setupAccessCode(session).map { activationResponse }
|
||||
VisaCardActivationResponse(
|
||||
signedActivationOrder = signedOrder,
|
||||
rootOTP = VisaRootOTP(otp.toHexString()),
|
||||
newCardDTO = CardDTO(card),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setupAccessCode(session: CardSession): CompletionResult<Unit> {
|
||||
private suspend fun SessionContext.setupAccessCode(): CompletionResult<Unit> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
if (card.isAccessCodeSet) {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
Timber.i("Setting access code")
|
||||
|
||||
val task = SetUserCodeCommand.changeAccessCode(accessCode)
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
task.run(session) { setAccessCodeResult ->
|
||||
continuation.resume(setAccessCodeResult)
|
||||
|
||||
val timedResult = RealtimeMonotonicTimeSource.measureTimedValue {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
task.run(session) { setAccessCodeResult ->
|
||||
continuation.resume(setAccessCodeResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
Timber.i("SetUserCodeCommand time: ${timedResult.duration}")
|
||||
|
||||
return when (val result = timedResult.value) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("SetUserCodeCommand success")
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.i("SetUserCodeCommand failure ${result.error}")
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
package com.tangem.tap.domain.tasks.visa
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.CardWallet
|
||||
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.hexToBytes
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
|
||||
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.visa.model.VisaActivationError
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
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> {
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<SignHashResponse>) {
|
||||
val card = session.environment.card ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
|
||||
return
|
||||
}
|
||||
|
||||
if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve")))
|
||||
return
|
||||
}
|
||||
|
||||
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) {
|
||||
callback(
|
||||
CompletionResult.Failure(
|
||||
TangemSdkError.Underlying("Use tangem wallet specified during visa registration"),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (card.settings.isHDWalletAllowed) {
|
||||
proceedApprove(card, session, callback)
|
||||
} else {
|
||||
proceedApproveWithLegacyCard(card, session, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedApprove(card: Card, session: CardSession, callback: CompletionCallback<SignHashResponse>) {
|
||||
val cardDTO = CardDTO(card)
|
||||
|
||||
val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run {
|
||||
proceedApproveWithLegacyCard(
|
||||
card = card,
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run {
|
||||
callback(
|
||||
CompletionResult.Failure(
|
||||
TangemSdkError.Underlying("Failed to generate derivation path with provided derivation style"),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message)))
|
||||
return
|
||||
}
|
||||
|
||||
val derivationTask = DeriveWalletPublicKeyTask(
|
||||
walletPublicKey = wallet.publicKey,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
derivationTask.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
processDerivedKey(
|
||||
wallet = wallet,
|
||||
extendedPublicKey = result.data,
|
||||
derivationPath = derivationPath,
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processDerivedKey(
|
||||
wallet: CardWallet,
|
||||
extendedPublicKey: ExtendedPublicKey,
|
||||
derivationPath: DerivationPath,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<SignHashResponse>,
|
||||
) {
|
||||
val validationResult = VisaWalletPublicKeyUtility.validateExtendedPublicKey(
|
||||
targetAddress = visaDataForApprove.targetAddress,
|
||||
extendedPublicKey = extendedPublicKey,
|
||||
)
|
||||
|
||||
validationResult.onLeft {
|
||||
callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message)))
|
||||
return
|
||||
}
|
||||
|
||||
signApproveData(
|
||||
targetWalletPublicKey = wallet.publicKey,
|
||||
derivationPath = derivationPath,
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
||||
private fun proceedApproveWithLegacyCard(
|
||||
card: Card,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<SignHashResponse>,
|
||||
) {
|
||||
val publicKey = findKeyWithoutDerivation(
|
||||
targetAddress = visaDataForApprove.targetAddress,
|
||||
card = CardDTO(card),
|
||||
).getOrElse {
|
||||
callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message)))
|
||||
return
|
||||
}
|
||||
|
||||
signApproveData(
|
||||
targetWalletPublicKey = publicKey,
|
||||
derivationPath = null,
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
||||
private fun signApproveData(
|
||||
targetWalletPublicKey: ByteArray,
|
||||
derivationPath: DerivationPath?,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<SignHashResponse>,
|
||||
) {
|
||||
val signTask = SignHashCommand(
|
||||
hash = visaDataForApprove.approveHash.hexToBytes(),
|
||||
walletPublicKey = targetWalletPublicKey,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
signTask.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
scanCard(
|
||||
signHashResponse = result.data,
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanCard(
|
||||
signHashResponse: SignHashResponse,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<SignHashResponse>,
|
||||
) {
|
||||
val scanTask = ScanTask()
|
||||
scanTask.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
callback(CompletionResult.Success(signHashResponse))
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.domain.userWalletList.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -7,16 +8,24 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletSensitiveInformation(
|
||||
@Json(name = "wallets")
|
||||
val wallets: List<CardDTO.Wallet>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletPublicInformation(
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "walletId")
|
||||
val walletId: UserWalletId,
|
||||
@Json(name = "artworkUrl")
|
||||
val artworkUrl: String,
|
||||
@Json(name = "cardsInWallet")
|
||||
val cardsInWallet: Set<String>,
|
||||
@Json(name = "scanResponse")
|
||||
val scanResponse: ScanResponse,
|
||||
@Json(name = "isMultiCurrency")
|
||||
val isMultiCurrency: Boolean,
|
||||
@Json(name = "hasBackupError")
|
||||
val hasBackupError: Boolean = false,
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ 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.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
|
|
@ -24,10 +25,16 @@ import kotlin.coroutines.resume
|
|||
|
||||
internal class VisaCardScanHandler @Inject constructor(
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaActivationRepository: VisaActivationRepository,
|
||||
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
) {
|
||||
|
||||
private class SessionContext(
|
||||
val visaActivationRepository: VisaActivationRepository,
|
||||
val cardId: String,
|
||||
val session: CardSession,
|
||||
)
|
||||
|
||||
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
Timber.i("Attempting to handle Visa card scan")
|
||||
|
||||
|
|
@ -36,6 +43,14 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
}
|
||||
|
||||
val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId)
|
||||
|
||||
val context = SessionContext(
|
||||
visaActivationRepository = visaActivationRepository,
|
||||
cardId = card.cardId,
|
||||
session = session,
|
||||
)
|
||||
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
|
||||
val activationInput =
|
||||
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
|
||||
|
|
@ -43,13 +58,10 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
return CompletionResult.Success(activationStatus)
|
||||
}
|
||||
|
||||
return deriveKey(wallet, session)
|
||||
return context.deriveKey(wallet)
|
||||
}
|
||||
|
||||
private suspend fun deriveKey(
|
||||
wallet: CardWallet,
|
||||
session: CardSession,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult<VisaCardActivationStatus> {
|
||||
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
|
||||
Timber.e("Failed to create derivation path while first scan")
|
||||
|
||||
|
|
@ -64,17 +76,16 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
return handleDerivationResponse(derivationTaskResult, session)
|
||||
return handleDerivationResponse(derivationTaskResult)
|
||||
}
|
||||
|
||||
private suspend fun handleDerivationResponse(
|
||||
private suspend fun SessionContext.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)
|
||||
handleWalletAuthorization()
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
|
|
@ -82,9 +93,8 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
private suspend fun SessionContext.handleWalletAuthorization(): 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")
|
||||
|
|
@ -93,6 +103,8 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
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(
|
||||
|
|
@ -108,7 +120,6 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
}
|
||||
|
||||
Timber.i("Requesting challenge for wallet authorization")
|
||||
// Will be changed later after backend implementation
|
||||
val challengeResponse = runCatching {
|
||||
visaAuthRepository.getCustomerWalletAuthChallenge(
|
||||
cardId = card.cardId,
|
||||
|
|
@ -122,14 +133,12 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
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()),
|
||||
)
|
||||
|
|
@ -141,8 +150,7 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun handleWalletAuthorizationTokens(
|
||||
session: CardSession,
|
||||
private suspend fun SessionContext.handleWalletAuthorizationTokens(
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
val authorizationTokensResponse = runCatching {
|
||||
|
|
@ -151,17 +159,20 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
Timber.i(
|
||||
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
|
||||
)
|
||||
return handleCardAuthorization(session)
|
||||
return handleCardAuthorization()
|
||||
}
|
||||
|
||||
visaAuthTokenStorage.store(authorizationTokensResponse)
|
||||
visaAuthTokenStorage.store(
|
||||
cardId = cardId,
|
||||
tokens = authorizationTokensResponse,
|
||||
)
|
||||
|
||||
Timber.i("Authorized using Wallet public key successfully")
|
||||
|
||||
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
|
||||
}
|
||||
|
||||
private suspend fun handleCardAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
private suspend fun SessionContext.handleCardAuthorization(): CompletionResult<VisaCardActivationStatus> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
Timber.i("Requesting authorization challenge to sign")
|
||||
|
|
@ -178,7 +189,7 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
|
||||
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
|
||||
|
||||
val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge)
|
||||
val signChallengeResult = signChallengeWithCard(challenge = challengeResponse.challenge)
|
||||
|
||||
val attestCardKeyResponse = when (signChallengeResult) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
@ -210,7 +221,10 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
visaAuthTokenStorage.store(authorizationTokensResponse)
|
||||
visaAuthTokenStorage.store(
|
||||
cardId = card.cardId,
|
||||
tokens = authorizationTokensResponse,
|
||||
)
|
||||
|
||||
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
|
||||
|
||||
|
|
@ -239,47 +253,31 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun signChallengeWithWallet(
|
||||
private suspend fun SessionContext.signChallengeWithWallet(
|
||||
publicKey: ByteArray,
|
||||
derivationPath: DerivationPath,
|
||||
nonce: String,
|
||||
session: CardSession,
|
||||
): CompletionResult<SignHashResponse> {
|
||||
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath)
|
||||
val result = suspendCancellableCoroutine {
|
||||
val signHashCommand = SignHashCommand(
|
||||
hash = nonce.hexToBytes(),
|
||||
walletPublicKey = publicKey,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
return 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,
|
||||
private suspend fun SessionContext.signChallengeWithCard(
|
||||
challenge: String,
|
||||
): CompletionResult<AttestCardKeyResponse> {
|
||||
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray())
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
val signHashCommand = AttestCardKeyCommand(challenge = challenge.hexToBytes())
|
||||
return 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,8 @@ data class WcSignMessage(
|
|||
@Json(name = "type")
|
||||
val type: WCSignType,
|
||||
) : WcRequestData {
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class WCSignType {
|
||||
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,16 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
data class Account(val chainId: String, val walletAddress: String, val derivationPath: String?)
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Account(
|
||||
@Json(name = "chainId")
|
||||
val chainId: String,
|
||||
|
||||
@Json(name = "walletAddress")
|
||||
val walletAddress: String,
|
||||
|
||||
@Json(name = "derivationPath")
|
||||
val derivationPath: String?,
|
||||
)
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Session(
|
||||
@Json(name = "topic")
|
||||
val topic: String,
|
||||
|
||||
@Json(name = "accounts")
|
||||
val accounts: List<Account>,
|
||||
)
|
||||
|
|
@ -2,29 +2,34 @@ package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class WcBinanceCancelOrder(
|
||||
data class WcBinanceCancelOrder(
|
||||
@Json(name = "account_number")
|
||||
accountNumber: String,
|
||||
val accountNumber: String,
|
||||
@Json(name = "chain_id")
|
||||
chainId: String,
|
||||
val chainId: String,
|
||||
@Json(name = "data")
|
||||
data: String?,
|
||||
val data: String?,
|
||||
@Json(name = "memo")
|
||||
memo: String?,
|
||||
val memo: String?,
|
||||
@Json(name = "sequence")
|
||||
sequence: String,
|
||||
val sequence: String,
|
||||
@Json(name = "source")
|
||||
source: String,
|
||||
val source: String,
|
||||
@Json(name = "msgs")
|
||||
msgs: List<Message>,
|
||||
) : WcBinanceOrder<WcBinanceCancelOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
|
||||
val msgs: List<Message>,
|
||||
) : WcRequestData {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Message(
|
||||
@Json(name = "refid")
|
||||
val refid: String,
|
||||
@Json(name = "sender")
|
||||
val sender: String,
|
||||
@Json(name = "symbol")
|
||||
val symbol: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
||||
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
open class WcBinanceOrder<T>(
|
||||
val accountNumber: String,
|
||||
val chainId: String,
|
||||
val data: String?,
|
||||
val memo: String?,
|
||||
val sequence: String,
|
||||
val source: String,
|
||||
val msgs: List<T>,
|
||||
) : WcRequestData
|
||||
|
||||
data class WcBinanceTxConfirmParam(
|
||||
val ok: Boolean,
|
||||
val errorMsg: String?,
|
||||
) : WcRequestData
|
||||
|
|
@ -4,21 +4,28 @@ import com.github.salomonbrys.kotson.jsonSerializer
|
|||
import com.google.gson.JsonObject
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class WcBinanceTradeOrder(
|
||||
data class WcBinanceTradeOrder(
|
||||
@Json(name = "account_number")
|
||||
accountNumber: String,
|
||||
val accountNumber: String,
|
||||
@Json(name = "chain_id")
|
||||
chainId: String,
|
||||
data: String?,
|
||||
memo: String?,
|
||||
sequence: String,
|
||||
source: String,
|
||||
msgs: List<Message>,
|
||||
) : WcBinanceOrder<WcBinanceTradeOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
|
||||
val chainId: String,
|
||||
@Json(name = "data")
|
||||
val data: String?,
|
||||
@Json(name = "memo")
|
||||
val memo: String?,
|
||||
@Json(name = "sequence")
|
||||
val sequence: String,
|
||||
@Json(name = "source")
|
||||
val source: String,
|
||||
@Json(name = "msgs")
|
||||
val msgs: List<Message>,
|
||||
) : WcRequestData {
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class MessageKey(val key: String) {
|
||||
ID("id"),
|
||||
ORDER_TYPE("ordertype"),
|
||||
|
|
@ -30,14 +37,23 @@ class WcBinanceTradeOrder(
|
|||
TIME_INFORCE("timeinforce"),
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Message(
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
@Json(name = "orderType")
|
||||
val orderType: Int,
|
||||
@Json(name = "price")
|
||||
val price: Long,
|
||||
@Json(name = "quantity")
|
||||
val quantity: Long,
|
||||
@Json(name = "sender")
|
||||
val sender: String,
|
||||
@Json(name = "side")
|
||||
val side: Int,
|
||||
@Json(name = "symbol")
|
||||
val symbol: String,
|
||||
@Json(name = "timeInforce")
|
||||
val timeInforce: Int,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,48 @@ package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class WcBinanceTransferOrder(
|
||||
data class WcBinanceTransferOrder(
|
||||
@Json(name = "account_number")
|
||||
accountNumber: String,
|
||||
val accountNumber: String,
|
||||
@Json(name = "chain_id")
|
||||
chainId: String,
|
||||
data: String?,
|
||||
memo: String?,
|
||||
sequence: String,
|
||||
source: String,
|
||||
msgs: List<Message>,
|
||||
) : WcBinanceOrder<WcBinanceTransferOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
|
||||
val chainId: String,
|
||||
@Json(name = "data")
|
||||
val data: String?,
|
||||
@Json(name = "memo")
|
||||
val memo: String?,
|
||||
@Json(name = "sequence")
|
||||
val sequence: String,
|
||||
@Json(name = "source")
|
||||
val source: String,
|
||||
@Json(name = "msgs")
|
||||
val msgs: List<Message>,
|
||||
) : WcRequestData {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Message(
|
||||
@Json(name = "inputs")
|
||||
val inputs: List<Item>,
|
||||
@Json(name = "outputs")
|
||||
val outputs: List<Item>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Item(
|
||||
@Json(name = "address")
|
||||
val address: String,
|
||||
@Json(name = "coins")
|
||||
val coins: List<Coin>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Coin(
|
||||
@Json(name = "amount")
|
||||
val amount: Long,
|
||||
@Json(name = "denom")
|
||||
val denom: String,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models.binance
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WcBinanceTxConfirmParam(
|
||||
@Json(name = "ok")
|
||||
val ok: Boolean,
|
||||
@Json(name = "errorMsg")
|
||||
val errorMsg: String?,
|
||||
) : WcRequestData
|
||||
|
|
@ -4,7 +4,7 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -27,13 +27,12 @@ internal fun SettingsScreensScaffold(
|
|||
addBottomInsets: Boolean = true,
|
||||
fab: @Composable () -> Unit = {},
|
||||
) {
|
||||
val state = rememberScaffoldState(snackbarHostState = snackbarHostState)
|
||||
val backgroundColor = TangemTheme.colors.background.secondary
|
||||
|
||||
BackHandler(onBack = onBackClick)
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = state,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
topBar = {
|
||||
EmptyTopBarWithNavigation(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
|
|
@ -43,7 +42,7 @@ internal fun SettingsScreensScaffold(
|
|||
},
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
backgroundColor = backgroundColor,
|
||||
containerColor = backgroundColor,
|
||||
floatingActionButton = {
|
||||
Box(modifier = Modifier.navigationBarsPadding()) {
|
||||
fab()
|
||||
|
|
@ -89,6 +88,7 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun EmptyTopBarWithNavigation(
|
||||
onBackClick: () -> Unit,
|
||||
|
|
@ -108,8 +108,9 @@ internal fun EmptyTopBarWithNavigation(
|
|||
)
|
||||
}
|
||||
},
|
||||
backgroundColor = backgroundColor,
|
||||
elevation = 0.dp,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = backgroundColor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -15,6 +18,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
|
|
@ -71,7 +75,7 @@ private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifie
|
|||
@Composable
|
||||
private fun EmptyScreen(state: WalletConnectScreenState) {
|
||||
if (state.isLoading) {
|
||||
LinearProgressIndicator(
|
||||
TangemLinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
|
|
@ -102,7 +106,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
|
|||
@Composable
|
||||
private fun WalletConnectSessions(state: WalletConnectScreenState) {
|
||||
if (state.isLoading) {
|
||||
LinearProgressIndicator(
|
||||
TangemLinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(2.dp),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.common.util.twinsIsTwinned
|
|||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
|
@ -42,6 +43,12 @@ object OnboardingHelper {
|
|||
store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response)
|
||||
val cardId = response.card.cardId
|
||||
return when {
|
||||
response.cardTypesResolver.isVisaWallet() -> {
|
||||
if (response.visaCardActivationStatus == null) error("Visa card activation status is null")
|
||||
|
||||
response.visaCardActivationStatus !is VisaCardActivationStatus.Activated
|
||||
}
|
||||
|
||||
response.cardTypesResolver.isTangemTwins() -> {
|
||||
if (!response.twinsIsTwinned()) {
|
||||
true
|
||||
|
|
@ -66,7 +73,8 @@ object OnboardingHelper {
|
|||
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
|
||||
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
|
||||
scanResponse.productType == ProductType.Ring ||
|
||||
scanResponse.productType == ProductType.Wallet // AppRoute.OnboardingOther is also supported
|
||||
scanResponse.productType == ProductType.Wallet ||
|
||||
scanResponse.productType == ProductType.Visa // AppRoute.OnboardingOther is also supported
|
||||
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled &&
|
||||
newOnboardingSupportTypes
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import android.content.DialogInterface
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultVisaAuthProvider @Inject constructor(
|
||||
private val authStorage: VisaAuthTokenStorage,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
) : TangemVisaAuthProvider {
|
||||
|
||||
override suspend fun getAuthHeader(): String {
|
||||
return authStorage.get()?.accessToken?.let { "Bearer $it" } ?: "Error in the app!"
|
||||
override suspend fun getAuthHeader(cardId: String): String {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.network.exchangeServices.mercuryo
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
|
|
@ -11,21 +12,25 @@ interface MercuryoApi {
|
|||
suspend fun currencies(@Path("apiVersion") apiVersion: String): MercuryoCurrenciesResponse
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MercuryoCurrenciesResponse(
|
||||
val status: Int,
|
||||
val data: Data,
|
||||
@Json(name = "status") val status: Int,
|
||||
@Json(name = "data") val data: Data,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
val fiat: List<String>,
|
||||
val crypto: List<String>,
|
||||
val config: Config,
|
||||
@Json(name = "fiat") val fiat: List<String>,
|
||||
@Json(name = "crypto") val crypto: List<String>,
|
||||
@Json(name = "config") val config: Config,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Config(
|
||||
@Json(name = "crypto_currencies")
|
||||
val cryptoCurrencies: List<MercuryoCryptoCurrency>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MercuryoCryptoCurrency(
|
||||
@Json(name = "currency")
|
||||
val currencySymbol: String,
|
||||
|
|
|
|||
|
|
@ -149,5 +149,7 @@ internal val Blockchain.mercuryoNetwork: String?
|
|||
Blockchain.Clore -> null
|
||||
Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> null
|
||||
Blockchain.Bitrock, Blockchain.BitrockTestnet -> null
|
||||
Blockchain.Sonic, Blockchain.SonicTestnet -> null
|
||||
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,9 @@ interface MoonPayApi {
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayUserStatus(
|
||||
@Json(name = "isBuyAllowed")
|
||||
val isBuyAllowed: Boolean,
|
||||
@Json(name = "isSellAllowed")
|
||||
val isSellAllowed: Boolean,
|
||||
@Json(name = "isAllowed")
|
||||
val isMoonpayAllowed: Boolean,
|
||||
|
|
@ -34,18 +36,18 @@ data class MoonPayUserStatus(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayCurrencies(
|
||||
val type: String,
|
||||
val code: String,
|
||||
val supportsLiveMode: Boolean = false,
|
||||
val isSuspended: Boolean = true,
|
||||
val isSupportedInUS: Boolean = false,
|
||||
val isSellSupported: Boolean = false,
|
||||
val notAllowedUSStates: List<String> = emptyList(),
|
||||
val metadata: MoonPayCurrenciesMetadata? = null,
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "code") val code: String,
|
||||
@Json(name = "supportsLiveMode") val supportsLiveMode: Boolean = false,
|
||||
@Json(name = "isSuspended") val isSuspended: Boolean = true,
|
||||
@Json(name = "isSupportedInUS") val isSupportedInUS: Boolean = false,
|
||||
@Json(name = "isSellSupported") val isSellSupported: Boolean = false,
|
||||
@Json(name = "notAllowedUSStates") val notAllowedUSStates: List<String> = emptyList(),
|
||||
@Json(name = "metadata") val metadata: MoonPayCurrenciesMetadata? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayCurrenciesMetadata(
|
||||
val contractAddress: String?,
|
||||
val networkCode: String?,
|
||||
@Json(name = "contractAddress") val contractAddress: String?,
|
||||
@Json(name = "networkCode") val networkCode: String?,
|
||||
)
|
||||
|
|
@ -150,4 +150,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
|
|||
VanarChainTestnet -> null
|
||||
OdysseyChain, OdysseyChainTestnet -> null
|
||||
Bitrock, BitrockTestnet -> null
|
||||
Sonic, SonicTestnet -> null
|
||||
ApeChain, ApeChainTestnet -> null
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue