Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-25 18:30:16 +07:00
commit 9b1c90cf12
19 changed files with 272 additions and 74 deletions

View file

@ -9,6 +9,7 @@ import androidx.work.WorkManager
import com.tangem.common.routing.AppRoute
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -28,6 +29,7 @@ internal class LockUserWalletsTimer(
private val userWalletsListRepository: UserWalletsListRepository,
private val coroutineScope: CoroutineScope,
private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase,
private val passwordRequester: HotWalletPasswordRequester,
) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver {
@ -53,6 +55,7 @@ internal class LockUserWalletsTimer(
)
if (shouldOpenWelcomeScreenOnResume) {
passwordRequester.dismiss()
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
@ -116,6 +119,7 @@ internal class LockUserWalletsTimer(
start()
}
.onRight {
passwordRequester.dismiss()
clearAllHotWalletContextualUnlockUseCase.invoke()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}

View file

@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
@ -29,6 +30,8 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.extensions.LocalUserInteractionTracker
import com.tangem.core.ui.extensions.UserInteractionTracker
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
@ -43,6 +46,7 @@ import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.google.GoogleServicesHelper
@ -160,6 +164,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
@Inject
internal lateinit var passwordRequester: HotWalletPasswordRequester
private val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -232,8 +239,16 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intent),
)
val userInteractionTracker = object : UserInteractionTracker {
override fun onUserInteraction() {
lockUserWalletsTimer?.restart()
}
}
setContent {
routingComponent.Content(Modifier.fillMaxSize())
CompositionLocalProvider(LocalUserInteractionTracker provides userInteractionTracker) {
routingComponent.Content(Modifier.fillMaxSize())
}
}
}
@ -250,6 +265,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
coroutineScope = mainScope,
userWalletsListRepository = userWalletsListRepository,
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
passwordRequester = passwordRequester,
)
store.dispatch(

View file

@ -75,12 +75,14 @@ internal object TransactionDomainModule {
walletManagersFacade: WalletManagersFacade,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
getHotTransactionSigner = tangemHotWalletSignerFactory::create,
)
}
@ -89,10 +91,12 @@ internal object TransactionDomainModule {
fun provideRetryTransactionUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): RetryIncompleteTransactionUseCase {
return RetryIncompleteTransactionUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
getHotTransactionSigner = tangemHotWalletSignerFactory::create,
)
}
@ -101,10 +105,12 @@ internal object TransactionDomainModule {
fun provideOpenTrustlineUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): OpenTrustlineUseCase {
return OpenTrustlineUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
getHotTransactionSigner = tangemHotWalletSignerFactory::create,
)
}

View file

@ -62,7 +62,7 @@
},
{
"name": "MAIN_SCREEN_QR_SCANNING_ENABLED",
"version": "undefined"
"version": "5.36"
},
{
"name": "NEW_PROMO_BANNERS_ENABLED",

View file

@ -0,0 +1,40 @@
package com.tangem.core.ui.extensions
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.pointerInput
val LocalUserInteractionTracker = staticCompositionLocalOf<UserInteractionTracker?> { null }
interface UserInteractionTracker {
fun onUserInteraction()
}
fun Modifier.trackUserInteraction(tracker: UserInteractionTracker?): Modifier {
if (tracker == null) return this
return this
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
if (down.pressed) {
tracker.onUserInteraction()
}
waitForUpOrCancellation()
}
}
.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyDown) {
tracker.onUserInteraction()
}
false
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.DataToSign
import com.tangem.operations.sign.SignData
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -32,7 +33,7 @@ class TangemHotWalletSigner @AssistedInject constructor(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
val result = runCatching {
val result = runSuspendCatching {
hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = listOf(
@ -59,7 +60,7 @@ class TangemHotWalletSigner @AssistedInject constructor(
dataToSign: List<SignData>,
publicKey: Wallet.PublicKey,
): CompletionResult<Map<ByteArray, ByteArray>> {
val result = runCatching {
val result = runSuspendCatching {
hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = dataToSign.map { signData ->

View file

@ -3,10 +3,12 @@ package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
@ -22,15 +24,13 @@ class AssociateAssetUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Either<AssociateAssetError, Unit> {
suspend operator fun invoke(userWallet: UserWallet, currency: CryptoCurrency): Either<AssociateAssetError, Unit> {
return either {
val networkCoin = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
params = MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId),
)
?.firstOrNull {
val network = currency.network
@ -38,18 +38,19 @@ class AssociateAssetUseCase(
}
?: error("Unable to create network coin for currencyID: ${currency.id}")
if (isBalanceZero(userWalletId, networkCoin)) {
if (isBalanceZero(userWallet.walletId, networkCoin)) {
raise(AssociateAssetError.NotEnoughBalance(networkCoin))
}
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards
)
val signer = createSigner(userWallet)
catch(
block = {
when (val result = walletManagersFacade.fulfillRequirements(userWalletId, currency, signer)) {
when (val result = walletManagersFacade.fulfillRequirements(
userWallet.walletId,
currency,
signer,
)) {
is SimpleResult.Failure -> raise(AssociateAssetError.DataError(result.error.customMessage))
SimpleResult.Success -> Unit
}
@ -59,6 +60,20 @@ class AssociateAssetUseCase(
}
}
private fun createSigner(userWallet: UserWallet): TransactionSigner {
return when (userWallet) {
is UserWallet.Hot -> getHotTransactionSigner(userWallet)
is UserWallet.Cold -> getColdSigner()
}
}
private fun getColdSigner(): TransactionSigner {
return cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards
)
}
private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
val networkStatus = singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(

View file

@ -4,32 +4,32 @@ import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.OpenTrustlineError
import com.tangem.domain.transaction.error.parseWrappedError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.models.wallet.UserWalletId
class OpenTrustlineUseCase(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Either<OpenTrustlineError, Unit> {
suspend operator fun invoke(userWallet: UserWallet, currency: CryptoCurrency): Either<OpenTrustlineError, Unit> {
return either {
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards
)
val signer = createSigner(userWallet)
catch(
block = {
when (val result = walletManagersFacade.fulfillRequirements(userWalletId, currency, signer)) {
when (val result = walletManagersFacade.fulfillRequirements(
userWallet.walletId,
currency,
signer,
)) {
is SimpleResult.Failure -> when (val error = result.error) {
is BlockchainSdkError.Stellar.MinReserveRequired ->
raise(
@ -58,4 +58,18 @@ class OpenTrustlineUseCase(
)
}
}
private fun createSigner(userWallet: UserWallet): TransactionSigner {
return when (userWallet) {
is UserWallet.Hot -> getHotTransactionSigner(userWallet)
is UserWallet.Cold -> getColdSigner()
}
}
private fun getColdSigner(): TransactionSigner {
return cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards
)
}
}

View file

@ -4,33 +4,36 @@ import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.IncompleteTransactionError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.error.parseWrappedError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.models.wallet.UserWalletId
class RetryIncompleteTransactionUseCase(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
userWallet: UserWallet,
currency: CryptoCurrency,
): Either<IncompleteTransactionError, Unit> {
return either {
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards
)
val signer = createSigner(userWallet)
catch(
block = {
when (val result = walletManagersFacade.fulfillRequirements(userWalletId, currency, signer)) {
when (val result = walletManagersFacade.fulfillRequirements(
userWallet.walletId,
currency,
signer,
)) {
is SimpleResult.Failure -> {
val error = result.error as? BlockchainSdkError
when (error) {
@ -51,4 +54,18 @@ class RetryIncompleteTransactionUseCase(
)
}
}
private fun createSigner(userWallet: UserWallet): TransactionSigner {
return when (userWallet) {
is UserWallet.Hot -> getHotTransactionSigner(userWallet)
is UserWallet.Cold -> getColdSigner()
}
}
private fun getColdSigner(): TransactionSigner {
return cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards
)
}
}

View file

@ -35,6 +35,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.cancellation.CancellationException
@Suppress("LongParameterList")
class SendTransactionUseCase(
@ -96,6 +97,8 @@ class SendTransactionUseCase(
is Result.Success -> sendResult.data.right()
}
}
} catch (e: CancellationException) {
throw e
} catch (ex: Exception) {
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
SendTransactionError.DataError(ex.message).left()

View file

@ -18,6 +18,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.extensions.LocalUserInteractionTracker
import com.tangem.core.ui.extensions.trackUserInteraction
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.appbar.TangemTopAppBar
@ -37,7 +39,13 @@ import com.tangem.features.hotwallet.impl.R
@Suppress("MagicNumber", "LongMethod")
@Composable
internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM, modifier: Modifier = Modifier) {
Box(Modifier.fillMaxSize()) {
val userInteractionTracker = LocalUserInteractionTracker.current
Box(
modifier = Modifier
.fillMaxSize()
.trackUserInteraction(userInteractionTracker),
) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,

View file

@ -19,6 +19,16 @@ sealed class PredefinedValues {
override val amount: String?,
override val address: String,
override val memo: String?,
val source: Source,
) : Content()
}
}
enum class Source {
MAIN_SCREEN,
SEND_SCREEN,
}
}
inline val PredefinedValues.isFromMainScreenQr: Boolean
get() = (this as? PredefinedValues.Content.QrCode)
?.source == PredefinedValues.Source.MAIN_SCREEN

View file

@ -60,10 +60,7 @@ internal class NetworkSelectionModel @Inject constructor(
private val searchQuery = MutableStateFlow("")
private val expandedWallets = MutableStateFlow(
params.walletGroups.firstOrNull()
?.userWalletId
?.let(::setOf)
.orEmpty(),
params.walletGroups.map { it.userWalletId }.toSet(),
)
val uiState: StateFlow<NetworkSelectionUM> = createUiStateFlow()

View file

@ -48,8 +48,10 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.entity.isFromMainScreenQr
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.CommonSendRoute.*
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
@ -171,11 +173,11 @@ internal class SendModel @Inject constructor(
subscribeOnBalanceHidden()
subscribeOnQRScannerResult()
subscribeOnCurrencyStatusUpdates()
if (params.amount != null) {
subscribeOnStatusForDeeplinkDestination()
initPredefinedValues()
if (predefinedValues is PredefinedValues.Content) {
subscribeOnStatusForInitialNavigation()
}
initAppCurrency()
initPredefinedValues()
}
override fun onNavigationResult(navigationUM: NavigationUM) {
@ -228,7 +230,14 @@ internal class SendModel @Inject constructor(
onBackClick()
} else {
when (currentRoute.value) {
is Amount -> router.push(Destination(isEditMode = false))
is Amount -> {
val nextRoute = if (predefinedValues.isFromMainScreenQr) {
Confirm
} else {
Destination(isEditMode = false)
}
router.push(nextRoute)
}
is Destination -> router.push(Confirm)
Confirm -> router.push(ConfirmSuccess)
else -> onBackClick()
@ -381,7 +390,7 @@ internal class SendModel @Inject constructor(
PredefinedValues.Content.QrCode(
amount = predefinedAmount,
address = predefinedAddress,
memo = params.tag,
memo = params.tag, source = PredefinedValues.Source.MAIN_SCREEN,
)
} else {
PredefinedValues.Empty
@ -429,26 +438,38 @@ internal class SendModel @Inject constructor(
}
}
private fun subscribeOnStatusForDeeplinkDestination() {
private fun subscribeOnStatusForInitialNavigation() {
combine(
cryptoCurrencyStatusFlow,
feeCryptoCurrencyStatusFlow,
) { cryptoCurrencyStatus, feeCryptoCurrencyStatus ->
if (isAvailableForSend && currentRoute.value == initialRoute) {
if (isPredefinedAmountExceedsBalance(cryptoCurrencyStatus)) {
router.replaceAll(Amount(isEditMode = false))
} else {
router.replaceAll(Confirm)
}
} else if (isUnavailableForSend) {
showAlertError()
) { cryptoCurrencyStatus, _ ->
if (!isAvailableForSend || currentRoute.value != initialRoute) {
if (isUnavailableForSend) showAlertError()
return@combine
}
val route = resolveInitialRoute(cryptoCurrencyStatus) ?: return@combine
router.replaceAll(route)
}.launchIn(modelScope)
}
private fun isPredefinedAmountExceedsBalance(cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
val predefinedAmount = (predefinedValues as? PredefinedValues.Content)?.amount
?.parseBigDecimalOrNull() ?: return false
private fun resolveInitialRoute(cryptoCurrencyStatus: CryptoCurrencyStatus): CommonSendRoute? {
return when (predefinedValues) {
is PredefinedValues.Content.Deeplink -> Confirm
is PredefinedValues.Content.QrCode -> {
if (!predefinedValues.isFromMainScreenQr) return null
val amount = (predefinedValues as PredefinedValues.Content.QrCode).amount ?: return null
if (isPredefinedAmountExceedsBalance(amount, cryptoCurrencyStatus)) {
Amount(isEditMode = false)
} else {
Confirm
}
}
PredefinedValues.Empty -> null
}
}
private fun isPredefinedAmountExceedsBalance(amount: String, cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
val predefinedAmount = amount.parseBigDecimalOrNull() ?: return false
val balance = cryptoCurrencyStatus.value.amount ?: return false
return predefinedAmount > balance
}
@ -495,6 +516,7 @@ internal class SendModel @Inject constructor(
amount = amount.orEmpty(),
address = parsedQrCode?.address.orEmpty(),
memo = parsedQrCode?.memo,
source = PredefinedValues.Source.SEND_SCREEN,
)
// If it is in active state use flow to update value in amount component
modelScope.launch {

View file

@ -30,6 +30,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.entity.isFromMainScreenQr
import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents.SelectedCurrencyType
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
@ -408,7 +409,7 @@ internal class SendAmountModel @Inject constructor(
val isMultiCurrency = userWallet?.isMultiCurrency == true
isSendWithSwapAvailable.update {
isAvailableForSwap && isMultiCurrency
isAvailableForSwap && isMultiCurrency && !params.predefinedValues.isFromMainScreenQr
}
}
}

View file

@ -34,6 +34,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.swap.SwapComponent
import com.tangem.utils.extensions.isZero
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -129,16 +130,23 @@ internal class DefaultSwapComponent @AssistedInject constructor(
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) {
if (shouldHideBlock) {
TangemLogger.e(
"Dismissing fee selector: " +
"shouldHideBlock = $shouldHideBlock, amount = ${dataState.amount}, " +
"isInsufficientFunds = ${model.uiState.isInsufficientFunds}",
)
slotNavigation.dismiss()
return@LaunchedEffect
}
val sendingCryptoCurrencyStatus = fromCryptoCurrency ?: run {
TangemLogger.e("Dismissing fee selector: fromCryptoCurrency is null")
slotNavigation.dismiss()
return@LaunchedEffect
}
val feeCurrencyStatus = feePaidCryptoCurrency ?: run {
TangemLogger.e("Dismissing fee selector: feePaidCryptoCurrency is null")
slotNavigation.dismiss()
return@LaunchedEffect
}

View file

@ -551,7 +551,11 @@ internal class SwapModel @Inject constructor(
dataState.fromCryptoCurrency
}
if (fromCryptoCurrency != null) updateFeePaidCryptoCurrencyFor(fromCryptoCurrency)
if (fromCryptoCurrency != null) {
updateFeePaidCryptoCurrencyFor(fromCryptoCurrency)
} else {
TangemLogger.e("updateFeePaidCryptoCurrencyFor failed: fromCryptoCurrency is null")
}
subscribeToCoinBalanceUpdatesIfNeeded()
}.onFailure { error ->
@ -763,7 +767,14 @@ internal class SwapModel @Inject constructor(
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = fromToken,
).getOrNull(),
)
.onLeft { TangemLogger.e("Unable to get fee paid crypto currency status for ${fromToken.currency.id}") }
.onRight { currencyStatus ->
if (currencyStatus == null) {
TangemLogger.e("Fee paid crypto currency status is null for ${fromToken.currency.id}")
}
}
.getOrNull(),
)
}
@ -1038,6 +1049,7 @@ internal class SwapModel @Inject constructor(
val fee = getSelectedFee()
if (fee == null && tangemPayInput?.isWithdrawal != true) {
TangemLogger.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}")
showAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
modelScope.launch {
delay(SWAP_IN_PROGRESS_DELAY)
@ -1064,6 +1076,7 @@ internal class SwapModel @Inject constructor(
when (swapTransactionState) {
is SwapTransactionState.TxSent -> {
if (fee == null) {
TangemLogger.e("onSwapClick: onSuccess: fee is null after swap")
showAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
return@onSuccess
}
@ -1400,16 +1413,16 @@ internal class SwapModel @Inject constructor(
}
modelScope.launch {
updateFeePaidCryptoCurrencyFor(fromToken)
startLoadingQuotes(
fromToken = fromToken,
fromAccount = fromAccount,
toToken = toToken,
toAccount = toAccount,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(fromToken, toToken),
)
}
startLoadingQuotes(
fromToken = fromToken,
fromAccount = fromAccount,
toToken = toToken,
toAccount = toAccount,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(fromToken, toToken),
)
updateTokensState(tokens)
}
}
@ -2295,10 +2308,14 @@ internal class SwapModel @Inject constructor(
private fun getSelectedFeeState(): TxFeeSealedState {
val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content
?: return TxFeeSealedState.Legacy(
if (feeStateUM == null) {
TangemLogger.e("getSelectedFeeState: FeeSelectorUM is not Content: $feeStateUM, returning Legacy state")
return TxFeeSealedState.Legacy(
txFeeState = TxFeeState.Empty,
selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
)
}
val transactionFeeExtended = feeStateUM.feeExtraInfo.transactionFeeExtended
return TxFeeSealedState.Component(
@ -2312,7 +2329,13 @@ internal class SwapModel @Inject constructor(
}
private fun getSelectedFee(): TxFee? {
val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content ?: return null
val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content
if (feeStateUM == null) {
TangemLogger.e("getSelectedFee: FeeSelectorUM is not Content: $feeStateUM, returning null")
return null
}
val transactionFeeExtended = feeStateUM.feeExtraInfo.transactionFeeExtended
return TxFee.FeeComponent(
@ -2368,6 +2391,7 @@ internal class SwapModel @Inject constructor(
if (newState is FeeSelectorUM.Error) {
modelScope.launch {
TangemLogger.e("onResult: FeeSelectorUM is Error, isHidden = true")
forceUpdateState.emit(newState.copy(isHidden = true))
}
return
@ -2382,6 +2406,8 @@ internal class SwapModel @Inject constructor(
newState.feeExtraInfo.feeCryptoCurrencyStatus.currency is CryptoCurrency.Coin
if (isFeeCurrencySameAsFromCurrency || isCoinFeeSelected) {
TangemLogger.e("onResult: Fee currency is same as from currency or coin fee selected, reloading quotes")
// block swap button until fee is loaded
uiState = uiState.copy(
swapButton = uiState.swapButton.copy(
@ -2404,15 +2430,19 @@ internal class SwapModel @Inject constructor(
}
override suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
TangemLogger.e("loadFee: Start loading fee")
val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) {
TangemLogger.e("loadFee: Quotes not loaded ${dataState.lastLoadedSwapStates[selectedProvider]}")
return Either.Left(GetFeeError.UnknownError)
}
if (isPermissionNotificationShown()) {
TangemLogger.e("loadFee: Permission notification is shown, cannot load fee")
return Either.Left(GetFeeError.UnknownError)
}
@ -2425,6 +2455,12 @@ internal class SwapModel @Inject constructor(
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
)
.onLeft {
TangemLogger.e("loadFee: Failed to load fee with error $it")
}
.onRight {
TangemLogger.e("loadFee: Fee loaded successfully")
}
}
override fun choosingInProgress(updatedState: Boolean) {

View file

@ -205,7 +205,7 @@ internal class SwapNotificationsFactory(
sendingAmount = amountToRequest.value,
cryptoCurrency = fromCurrencyStatus.currency,
feeCryptoCurrency = feeCryptoCurrencyStatus?.currency,
isAccountFunded = false,
isAccountFunded = true, // consider the account is funded on the provider side
)
addReduceAmountNotification(
cryptoCurrencyStatus = fromCurrencyStatus,

View file

@ -936,7 +936,7 @@ internal class TokenDetailsModel @Inject constructor(
)
modelScope.launch {
retryIncompleteTransactionUseCase(
userWalletId = userWalletId,
userWallet = userWallet,
currency = cryptoCurrency,
).fold(
ifLeft = { e ->
@ -977,7 +977,7 @@ internal class TokenDetailsModel @Inject constructor(
)
modelScope.launch(dispatchers.mainImmediate) {
openTrustlineUseCase(
userWalletId = userWalletId,
userWallet = userWallet,
currency = cryptoCurrency,
).fold(
ifLeft = { e ->
@ -1049,7 +1049,7 @@ internal class TokenDetailsModel @Inject constructor(
)
modelScope.launch(dispatchers.io) {
associateAssetUseCase(
userWalletId = userWalletId,
userWallet = userWallet,
currency = cryptoCurrency,
).fold(
ifLeft = { e ->