Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-26 23:21:38 +07:00
commit c62f464b86
22 changed files with 70 additions and 48 deletions

View file

@ -20,6 +20,7 @@ internal class DefaultScanCardProcessor(
cardId: String?,
allowsRequestAccessCodeFromRepository: Boolean,
analyticsSource: AnalyticsParam.ScreensSources,
shouldCheckIsAlreadyActivated: Boolean,
): CompletionResult<ScanResponse> {
return if (isNewCardScanningEnabled) {
UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository)
@ -28,6 +29,7 @@ internal class DefaultScanCardProcessor(
analyticsSource = analyticsSource,
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
)
}
}
@ -35,6 +37,7 @@ internal class DefaultScanCardProcessor(
@Suppress("LongParameterList")
override suspend fun scan(
analyticsSource: AnalyticsParam.ScreensSources,
shouldCheckIsAlreadyActivated: Boolean,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
@ -56,6 +59,7 @@ internal class DefaultScanCardProcessor(
} else {
legacyScanProcessor.scan(
analyticsSource = analyticsSource,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
cardId = cardId,
onProgressStateChange = onProgressStateChange,
onWalletNotCreated = onWalletNotCreated,

View file

@ -55,10 +55,12 @@ internal class LegacyScanProcessor @Inject constructor(
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
analyticsSource: AnalyticsParam.ScreensSources,
shouldCheckIsAlreadyActivated: Boolean,
): CompletionResult<ScanResponse> {
return tangemSdkManager.scanProduct(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
)
.doOnFailure { error ->
onScanFailure(analyticsSource = analyticsSource, error = error, onFailure = {}, onCancel = {})
@ -68,6 +70,7 @@ internal class LegacyScanProcessor @Inject constructor(
@Suppress("LongParameterList")
suspend fun scan(
analyticsSource: AnalyticsParam.ScreensSources,
shouldCheckIsAlreadyActivated: Boolean,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
@ -80,7 +83,10 @@ internal class LegacyScanProcessor @Inject constructor(
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(cardId)
val result = tangemSdkManager.scanProduct(
cardId = cardId,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
)
val analyticsEvent = Basic.CardWasScanned(analyticsSource)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource))

View file

@ -13,11 +13,16 @@ internal class DefaultScanCardRepository(
private val exceptionConverter = ScanCardExceptionConverter()
override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse {
override suspend fun scanCard(
cardId: String?,
allowRequestAccessCodeFromStorage: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
): ScanResponse {
return when (
val result = tangemSdkManager.scanProduct(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
)
) {
is CompletionResult.Success -> result.data

View file

@ -163,6 +163,7 @@ internal class DefaultTangemSdkManager(
cardId: String?,
messageRes: Int?,
allowsRequestAccessCodeFromRepository: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
): CompletionResult<ScanResponse> {
val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header))
return coroutineScope {
@ -173,7 +174,7 @@ internal class DefaultTangemSdkManager(
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this,
shouldCheckIsAlreadyActivated = true,
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
),
cardId = cardId,

View file

@ -59,6 +59,7 @@ class MockTangemSdkManager(
cardId: String?,
messageRes: Int?,
allowsRequestAccessCodeFromRepository: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
): CompletionResult<ScanResponse> {
return MockProvider.getScanResponse()
}

View file

@ -296,7 +296,9 @@ internal class DefaultUserWalletsListRepository(
}
val scanResponse = unlockMethod.scanResponse ?: run {
when (val res = tangemSdkManagerProvider().scanProduct()) {
when (val res = tangemSdkManagerProvider().scanProduct(
shouldCheckIsAlreadyActivated = false,
)) {
is CompletionResult.Failure -> raise(UnlockWalletError.UserCancelled)
is CompletionResult.Success -> res.data
}

View file

@ -107,6 +107,7 @@ internal class CardSettingsModel @Inject constructor(
private fun scanCard() = modelScope.launch {
scanCardProcessor.scan(
analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Settings,
shouldCheckIsAlreadyActivated = false,
allowsRequestAccessCodeFromRepository = true,
)
.doOnSuccess { scanResponse ->

View file

@ -11,10 +11,12 @@ interface ScanCardProcessor {
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
analyticsSource: AnalyticsParam.ScreensSources,
shouldCheckIsAlreadyActivated: Boolean,
): CompletionResult<ScanResponse>
suspend fun scan(
analyticsSource: AnalyticsParam.ScreensSources,
shouldCheckIsAlreadyActivated: Boolean,
cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
onWalletNotCreated: suspend () -> Unit = {},

View file

@ -49,9 +49,14 @@ class ScanCardUseCase(
cardId: String?,
allowRequestAccessCodeFromStorage: Boolean,
): Either<ScanCardException, ScanResponse> {
return Either.catch { scanCardRepository.scanCard(cardId, allowRequestAccessCodeFromStorage) }
.mapLeft { e ->
e as? ScanCardException ?: ScanCardException.UnknownException(e)
}
return Either.catch {
scanCardRepository.scanCard(
cardId = cardId,
allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage,
shouldCheckIsAlreadyActivated = true, // TODO use correct when enable NEW_CARD_SCANNING_ENABLED
)
}.mapLeft { e ->
e as? ScanCardException ?: ScanCardException.UnknownException(e)
}
}
}

View file

@ -17,5 +17,9 @@ interface ScanCardRepository {
* @return a [ScanResponse] object with the result of the scan.
* @throws [ScanCardException] if the scan process fails.
*/
suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse
suspend fun scanCard(
cardId: String?,
allowRequestAccessCodeFromStorage: Boolean,
shouldCheckIsAlreadyActivated: Boolean,
): ScanResponse
}

View file

@ -186,6 +186,7 @@ internal class CreateWalletStartModel @Inject constructor(
scanCardProcessor.scan(
analyticsSource = analyticsSource,
shouldCheckIsAlreadyActivated = true,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)

View file

@ -120,6 +120,7 @@ internal class UserWalletSaver @Inject constructor(
scope.launch {
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Settings,
shouldCheckIsAlreadyActivated = true,
onWalletNotCreated = {
continuation.resume(Either.Right(null))
},

View file

@ -171,6 +171,7 @@ internal class HomeModel @Inject constructor(
scanCardProcessor.scan(
analyticsSource = analyticsSource,
shouldCheckIsAlreadyActivated = true,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)

View file

@ -104,6 +104,7 @@ internal class CreateHardwareWalletModel @Inject constructor(
scanCardProcessor.scan(
analyticsSource = analyticsSource,
shouldCheckIsAlreadyActivated = true,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)

View file

@ -115,7 +115,7 @@ internal class UpgradeWalletModel @Inject constructor(
)
tangemSdkManager
.scanProduct()
.scanProduct(shouldCheckIsAlreadyActivated = true)
.doOnSuccess { scanResponse ->
checkIsWalletSuitableToBeUsedAsUpgrade(scanResponse = scanResponse) {
delay(DELAY_SDK_DIALOG_CLOSE)

View file

@ -9,9 +9,7 @@ import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Approver
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
@ -25,7 +23,6 @@ import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency
@ -53,19 +50,16 @@ internal class DefaultSwapRepository(
private val tangemExpressApi: TangemExpressApi,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsListRepository: UserWalletsListRepository,
private val errorsDataConverter: ErrorsDataConverter,
private val dataSignatureVerifier: DataSignatureVerifier,
private val appPreferencesStore: AppPreferencesStore,
private val rampStateManager: RampStateManager,
moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
) : SwapRepository {
private val expressDataConverter = ExpressDataConverter()
private val leastTokenInfoConverter = LeastTokenInfoConverter()
private val swapPairInfoConverter = SwapPairInfoConverter()
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
private val exchangeStatusConverter = ExchangeStatusConverter()
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
@ -445,18 +439,6 @@ internal class DefaultSwapRepository(
)
}
override suspend fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return requireNotNull(
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = requireNotNull(userWalletsListRepository.selectedUserWalletSync()),
),
)
}
private fun getDataError(ex: Exception): ExpressDataError {
return if (ex is ApiResponseError.HttpException) {
errorsDataConverter.convert(ex.errorBody.orEmpty())

View file

@ -1,7 +1,6 @@
package com.tangem.feature.swap.di
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.express.TangemExpressApi
@ -11,7 +10,6 @@ import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.DefaultSwapRepository
@ -37,10 +35,8 @@ internal class SwapDataModule {
coroutineDispatcher: CoroutineDispatcherProvider,
dataSignature: DataSignatureVerifier,
walletManagerFacade: WalletManagersFacade,
userWalletsListRepository: UserWalletsListRepository,
errorsDataConverter: ErrorsDataConverter,
@NetworkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
appPreferencesStore: AppPreferencesStore,
rampStateManager: RampStateManager,
): SwapRepository {
@ -48,11 +44,9 @@ internal class SwapDataModule {
tangemExpressApi = tangemExpressApi,
coroutineDispatcher = coroutineDispatcher,
walletManagersFacade = walletManagerFacade,
userWalletsListRepository = userWalletsListRepository,
errorsDataConverter = errorsDataConverter,
dataSignatureVerifier = dataSignature,
moshi = moshi,
excludedBlockchains = excludedBlockchains,
appPreferencesStore = appPreferencesStore,
rampStateManager = rampStateManager,
)

View file

@ -6,6 +6,7 @@ import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.TransactionFeeExtended
@ -123,7 +124,7 @@ interface SwapInteractor {
isReverseFromTo: Boolean,
): AccountSwapCurrency?
suspend fun getNativeToken(networkId: String): CryptoCurrency
suspend fun getNativeToken(network: Network): CryptoCurrency
@Suppress("LongParameterList")
suspend fun storeSwapTransaction(

View file

@ -1355,8 +1355,13 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
}
override suspend fun getNativeToken(networkId: String): CryptoCurrency {
return repository.getNativeTokenForNetwork(networkId)
override suspend fun getNativeToken(network: Network): CryptoCurrency {
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
?.filterIsInstance<CryptoCurrency.Coin>()
?.firstOrNull { it.network.id == network.id && it.network.derivationPath == network.derivationPath }
?: error("Unable to create network coin with ID: ${network.id}")
}
private suspend fun isAllowedToSpend(
@ -1505,7 +1510,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
return quoteDataModel.fold(
ifRight = { quoteModel ->
val swapState = updateBalances(
networkId = networkId,
fromTokenStatus = fromToken,
fromAccount = fromAccount,
toTokenStatus = toToken,
@ -1753,10 +1757,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
else -> {
if (feeValue < amount.value) {
val nativeCoinDecimals = Blockchain.fromNetworkId(networkId)?.decimals()
?: error("Blockchain not found")
IncludeFeeInAmount.Included(
amountSubtractFee = SwapAmount(
reducedBalance - feeValue,
getNativeToken(fromToken.network.backendId).decimals,
nativeCoinDecimals,
),
)
} else {
@ -1773,7 +1779,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
val feeCurrencyId: CryptoCurrency.ID = when (feePaidCurrency) {
is FeePaidCurrency.Token -> feePaidCurrency.tokenId
else -> getNativeToken(networkId = fromToken.network.backendId).id
else -> getNativeToken(network = fromToken.network).id
}
val rates = getQuotes(feeCurrencyId)
return rates[feeCurrencyId]?.let { rate ->
@ -1869,7 +1875,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
includeFeeInAmount = includeFeeInAmount,
)
val swapState = updateBalances(
networkId = networkId,
fromTokenStatus = fromToken,
fromAccount = fromAccount,
toTokenStatus = toToken,
@ -2037,7 +2042,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
@Suppress("LongParameterList", "MaxChainedCallsOnSameLine")
private suspend fun updateBalances(
provider: SwapProvider,
networkId: String,
fromTokenStatus: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
toTokenStatus: CryptoCurrencyStatus,
@ -2049,7 +2053,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
): SwapState.QuotesLoadedState {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
val nativeToken = repository.getNativeTokenForNetwork(networkId)
val nativeToken = getNativeToken(fromToken.network)
val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id)
return SwapState.QuotesLoadedState(
fromTokenInfo = TokenSwapInfo(
@ -2478,7 +2482,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
if (balanceToCheck > fee.multiply(percentsToFeeIncrease)) {
SwapFeeState.Enough
} else {
val nativeToken = getNativeToken(fromTokenStatus.currency.network.backendId)
val nativeToken = getNativeToken(fromTokenStatus.currency.network)
SwapFeeState.NotEnough(
feeCurrency = nativeToken,
currencyName = nativeToken.network.name,
@ -2522,11 +2526,10 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
}
is FeePaidCurrency.FeeResource -> {
val network = repository.getNativeTokenForNetwork(networkId).network
val isFeeResourceEnough = currencyChecksRepository.checkIfFeeResourceEnough(
amount = spendAmount.value,
userWalletId = userWalletId,
network = network,
network = fromTokenStatus.currency.network,
)
if (isFeeResourceEnough) {

View file

@ -83,6 +83,4 @@ interface SwapRepository {
txHash: String,
payInExtraId: String?,
): Either<ExpressDataError, Unit>
suspend fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
}

View file

@ -192,6 +192,7 @@ internal class WelcomeModel @Inject constructor(
modelScope.launch {
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.SignIn,
shouldCheckIsAlreadyActivated = true,
onWalletNotCreated = {},
disclaimerWillShow = { router.pop() },
onSuccess = { scanResponse ->

View file

@ -45,10 +45,18 @@ interface TangemSdkManager {
suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean = true): Boolean
/**
* Scans the card and returns [ScanResponse] with the card information.
* If [cardId] is provided, it will try to find the card with this ID in the card reader's range.
* If [allowsRequestAccessCodeFromRepository] is true, it will try to request access code from the repository
* if the card is protected by access code and the access code is not saved in the repository.
* If [shouldCheckIsAlreadyActivated] is true, it will check if the card is already activated and return an error if it is.
*/
suspend fun scanProduct(
cardId: String? = null,
messageRes: Int? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
shouldCheckIsAlreadyActivated: Boolean,
): CompletionResult<ScanResponse>
suspend fun createProductWallet(