Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-09 12:15:55 +03:00
commit 25610a2321
26 changed files with 281 additions and 99 deletions

View file

@ -279,6 +279,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
onboardingRepository = onboardingRepository,
excludedBlockchains = excludedBlockchains,
appPreferencesStore = appPreferencesStore,
),
),
)

View file

@ -170,6 +170,12 @@ class DialogManager : StoreSubscriber<GlobalState> {
),
context = context,
)
is AppDialog.WalletAlreadyWasUsedDialog -> WalletAlreadyWasUsedDialog.create(
context = context,
onOk = state.dialog.onOk,
onSupport = state.dialog.onSupportClick,
onCancel = state.dialog.onCancel,
)
is AppDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencyTitle),
messageRes = state.dialog.messageRes,

View file

@ -48,4 +48,10 @@ sealed class AppDialog : StateDialog {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title
}
data class WalletAlreadyWasUsedDialog(
val onOk: () -> Unit,
val onSupportClick: () -> Unit,
val onCancel: () -> Unit,
) : AppDialog()
}

View file

@ -32,6 +32,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onCancel: suspend () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) {
@ -52,6 +53,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
onProgressStateChange,
onWalletNotCreated,
disclaimerWillShow,
onCancel,
onFailure,
onSuccess,
)

View file

@ -9,18 +9,25 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -50,6 +57,7 @@ internal object LegacyScanProcessor {
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onCancel: suspend () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = withMainContext {
@ -81,6 +89,7 @@ internal object LegacyScanProcessor {
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
onCancel = onCancel,
)
},
)
@ -131,36 +140,89 @@ internal object LegacyScanProcessor {
scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline onWalletNotCreated: suspend () -> Unit,
crossinline onCancel: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
) {
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
onWalletNotCreated()
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
scanResponse = scanResponse,
source = BackupStartedSource.Onboarding,
canSkipBackup = scanResponse.card.canSkipBackup,
checkCardWasUsedInApp(
scanResponse = scanResponse,
onCancel = {
mainScope.launch {
onProgressStateChange.invoke(false)
onCancel()
}
},
) {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
onWalletNotCreated()
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
scanResponse = scanResponse,
source = BackupStartedSource.Onboarding,
canSkipBackup = scanResponse.card.canSkipBackup,
),
)
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
}
}
}
}
/**
* Checks if card has password and never login at this app
* Show alert in this case
*/
private suspend fun checkCardWasUsedInApp(
scanResponse: ScanResponse,
onCancel: () -> Unit,
onSuccess: suspend () -> Unit,
) {
val userWalletId = runCatching { UserWalletIdBuilder.card(scanResponse.card).build() }.getOrNull()
if (userWalletId == null) {
onSuccess()
return
}
val appPrefStoreStore = store.inject(DaggerGraphState::appPreferencesStore)
val tokens = appPrefStoreStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
if (scanResponse.card.isAccessCodeSet && tokens == null) {
store.dispatchDialogShow(
AppDialog.WalletAlreadyWasUsedDialog(
onOk = { mainScope.launch { onSuccess() } },
onSupportClick = {
val cardInfo =
store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.PreActivatedWallet(cardInfo))
}
onCancel()
},
onCancel = { onCancel() },
),
)
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
}
onSuccess()
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
object WalletAlreadyWasUsedDialog {
fun create(context: Context, onOk: () -> Unit, onCancel: () -> Unit, onSupport: () -> Unit): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.security_alert_title)
setMessage(R.string.wallet_been_activated_message)
setPositiveButton(R.string.this_is_my_wallet_title) { dialog, _ ->
onOk()
dialog.dismiss()
}
setNeutralButton(R.string.common_cancel) { dialog, _ ->
onCancel()
dialog.dismiss()
}
setNegativeButton(R.string.alert_button_request_support) { dialog, _ ->
onSupport()
dialog.dismiss()
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -19,6 +19,7 @@ internal sealed interface WelcomeAction : Action {
object ProceedWithCard : WelcomeAction {
object Success : WelcomeAction
data class Error(val error: TangemError) : WelcomeAction
data class ChangeProgress(val showProgress: Boolean) : WelcomeAction
}
data class ProceedWithIntent(val intent: Intent) : WelcomeAction

View file

@ -197,6 +197,9 @@ internal class WelcomeMiddleware {
}
}
},
onProgressStateChange = {
store.dispatchWithMain(WelcomeAction.ProceedWithCard.ChangeProgress(it))
},
onWalletNotCreated = {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
},

View file

@ -27,6 +27,9 @@ internal object WelcomeReducer {
error = action.error,
isUnlockWithCardInProgress = false,
)
is WelcomeAction.ProceedWithCard.ChangeProgress -> state.copy(
isUnlockWithCardInProgress = action.showProgress,
)
is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false)
is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false)
is WelcomeAction.CloseError -> state.copy(error = null)

View file

@ -9,6 +9,7 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
@ -75,4 +76,5 @@ data class DaggerGraphState(
val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null,
val onboardingRepository: OnboardingRepository? = null,
val excludedBlockchains: ExcludedBlockchains? = null,
val appPreferencesStore: AppPreferencesStore? = null,
) : StateType

View file

@ -681,6 +681,7 @@
<string name="scan_card_settings_button">Сканировать</string>
<string name="scan_card_settings_message">Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свой Tangem!</string>
<string name="security_alert_title">Уведомление безопасности</string>
<string name="selling_insufficient_balance_alert_message">На вашем балансе недостаточно средств для продажи этой криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить.</string>
<string name="selling_insufficient_balance_alert_title">Недостаточно средств</string>
<string name="selling_regional_restriction_alert_message">Продажа криптовалюты в вашем регионе временно недоступна. Мы активно работаем над тем, чтобы добавить эту возможность. Следите за нашими новостями!</string>
@ -897,6 +898,7 @@
<string name="swapping_to_title">Вы получите</string>
<string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_not_available">не доступен</string>
<string name="this_is_my_wallet_title">Это мой кошелек</string>
<string name="toast_balances_hidden">Балансы скрыты</string>
<string name="toast_balances_shown">Балансы показаны</string>
<string name="toast_undo">Отменить</string>
@ -966,6 +968,7 @@
<string name="user_wallet_list_unlock_all_with">Разблокировать все с %s</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуйте позже.</string>
<string name="wallet_balance_missing_derivation">Отсканируйте карту или кольцо</string>
<string name="wallet_been_activated_message">Этот кошелек уже был активирован ранее.\nЕсли это сделали не вы, свяжитесь со службой поддержки.\nTangem никогда не продает кошелек вместе с предустановленным кодом доступа.</string>
<string name="wallet_connect_alert_sign_message">Запрос на подпись сообщения.\n\n%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s</string>
<string name="wallet_connect_bnb_trade_order_message">Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s</string>

View file

@ -319,6 +319,7 @@
<string name="feedback_preface_scan_failed">Please tell us what card or ring do you have</string>
<string name="feedback_preface_support">Hi support team,</string>
<string name="feedback_preface_tx_failed">Please tell us more about your issue. Every small detail can help.</string>
<string name="feedback_subject_pre_activated_wallet">Previously activated wallet</string>
<string name="feedback_subject_rate_negative">My suggestions</string>
<string name="feedback_subject_scan_failed">Can\'t scan a card/ring</string>
<string name="feedback_subject_support">Feedback</string>
@ -663,6 +664,7 @@
<string name="scan_card_settings_button">Scan card or ring</string>
<string name="scan_card_settings_message">Scan the card or ring to change its settings. The changes will impact only the card or ring you\'ve scanned and will not affect other devices tied to your wallet.</string>
<string name="scan_card_settings_title">Get your Tangem ready!</string>
<string name="security_alert_title">Security Alert</string>
<string name="selling_insufficient_balance_alert_message">You dont have enough funds in your balance to sell this cryptocurrency. Please deposit the desired asset to proceed.</string>
<string name="selling_insufficient_balance_alert_title">Insufficient Balance</string>
<string name="selling_regional_restriction_alert_message">Selling cryptocurrency is unavailable in your region at the moment. Were actively working to bring this option to you soon—stay tuned!</string>
@ -880,6 +882,7 @@
<string name="swapping_to_title">You receive</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_not_available">not available</string>
<string name="this_is_my_wallet_title">This is my wallet</string>
<string name="toast_balances_hidden">Balances hidden</string>
<string name="toast_balances_shown">Balances shown</string>
<string name="toast_undo">Undo</string>
@ -949,6 +952,7 @@
<string name="user_wallet_list_unlock_all_with">Unlock all with %s</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>
<string name="wallet_balance_missing_derivation">Scan card or ring</string>
<string name="wallet_been_activated_message">This wallet has already been activated earlier.\nIf it was done not by you please contact support.\nTangem never sells wallets along with the pre-generated access code.</string>
<string name="wallet_connect_alert_sign_message">Requesting to sign a message.\n\n%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s</string>
<string name="wallet_connect_bnb_trade_order_message">Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s</string>

View file

@ -18,6 +18,7 @@ interface ScanCardProcessor {
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
onWalletNotCreated: suspend () -> Unit = {},
disclaimerWillShow: () -> Unit = {},
onCancel: suspend () -> Unit = {},
onFailure: suspend (error: TangemError) -> Unit = {},
onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {},
)

View file

@ -57,6 +57,7 @@ class SendFeedbackEmailUseCase(
return when (type) {
is FeedbackEmailType.ScanningProblem,
is FeedbackEmailType.CurrencyDescriptionError,
is FeedbackEmailType.PreActivatedWallet,
-> this
is FeedbackEmailType.DirectUserRequest,
is FeedbackEmailType.RateCanBeBetter,

View file

@ -46,4 +46,6 @@ sealed interface FeedbackEmailType {
data class CurrencyDescriptionError(val currencyId: String, val currencyName: String) : FeedbackEmailType {
override val cardInfo: CardInfo? = null
}
data class PreActivatedWallet(override val cardInfo: CardInfo) : FeedbackEmailType
}

View file

@ -26,6 +26,7 @@ internal class EmailMessageBodyResolver(
is FeedbackEmailType.StakingProblem -> addStakingProblemBody(type)
is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type)
is FeedbackEmailType.CurrencyDescriptionError -> addTokenInfo(type)
is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.cardInfo)
}
return build()

View file

@ -25,6 +25,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.StakingProblem,
is FeedbackEmailType.SwapProblem,
-> R.string.feedback_preface_tx_failed
is FeedbackEmailType.PreActivatedWallet -> R.string.feedback_preface_support
}
.let(resources::getString)
}

View file

@ -29,6 +29,7 @@ internal class EmailSubjectResolver(private val resources: Resources) {
is FeedbackEmailType.StakingProblem,
is FeedbackEmailType.SwapProblem,
-> R.string.feedback_subject_tx_failed
is FeedbackEmailType.PreActivatedWallet -> R.string.feedback_subject_pre_activated_wallet
is FeedbackEmailType.CurrencyDescriptionError -> R.string.feedback_token_description_error
}
.let(resources::getString)

View file

@ -15,6 +15,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withTimeoutOrNull
/**
* Use case to determine which TokenActions are available for a [CryptoCurrency]
@ -45,14 +46,20 @@ class GetCryptoCurrencyActionsUseCase(
userWalletId = userWallet.walletId,
)
val networkId = cryptoCurrencyStatus.currency.network.id
val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency)
val requirements = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) {
walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency)
}
return flow {
val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId)
} else if (!userWallet.isMultiCurrency) {
operations.getPrimaryCurrencyStatusFlow()
operations.getPrimaryCurrencyStatusFlow(includeQuotes = false)
} else {
operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath)
operations.getNetworkCoinFlow(
networkId = networkId,
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
includeQuotes = false,
)
}
val flow = networkFlow.mapLatest { maybeCoinStatus ->
@ -165,10 +172,10 @@ class GetCryptoCurrencyActionsUseCase(
// swap
if (userWallet.isMultiCurrency) {
if (
rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) &&
cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote
) {
val isExchangeable = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) {
rampManager.availableForSwap(userWallet.walletId, cryptoCurrency)
} ?: false
if (isExchangeable && cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote) {
activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None))
} else {
disabledList.add(
@ -304,4 +311,8 @@ class GetCryptoCurrencyActionsUseCase(
cryptoCurrency = cryptoCurrency,
) is StakingAvailability.Available
}
private companion object {
const val REQUEST_EXCHANGE_DATA_TIMEOUT = 1000L
}
}

View file

@ -143,13 +143,14 @@ internal class CurrenciesStatusesOperations(
suspend fun getNetworkCoinFlow(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
includeQuotes: Boolean = true,
): Flow<Either<Error, CryptoCurrencyStatus>> {
val currency = recover(
block = { getNetworkCoin(networkId, derivationPath) },
recover = { return flowOf(it.left()) },
)
return getCurrencyStatusFlow(currency)
return getCurrencyStatusFlow(currency, includeQuotes)
}
suspend fun getNetworkCoinForSingleWalletWithTokenFlow(
@ -163,25 +164,34 @@ internal class CurrenciesStatusesOperations(
return getCurrencyStatusFlow(currency)
}
suspend fun getPrimaryCurrencyStatusFlow(): Flow<Either<Error, CryptoCurrencyStatus>> {
suspend fun getPrimaryCurrencyStatusFlow(includeQuotes: Boolean = true): Flow<Either<Error, CryptoCurrencyStatus>> {
val currency = recover(
block = { getPrimaryCurrency() },
recover = { return flowOf(it.left()) },
)
return getCurrencyStatusFlow(currency)
return getCurrencyStatusFlow(currency, includeQuotes)
}
fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow<Either<Error, CryptoCurrencyStatus>> {
fun getCurrencyStatusFlow(
currency: CryptoCurrency,
includeQuotes: Boolean = true,
): Flow<Either<Error,
CryptoCurrencyStatus,>,> {
val (networks, currenciesIds) = getIds(nonEmptyListOf(currency))
val quoteFlow = getQuotes(currenciesIds)
.map { maybeQuotes ->
maybeQuotes.flatMap { quotes ->
quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }?.right()
?: Error.EmptyQuotes.left()
val quoteFlow = if (includeQuotes) {
getQuotes(currenciesIds)
.map { maybeQuotes ->
maybeQuotes.flatMap { quotes ->
quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }?.right()
?: Error.EmptyQuotes.left()
}
}
}
} else {
// don't use emptyFlow()
flow { emit(Error.EmptyQuotes.left()) }
}
val statusFlow = getNetworksStatuses(networks)
.map { maybeStatuses ->

View file

@ -85,14 +85,8 @@ internal class CurrencyStatusOperations(
} else {
null
}
// order is important for correct total balance calculation
return when {
quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote(
amount = amount,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = status.address,
yieldBalance = currentYieldBalance,
)
currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate),
@ -103,6 +97,13 @@ internal class CurrencyStatusOperations(
networkAddress = status.address,
yieldBalance = currentYieldBalance,
)
quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote(
amount = amount,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = status.address,
yieldBalance = currentYieldBalance,
)
quote is Quote.Value -> CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = calculateFiatAmount(amount, quote.fiatRate),

View file

@ -42,13 +42,7 @@ platform :android do
desc "Build external and release APKs"
lane :build do |options|
gradle(
task: "clean assemble",
build_type: "External",
properties: {
'versionCode' => options[:versionCode],
'versionName' => options[:versionName],
})
gradle(task: 'clean')
gradle(
task: "bundle",
build_type: "Release",

View file

@ -64,6 +64,6 @@ internal class UserWalletListModel @Inject constructor(
}
private fun addUserWallet() = withProgress(isWalletSavingInProgress) {
userWalletSaver.scanAndSaveUserWallet()
userWalletSaver.scanAndSaveUserWallet(modelScope)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.details.utils
import androidx.compose.ui.res.stringResource
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.ensureNotNull
import arrow.core.raise.fold
@ -28,7 +29,11 @@ import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
import com.tangem.features.details.impl.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import kotlin.coroutines.resume
@ComponentScoped
@Suppress("LongParameterList")
@ -42,21 +47,31 @@ internal class UserWalletSaver @Inject constructor(
private val router: Router,
) {
suspend fun scanAndSaveUserWallet() = recover(
block = {
val response = scanCard() ?: return@recover
val userWallet = createUserWallet(response)
suspend fun scanAndSaveUserWallet(scope: CoroutineScope) {
val response = scanCard(scope)
response.fold(
ifLeft = {
val message = it.message
saveWallet(userWallet)
},
recover = { error ->
val message = error.message
if (!message.isNullOrEmpty()) {
messageSender.send(SnackbarMessage(message))
}
},
ifRight = { scanResponse ->
recover(block = {
scanResponse ?: return
val userWallet = createUserWallet(scanResponse)
saveWallet(userWallet)
}, recover = {
val message = it.message
if (!message.isNullOrEmpty()) {
messageSender.send(SnackbarMessage(message))
}
},
)
if (!message.isNullOrEmpty()) {
messageSender.send(SnackbarMessage(message))
}
},)
},
)
}
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet) {
fold(
@ -105,36 +120,37 @@ internal class UserWalletSaver @Inject constructor(
return ensureNotNull(userWallet) { Error.Unknown }
}
private suspend fun Raise<Error>.scanCard(): ScanResponse? {
var response: ScanResponse? = null
private suspend fun scanCard(scope: CoroutineScope) = suspendCancellableCoroutine { continuation ->
scope.launch {
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Settings,
onWalletNotCreated = {
/* no-op */
},
disclaimerWillShow = {
continuation.resume(Either.Right(null))
router.pop()
},
onSuccess = {
continuation.resume(Either.Right(it))
},
onCancel = {
continuation.resume(Either.Right(null))
},
onFailure = { tangemError ->
val error = if (!tangemError.silent) {
val message = tangemError.messageResId
?.let(::resourceReference)
?: stringReference(tangemError.customMessage)
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Settings,
onWalletNotCreated = {
/* no-op */
},
disclaimerWillShow = {
router.pop()
},
onSuccess = {
response = it
},
onFailure = { tangemError ->
val error = if (!tangemError.silent) {
val message = tangemError.messageResId
?.let(::resourceReference)
?: stringReference(tangemError.customMessage)
Error.Message(message)
} else {
Error.Silent
}
raise(error)
},
)
return response
Error.Message(message)
} else {
Error.Silent
}
continuation.resume(Either.Left(error))
},
)
}
}
sealed class Error {

View file

@ -112,8 +112,9 @@ internal class TokenDetailsViewModel @Inject constructor(
private val analyticsEventsHandler: AnalyticsEventHandler,
private val vibratorHapticManager: VibratorHapticManager,
private val clipboardManager: ClipboardManager,
expressStatusFactory: ExpressStatusFactory.Factory,
private val getCryptoCurrencySyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val onrampFeatureToggles: OnrampFeatureToggles,
expressStatusFactory: ExpressStatusFactory.Factory,
getUserWalletUseCase: GetUserWalletUseCase,
getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase,
deepLinksRegistry: DeepLinksRegistry,
@ -212,6 +213,7 @@ internal class TokenDetailsViewModel @Inject constructor(
event = TokenScreenAnalyticsEvent.DetailsScreenOpened(token = cryptoCurrency.symbol),
)
updateTopBarMenu()
initButtons()
updateContent()
handleBalanceHiding(owner)
}
@ -222,6 +224,21 @@ internal class TokenDetailsViewModel @Inject constructor(
super.onCleared()
}
private fun initButtons() {
// we need also init buttons before start all loading to avoid buttons blocking
viewModelScope.launch {
val currentCryptoCurrencyStatus = getCryptoCurrencySyncUseCase.invoke(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
isSingleWalletWithTokens = false,
).getOrNull()
currentCryptoCurrencyStatus?.let {
cryptoCurrencyStatus = it
updateButtons(it)
}
}
}
private fun updateContent() {
subscribeOnCurrencyStatusUpdates()
subscribeOnExpressTransactionsUpdates()

View file

@ -90,7 +90,7 @@ markdownComposeView = "0.5.4"
# region Tangem
tangemBlockchainSdk = "release-app_5.19-881"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.19-412"
tangemCardSdk = "release-app_5.19-414"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem17"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^