Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-29 21:44:25 +03:00
commit bf0cb06cb3
297 changed files with 4771 additions and 2376 deletions

View file

@ -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"
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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))
}
}
}
}
}

View file

@ -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,
)

View file

@ -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)
}
}
}
}

View file

@ -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,
}

View file

@ -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?,
)

View file

@ -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>,
)

View file

@ -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,
)
}

View file

@ -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

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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

View file

@ -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,
),
)
}

View file

@ -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),

View file

@ -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
) {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
}
}

View file

@ -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,

View file

@ -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
}
}

View file

@ -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?,
)

View file

@ -150,4 +150,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
VanarChainTestnet -> null
OdysseyChain, OdysseyChainTestnet -> null
Bitrock, BitrockTestnet -> null
Sonic, SonicTestnet -> null
ApeChain, ApeChainTestnet -> null
}