Updated on 2026-08-14
This commit is contained in:
parent
f81e51f4c4
commit
3baec6cc7e
27 changed files with 580 additions and 22 deletions
|
|
@ -17,6 +17,7 @@ import com.tangem.domain.transaction.usecase.*
|
|||
import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -323,4 +324,12 @@ internal object TransactionDomainModule {
|
|||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsGaslessFeeSupportedForNetwork(
|
||||
gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
): IsGaslessFeeSupportedForNetwork {
|
||||
return IsGaslessFeeSupportedForNetwork(gaslessTransactionRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.components.token.internal
|
|||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -52,6 +53,10 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
|
|||
is TokenFiatAmountState.Locked -> {
|
||||
LockedRectangle(modifier = modifier.placeholderSize())
|
||||
}
|
||||
is TokenFiatAmountState.Empty -> {
|
||||
// Empty box for proper measurements
|
||||
Box(modifier)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,6 +243,8 @@ sealed class TokenItemState {
|
|||
data object Loading : FiatAmountState()
|
||||
|
||||
data object Locked : FiatAmountState()
|
||||
|
||||
data object Empty : FiatAmountState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.transaction.usecase.gasless
|
||||
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.transaction.GaslessTransactionRepository
|
||||
|
||||
/**
|
||||
* Use case to check if gasless fee is supported for a given network.
|
||||
*/
|
||||
class IsGaslessFeeSupportedForNetwork(
|
||||
private val gaslessTransactionRepository: GaslessTransactionRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(network: Network): Boolean {
|
||||
return gaslessTransactionRepository.isNetworkSupported(network)
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ data class FeeExtraInfo(
|
|||
val isFeeConvertibleToFiat: Boolean,
|
||||
val isTronToken: Boolean,
|
||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val availableFeeCurrencies: ImmutableList<CryptoCurrencyStatus>? = null,
|
||||
)
|
||||
|
||||
sealed class FeeNonce {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.blockchain.common.transaction.Fee
|
|||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
|
|
@ -13,6 +14,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
|||
|
||||
sealed class FeeSelectorParams {
|
||||
abstract val state: FeeSelectorUM
|
||||
abstract val userWalletId: UserWalletId
|
||||
abstract val onLoadFeeExtended: (suspend () -> Either<GetFeeError, TransactionFeeExtended>)?
|
||||
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
|
||||
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
|
|
@ -24,6 +26,7 @@ sealed class FeeSelectorParams {
|
|||
|
||||
data class FeeSelectorBlockParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val onLoadFeeExtended: (suspend () -> Either<GetFeeError, TransactionFeeExtended>)? = null,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
|
|
@ -36,6 +39,7 @@ sealed class FeeSelectorParams {
|
|||
|
||||
data class FeeSelectorDetailsParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val onLoadFeeExtended: (suspend () -> Either<GetFeeError, TransactionFeeExtended>)? = null,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWalletId,
|
||||
),
|
||||
onDismiss = {
|
||||
model.feeSelectorBottomSheet.dismiss()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
|||
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.v2.feeselector.component.extended.FeeExtendedSelectorComponent
|
||||
import com.tangem.features.send.v2.feeselector.component.speed.FeeSpeedSelectorComponent
|
||||
import com.tangem.features.send.v2.feeselector.component.token.FeeTokenSelectorComponent
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
|
||||
import com.tangem.features.send.v2.feeselector.ui.FeeSelectorModalBottomSheet
|
||||
|
|
@ -28,8 +29,9 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
|||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
@Assisted private val onDismiss: () -> Unit,
|
||||
private val feeExtendedSelectorComponent: FeeExtendedSelectorComponent.Factory,
|
||||
private val feeExtendedSelectorComponentFactory: FeeExtendedSelectorComponent.Factory,
|
||||
private val feeSpeedSelectorComponentFactory: FeeSpeedSelectorComponent.Factory,
|
||||
private val feeTokenSelectorComponentFactory: FeeTokenSelectorComponent.Factory,
|
||||
) : FeeSelectorComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: FeeSelectorModel = getOrCreateModel(params = params)
|
||||
|
|
@ -86,7 +88,7 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
|||
)
|
||||
|
||||
return when (config) {
|
||||
FeeSelectorRoute.NetworkFee -> feeExtendedSelectorComponent.create(
|
||||
FeeSelectorRoute.NetworkFee -> feeExtendedSelectorComponentFactory.create(
|
||||
appComponentContext = appComponentContext,
|
||||
params = componentParams,
|
||||
)
|
||||
|
|
@ -94,7 +96,10 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
|||
appComponentContext = appComponentContext,
|
||||
params = componentParams,
|
||||
)
|
||||
FeeSelectorRoute.ChooseToken -> TODO()
|
||||
FeeSelectorRoute.ChooseToken -> feeTokenSelectorComponentFactory.create(
|
||||
appComponentContext = appComponentContext,
|
||||
params = componentParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.v2.feeselector.component.extended.entity.FeeExtendedSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.component.extended.entity.SelectedTokenItemConverter
|
||||
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.extended.entity
|
||||
package com.tangem.features.send.v2.feeselector.component.extended.model
|
||||
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter
|
||||
import com.tangem.core.ui.components.icons.IconTint
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.v2.feeselector.component.token.model.FeeTokenSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.component.token.ui.FeeTokenSelectorContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class FeeTokenSelectorComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: FeeSelectorComponentParams,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: FeeTokenSelectorModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
FeeTokenSelectorContent(
|
||||
state = state,
|
||||
intents = model,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: FeeSelectorComponentParams,
|
||||
): FeeTokenSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents
|
||||
|
||||
@Stable
|
||||
internal interface FeeTokenSelectorIntents : FeeSelectorIntents {
|
||||
fun onLearnMoreClick()
|
||||
}
|
||||
|
||||
@Stable
|
||||
internal class StubFeeTokenSelectorIntents : FeeTokenSelectorIntents, FeeSelectorIntents by StubFeeSelectorIntents() {
|
||||
override fun onLearnMoreClick() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token.entity
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class FeeTokenSelectorUM(
|
||||
val parent: FeeSelectorUM.Content,
|
||||
val selectedToken: TokenItemState,
|
||||
val tokens: ImmutableList<TokenItemState>,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token.model
|
||||
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class FeeTokenForListConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val onTokenClick: () -> Unit,
|
||||
) : Converter<CryptoCurrencyStatus, TokenItemState> {
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
|
||||
if (value.value !is CryptoCurrencyStatus.Loaded) {
|
||||
return TokenItemState.Loading(id = value.currency.id.value)
|
||||
}
|
||||
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val tokenItemConverter = TokenItemStateConverter(
|
||||
appCurrency = appCurrencyProvider(),
|
||||
onItemClick = { _, _ -> onTokenClick() },
|
||||
)
|
||||
|
||||
val state = tokenItemConverter.convert(value)
|
||||
|
||||
return when (state) {
|
||||
is TokenItemState.Content -> {
|
||||
val balance = value.value.fiatAmount.format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
|
||||
state.copy(
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Empty,
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = resourceReference(
|
||||
R.string.common_balance,
|
||||
wrappedList(balance),
|
||||
),
|
||||
isAvailable = false,
|
||||
),
|
||||
subtitle2State = null,
|
||||
|
||||
)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.TangemBlogUrlBuilder
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.v2.feeselector.component.token.FeeTokenSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.component.token.entity.FeeTokenSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class FeeTokenSelectorModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
) : Model(), FeeTokenSelectorIntents,
|
||||
FeeSelectorIntents by paramsContainer.require<FeeSelectorComponentParams>().intents {
|
||||
|
||||
private val params = paramsContainer.require<FeeSelectorComponentParams>()
|
||||
private var appCurrency = AppCurrency.Default
|
||||
|
||||
val uiState: StateFlow<FeeTokenSelectorUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
init {
|
||||
initAppCurrency()
|
||||
|
||||
params.state
|
||||
.onEach { uiState.value = stateFromParent(it as FeeSelectorUM.Content) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun initAppCurrency() {
|
||||
modelScope.launch {
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): FeeTokenSelectorUM {
|
||||
val parentState = params.state.value as FeeSelectorUM.Content
|
||||
return stateFromParent(parentState)
|
||||
}
|
||||
|
||||
private fun stateFromParent(newParentState: FeeSelectorUM.Content): FeeTokenSelectorUM {
|
||||
val parentState = newParentState
|
||||
val selectedCurrencyStatus = parentState.feeExtraInfo.feeCryptoCurrencyStatus
|
||||
val selectedTokenUM = convertToken(selectedCurrencyStatus)
|
||||
|
||||
return FeeTokenSelectorUM(
|
||||
parent = parentState,
|
||||
selectedToken = selectedTokenUM,
|
||||
tokens = parentState.feeExtraInfo.availableFeeCurrencies?.map { status ->
|
||||
convertToken(status)
|
||||
}?.toImmutableList() ?: persistentListOf(selectedTokenUM),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertToken(status: CryptoCurrencyStatus): TokenItemState {
|
||||
return FeeTokenForListConverter(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
onTokenClick = { onTokenSelected(status) },
|
||||
).convert(status)
|
||||
}
|
||||
|
||||
override fun onLearnMoreClick() {
|
||||
modelScope.launch {
|
||||
urlOpener.openUrl(
|
||||
TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.selectedBorder
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.feeselector.component.token.FeeTokenSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.component.token.StubFeeTokenSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.component.token.entity.FeeTokenSelectorUM
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
internal fun FeeTokenSelectorContent(
|
||||
state: FeeTokenSelectorUM,
|
||||
intents: FeeTokenSelectorIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(
|
||||
bottom = 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
) {
|
||||
Title(onLearnMoreClick = intents::onLearnMoreClick)
|
||||
SpacerH8()
|
||||
TokenListContent(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(modifier: Modifier = Modifier, onLearnMoreClick: () -> Unit) {
|
||||
val linkText = stringResourceSafe(R.string.common_learn_more)
|
||||
val fullString = stringResourceSafe(R.string.fee_selector_choose_token_description, linkText)
|
||||
val linkTextPosition = fullString.length - linkText.length
|
||||
val defaultColor = TangemTheme.colors.text.secondary
|
||||
val linkColor = TangemTheme.colors.text.accent
|
||||
val annotatedString = remember(defaultColor, linkColor, onLearnMoreClick) {
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(defaultColor)) {
|
||||
append(fullString.substring(0, linkTextPosition))
|
||||
}
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "learn_more",
|
||||
linkInteractionListener = { onLearnMoreClick() },
|
||||
),
|
||||
block = {
|
||||
withStyle(SpanStyle(color = linkColor, textDecoration = TextDecoration.None)) {
|
||||
append(
|
||||
fullString.substring(linkTextPosition, fullString.length).replace(
|
||||
' ',
|
||||
StringsSigns.NON_BREAKING_SPACE,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier.padding(horizontal = 32.dp),
|
||||
text = annotatedString,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TokenListContent(state: FeeTokenSelectorUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
state.tokens.fastForEach { item ->
|
||||
key(item.id) {
|
||||
TokenItem(
|
||||
modifier = Modifier
|
||||
.selectedBorder(isSelected = state.selectedToken.id == item.id)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.action),
|
||||
state = item,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
val cryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = CryptoCurrency.Coin(
|
||||
id = CryptoCurrency.ID.fromValue("coin⟨BITCOIN⟩bitcoin"),
|
||||
network = Network(
|
||||
id = Network.ID(
|
||||
value = "bitcoin",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
),
|
||||
backendId = "bitcoin",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.Unspecified("bitcoin"),
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
nameResolvingType = Network.NameResolvingType.NONE,
|
||||
),
|
||||
name = "Bitcoin",
|
||||
symbol = "BTC",
|
||||
decimals = 8,
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/medium/bitcoin.png",
|
||||
isCustom = false,
|
||||
),
|
||||
value = CryptoCurrencyStatus.Loading,
|
||||
)
|
||||
|
||||
val parentUM = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Market(
|
||||
fee = Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.02"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Slow(
|
||||
fee = Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.01"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = true,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal.TEN,
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
feeNonce = FeeNonce.None,
|
||||
fees = TransactionFee.Single(
|
||||
Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.01"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val tokenItemState = TokenItemState.Content(
|
||||
id = "1",
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Empty,
|
||||
subtitle2State = null,
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference("Balance: 0,35853044 BTC"),
|
||||
isAvailable = false,
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
||||
val state = FeeTokenSelectorUM(
|
||||
parent = parentUM,
|
||||
selectedToken = tokenItemState,
|
||||
tokens = persistentListOf(tokenItemState, tokenItemState.copy(id = "2")),
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
FeeTokenSelectorContent(
|
||||
state = state,
|
||||
intents = StubFeeTokenSelectorIntents(),
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.features.send.v2.feeselector.component.extended.model.FeeExtendedSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel
|
||||
import com.tangem.features.send.v2.feeselector.component.speed.model.FeeSpeedSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.component.token.model.FeeTokenSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -33,5 +34,10 @@ internal interface FeeSelectorModelModule {
|
|||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeeSpeedSelectorModel::class)
|
||||
fun bindFeeSelectorSpeedModel(model: FeeSpeedSelectorModel): Model
|
||||
fun bindFeeSpeedSelectorModel(model: FeeSpeedSelectorModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeeTokenSelectorModel::class)
|
||||
fun bindFeeTokenSelectorModel(model: FeeTokenSelectorModel): Model
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.features.send.v2.feeselector.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
|
||||
@Stable
|
||||
internal interface FeeSelectorIntents {
|
||||
fun onFeeItemSelected(feeItem: FeeItem)
|
||||
fun onTokenSelected(status: CryptoCurrencyStatus)
|
||||
fun onCustomFeeValueChange(index: Int, value: String)
|
||||
fun onNonceChange(value: String)
|
||||
fun onDoneClick()
|
||||
|
|
@ -13,6 +15,7 @@ internal interface FeeSelectorIntents {
|
|||
|
||||
internal class StubFeeSelectorIntents : FeeSelectorIntents {
|
||||
override fun onFeeItemSelected(feeItem: FeeItem) {}
|
||||
override fun onTokenSelected(status: CryptoCurrencyStatus) {}
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {}
|
||||
override fun onNonceChange(value: String) {}
|
||||
override fun onDoneClick() {}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.v2.feeselector.model
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -12,6 +13,8 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
|||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
|
|
@ -32,6 +35,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -47,6 +51,8 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getAvailableFeeTokensUseCase: GetAvailableFeeTokensUseCase,
|
||||
) : FeeSelectorIntents {
|
||||
|
||||
private var appCurrency: AppCurrency = AppCurrency.Default
|
||||
|
|
@ -105,6 +111,20 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
uiState.update(FeeItemSelectedTransformer(feeItem))
|
||||
}
|
||||
|
||||
override fun onTokenSelected(status: CryptoCurrencyStatus) {
|
||||
uiState.update { state ->
|
||||
if (state is FeeSelectorUM.Content) {
|
||||
state.copy(
|
||||
feeExtraInfo = state.feeExtraInfo.copy(
|
||||
feeCryptoCurrencyStatus = status,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState.update(
|
||||
FeeSelectorCustomValueChangedTransformer(
|
||||
|
|
@ -208,20 +228,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
}
|
||||
},
|
||||
ifRight = { fee ->
|
||||
Either.Right(
|
||||
LoadedFeeResult.Extended(
|
||||
fee = fee,
|
||||
selectedToken = if (params.feeCryptoCurrencyStatus.currency.id != fee.feeTokenId) {
|
||||
getSingleCryptoCurrencyStatusUseCase
|
||||
.invokeMultiWalletSync(
|
||||
userWalletId = TODO(),
|
||||
cryptoCurrencyId = fee.feeTokenId,
|
||||
).getOrNull()
|
||||
} else {
|
||||
params.feeCryptoCurrencyStatus
|
||||
},
|
||||
),
|
||||
)
|
||||
populateExtendedFee(fee)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
|
@ -229,10 +236,42 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun populateExtendedFee(
|
||||
fee: TransactionFeeExtended,
|
||||
): Either<GetFeeError, LoadedFeeResult.Extended> = either {
|
||||
val selectedToken = if (params.feeCryptoCurrencyStatus.currency.id != fee.feeTokenId) {
|
||||
getSingleCryptoCurrencyStatusUseCase
|
||||
.invokeMultiWalletSync(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyId = fee.feeTokenId,
|
||||
).getOrElse {
|
||||
raise(GetFeeError.DataError(IllegalStateException("No token found for id: ${fee.feeTokenId}")))
|
||||
}
|
||||
} else {
|
||||
params.feeCryptoCurrencyStatus
|
||||
}
|
||||
|
||||
val userWallet = getUserWalletUseCase(params.userWalletId).mapLeft {
|
||||
GetFeeError.DataError(IllegalStateException("No wallet found for id: ${params.userWalletId}"))
|
||||
}.bind()
|
||||
|
||||
val availableTokens = getAvailableFeeTokensUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
).bind()
|
||||
|
||||
LoadedFeeResult.Extended(
|
||||
fee = fee,
|
||||
selectedToken = selectedToken,
|
||||
availableTokens = availableTokens,
|
||||
)
|
||||
}
|
||||
|
||||
sealed class LoadedFeeResult {
|
||||
data class Extended(
|
||||
val fee: TransactionFeeExtended,
|
||||
val selectedToken: CryptoCurrencyStatus?,
|
||||
val availableTokens: List<CryptoCurrencyStatus>,
|
||||
) : LoadedFeeResult()
|
||||
|
||||
data class Basic(val fee: TransactionFee) : LoadedFeeResult()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.utils.stack
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
|
|
@ -24,6 +26,7 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
) : Model(), FeeSelectorIntents {
|
||||
|
||||
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorDetailsParams>()
|
||||
|
|
@ -54,6 +57,11 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onTokenSelected(status: CryptoCurrencyStatus) {
|
||||
feeSelectorLogic.onTokenSelected(status)
|
||||
stackNavigation.pop()
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
feeSelectorLogic.onCustomFeeValueChange(index, value)
|
||||
}
|
||||
|
|
@ -85,7 +93,10 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
return FeeSelectorRoute.ChooseSpeed
|
||||
}
|
||||
|
||||
// TODO: Add logic to choose initial route based on other conditions
|
||||
return FeeSelectorRoute.NetworkFee
|
||||
return if (isGaslessFeeSupportedForNetwork(params.feeCryptoCurrencyStatus.currency.network)) {
|
||||
FeeSelectorRoute.NetworkFee
|
||||
} else {
|
||||
FeeSelectorRoute.ChooseSpeed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.features.send.v2.feeselector.model.FeeSelectorLogic.LoadedFeeR
|
|||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeeSelectorLoadedTransformer(
|
||||
|
|
@ -61,6 +62,7 @@ internal class FeeSelectorLoadedTransformer(
|
|||
isTronToken = cryptoCurrencyStatus.currency is CryptoCurrency.Token &&
|
||||
isTron(cryptoCurrencyStatus.currency.network.rawId),
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
availableFeeCurrencies = getAvailableFeeCurrencies(),
|
||||
),
|
||||
feeFiatRateUM = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
|
||||
FeeFiatRateUM(
|
||||
|
|
@ -78,4 +80,9 @@ internal class FeeSelectorLoadedTransformer(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAvailableFeeCurrencies(): ImmutableList<CryptoCurrencyStatus>? {
|
||||
if (fees !is LoadedFeeResult.Extended) return null
|
||||
return fees.availableTokens.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +92,7 @@ internal class SendConfirmComponent(
|
|||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
feeDisplaySource = FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ internal fun getWcCommonScreen(
|
|||
feeDisplaySource = FeeDisplaySource.BottomSheet,
|
||||
analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.WalletConnect,
|
||||
userWalletId = model.userWallet.walletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ internal class WcSendTransactionComponent(
|
|||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet,
|
||||
analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.WalletConnect,
|
||||
userWalletId = model.userWallet.walletId,
|
||||
),
|
||||
onResult = model::updateFee,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ internal class WcSendTransactionModel @Inject constructor(
|
|||
private val securityStatusState =
|
||||
MutableStateFlow<Lce<Throwable, BlockAidTransactionCheck.Result>>(Lce.Loading(partialContent = null))
|
||||
|
||||
val userWallet
|
||||
get() = useCase.wallet
|
||||
|
||||
init {
|
||||
@Suppress("UnusedPrivateMember")
|
||||
modelScope.launch {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue