Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-14 12:58:58 +03:00
commit 58d34d0fa7
11 changed files with 150 additions and 76 deletions

View file

@ -46,6 +46,7 @@ import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmat
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
@ -81,6 +82,7 @@ internal class SendConfirmModel @Inject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
private val alertFactory: SendConfirmAlertFactory,
private val sendAnalyticHelper: SendAnalyticHelper,
@ -385,7 +387,7 @@ internal class SendConfirmModel @Inject constructor(
}
private fun subscribeOnCheckFeeResultUpdates() {
sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess ->
sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
if (isFeeResultSuccess) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update(SendConfirmSendingStateTransformer(isSending = true))

View file

@ -27,6 +27,7 @@ import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendCon
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
@ -50,6 +51,7 @@ internal class NFTSendConfirmModel @Inject constructor(
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -219,7 +221,7 @@ internal class NFTSendConfirmModel @Inject constructor(
// }
private fun subscribeOnCheckFeeResultUpdates() {
sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess ->
sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
if (isFeeResultSuccess) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true))

View file

@ -25,6 +25,8 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParam
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.isNullOrZero
@ -41,6 +43,7 @@ internal class SendAmountModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val sendAmountReduceListener: SendAmountReduceListener,
private val feeReloadTrigger: SendFeeReloadTrigger,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), AmountScreenClickIntents {
@ -170,6 +173,11 @@ internal class SendAmountModel @Inject constructor(
value = reduceTo,
),
)
feeReloadTrigger.triggerUpdate(
feeData = SendFeeData(
amount = (uiState.value as? AmountState.Data)?.amountTextField?.cryptoAmount?.value,
),
)
}
.launchIn(modelScope)
}
@ -184,6 +192,11 @@ internal class SendAmountModel @Inject constructor(
value = reduceByData,
),
)
feeReloadTrigger.triggerUpdate(
feeData = SendFeeData(
amount = (uiState.value as? AmountState.Data)?.amountTextField?.cryptoAmount?.value,
),
)
}
.launchIn(modelScope)
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.send.v2.subcomponents.fee
import java.math.BigDecimal
data class SendFeeData(
val amount: BigDecimal?,
val destinationAddress: String? = null,
)

View file

@ -8,15 +8,24 @@ import javax.inject.Singleton
interface SendFeeReloadTrigger {
/** Flow triggers fee reload */
val reloadTriggerFlow: Flow<Unit>
/** Trigger fee update */
suspend fun triggerUpdate()
suspend fun triggerUpdate(feeData: SendFeeData)
}
interface SendFeeReloadListener {
/** Flow triggers fee reload */
val reloadTriggerFlow: Flow<SendFeeData>
}
interface SendFeeCheckReloadTrigger {
/** Trigger return callback with check result */
suspend fun callbackCheckResult(isSuccess: Boolean)
/** Trigger fee check reload */
suspend fun triggerCheckUpdate()
}
interface SendFeeCheckReloadListener {
/**
* Flow triggers fee check reload before transaction.
* Usually after significant time after fee was updated last time
@ -25,18 +34,16 @@ interface SendFeeCheckReloadTrigger {
/** Flow return result of fee check update */
val checkReloadResultFlow: Flow<Boolean>
/** Trigger return callback with check result */
suspend fun callbackCheckResult(isSuccess: Boolean)
/** Trigger fee check reload */
suspend fun triggerCheckUpdate()
}
@Singleton
internal class DefaultSendFeeReloadTrigger @Inject constructor() : SendFeeReloadTrigger, SendFeeCheckReloadTrigger {
internal class DefaultSendFeeReloadTrigger @Inject constructor() :
SendFeeReloadTrigger,
SendFeeReloadListener,
SendFeeCheckReloadTrigger,
SendFeeCheckReloadListener {
private val _reloadTriggerFlow = MutableSharedFlow<Unit>()
private val _reloadTriggerFlow = MutableSharedFlow<SendFeeData>()
override val reloadTriggerFlow = _reloadTriggerFlow.asSharedFlow()
private val _checkReloadTriggerFlow = MutableSharedFlow<Unit>()
@ -45,8 +52,8 @@ internal class DefaultSendFeeReloadTrigger @Inject constructor() : SendFeeReload
private val _checkReloadResultFlow = MutableSharedFlow<Boolean>()
override val checkReloadResultFlow = _checkReloadResultFlow.asSharedFlow()
override suspend fun triggerUpdate() {
_reloadTriggerFlow.emit(Unit)
override suspend fun triggerUpdate(feeData: SendFeeData) {
_reloadTriggerFlow.emit(feeData)
}
override suspend fun triggerCheckUpdate() {

View file

@ -1,27 +1,29 @@
package com.tangem.features.send.v2.subcomponents.fee.di
import com.tangem.features.send.v2.subcomponents.fee.DefaultSendFeeReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.*
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
internal object SendFeeModule {
internal interface SendFeeModule {
@Provides
@Binds
@Singleton
fun provideSendFeeReloadTrigger(): SendFeeReloadTrigger {
return DefaultSendFeeReloadTrigger()
}
fun provideSendFeeReloadTrigger(impl: DefaultSendFeeReloadTrigger): SendFeeReloadTrigger
@Provides
@Binds
@Singleton
fun provideSendFeeCheckReloadTrigger(): SendFeeCheckReloadTrigger {
return DefaultSendFeeReloadTrigger()
}
fun provideSendFeeReloadListener(impl: DefaultSendFeeReloadTrigger): SendFeeReloadListener
@Binds
@Singleton
fun provideSendFeeCheckReloadTrigger(impl: DefaultSendFeeReloadTrigger): SendFeeCheckReloadTrigger
@Binds
@Singleton
fun provideSendFeeCheckReloadListener(impl: DefaultSendFeeReloadTrigger): SendFeeCheckReloadListener
}

View file

@ -13,9 +13,10 @@ import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadListener
import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents
import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents.GasPriceInserter
import com.tangem.features.send.v2.subcomponents.fee.model.transformers.*
@ -28,6 +29,7 @@ import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
import javax.inject.Inject
@ -42,7 +44,8 @@ internal class SendFeeModel @Inject constructor(
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeeAlertFactory: SendFeeAlertFactory,
private val feeReloadTrigger: SendFeeReloadTrigger,
private val feeReloadListener: SendFeeReloadListener,
private val feeCheckReloadListener: SendFeeCheckReloadListener,
private val feeCheckReloadTrigger: SendFeeCheckReloadTrigger,
) : Model(), SendFeeClickIntents {
@ -63,14 +66,19 @@ internal class SendFeeModel @Inject constructor(
subscribeOnFeeReloadTriggerUpdates()
subscribeOnFeeCheckReloadTriggerUpdates()
initialState()
loadFee()
feeReload()
}
fun updateState(state: FeeUM) {
_uiState.value = state
}
override fun feeReload() = loadFee()
override fun feeReload() {
loadFee(
amountValue = params.sendAmount,
destinationAddress = params.destinationAddress,
)
}
override fun onFeeSelectorClick(feeType: FeeType) {
_uiState.update(
@ -150,13 +158,18 @@ internal class SendFeeModel @Inject constructor(
}
private fun subscribeOnFeeReloadTriggerUpdates() {
feeReloadTrigger.reloadTriggerFlow
.onEach { feeReload() }
feeReloadListener.reloadTriggerFlow
.onEach { (amount, destination) ->
loadFee(
amountValue = amount ?: params.sendAmount,
destinationAddress = destination ?: params.destinationAddress,
)
}
.launchIn(modelScope)
}
private fun subscribeOnFeeCheckReloadTriggerUpdates() {
feeCheckReloadTrigger.checkReloadTriggerFlow
feeCheckReloadListener.checkReloadTriggerFlow
.onEach { checkLoadFee() }
.launchIn(modelScope)
}
@ -173,13 +186,18 @@ internal class SendFeeModel @Inject constructor(
params.callback.onFeeResult(uiState.value)
}
private fun loadFee() {
private fun loadFee(amountValue: BigDecimal, destinationAddress: String) {
modelScope.launch {
val isShowLoading = (uiState.value as? FeeUM.Content)?.feeSelectorUM !is FeeSelectorUM.Content
if (isShowLoading) {
_uiState.update(SendFeeLoadingTransformer)
}
callFeeUseCase().fold(
getFeeUseCase.invoke(
amount = amountValue,
destination = destinationAddress,
userWallet = params.userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
).fold(
ifRight = {
_uiState.update(
SendFeeLoadedTransformer(
@ -204,7 +222,12 @@ internal class SendFeeModel @Inject constructor(
private fun checkLoadFee() {
modelScope.launch {
callFeeUseCase().fold(
getFeeUseCase.invoke(
amount = params.sendAmount,
destination = params.destinationAddress,
userWallet = params.userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
).fold(
ifRight = {
sendFeeAlertFactory.getFeeUpdatedAlert(
newFee = it,
@ -234,20 +257,13 @@ internal class SendFeeModel @Inject constructor(
ifLeft = { feeError ->
feeCheckReloadTrigger.callbackCheckResult(false)
_uiState.update(SendFeeFailedTransformer(feeError))
sendFeeAlertFactory.getFeeUnreachableErrorState(::loadFee)
sendFeeAlertFactory.getFeeUnreachableErrorState(::feeReload)
updateFeeNotifications()
},
)
}.saveIn(feeJobHolder)
}
private suspend fun callFeeUseCase() = getFeeUseCase.invoke(
amount = params.sendAmount,
destination = params.destinationAddress,
userWallet = params.userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
)
private fun isFeeApproximate(amountType: AmountType): Boolean {
val networkId = feeCryptoCurrencyStatus.currency.network.id
return isFeeApproximateUseCase(

View file

@ -32,6 +32,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
import com.tangem.features.send.v2.subcomponents.fee.model.checkFeeCoverage
@ -52,7 +53,7 @@ import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@Stable
@ModelScoped
internal class NotificationsModel @Inject constructor(
@ -116,7 +117,12 @@ internal class NotificationsModel @Inject constructor(
feeError = notificationData.feeError,
onReload = {
modelScope.launch {
sendFeeReloadTrigger.triggerUpdate()
sendFeeReloadTrigger.triggerUpdate(
feeData = SendFeeData(
amount = notificationData.amountValue,
destinationAddress = notificationData.destinationAddress,
),
)
}
},
onClick = ::showTokenDetails,
@ -341,7 +347,7 @@ internal class NotificationsModel @Inject constructor(
},
onCloseClick = {
modelScope.launch {
buildNotifications()
sendAmountReduceTrigger.triggerIgnoreReduce()
}
},
)

View file

@ -6,12 +6,14 @@ import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.mapLatest
import java.util.UUID
@ -23,6 +25,8 @@ internal class TxHistoryUiManager(
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
// filter initial states, since we dont emit loading items as UI items
.filter { it.status !is PaginationStatus.None && it.status !is PaginationStatus.InitialLoading }
.mapLatest { state ->
state.uiBatches.asSequence()
.flatMap { it.data }

View file

@ -66,33 +66,34 @@ dependencies {
implementation(projects.libs.blockchainSdk)
/** Domain modules */
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.settings)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.analytics)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.analytics)
implementation(projects.domain.visa)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.markets.models)
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.feedback)
implementation(projects.domain.onramp.models)
implementation(projects.domain.onramp)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.legacy)
implementation(projects.domain.markets.models)
implementation(projects.domain.models)
implementation(projects.domain.networks)
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)
implementation(projects.domain.onramp)
implementation(projects.domain.onramp.models)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.settings)
implementation(projects.domain.staking)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.visa)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
implementation(projects.features.onboarding)

View file

@ -12,6 +12,7 @@ import com.tangem.domain.card.SetCardWasScannedUseCase
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
@ -19,6 +20,7 @@ import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
@ -101,6 +103,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val urlOpener: UrlOpener,
private val tokensFeatureToggles: TokensFeatureToggles,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
override fun onAddBackupCardClick() {
@ -149,12 +153,21 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
).fold(
ifLeft = { Timber.e(it, "Failed to derive public keys") },
ifRight = {
fetchTokenListUseCase(
userWalletId = userWallet.walletId,
mode = RefreshMode.SKIP_CURRENCIES,
currencies = missedAddressCurrencies,
).onLeft {
Timber.e("Unable to refresh token list: $it")
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(
userWalletId = userWallet.walletId,
networks = missedAddressCurrencies.map(CryptoCurrency::network).toSet(),
),
)
} else {
fetchTokenListUseCase(
userWalletId = userWallet.walletId,
mode = RefreshMode.SKIP_CURRENCIES,
currencies = missedAddressCurrencies,
).onLeft {
Timber.e("Unable to refresh token list: $it")
}
}
},
)