Updated on 2026-08-14
This commit is contained in:
commit
9b1c90cf12
19 changed files with 272 additions and 74 deletions
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
},
|
||||
{
|
||||
"name": "MAIN_SCREEN_QR_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.36"
|
||||
},
|
||||
{
|
||||
"name": "NEW_PROMO_BANNERS_ENABLED",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue