Updated on 2026-08-14
This commit is contained in:
commit
7e48e67354
942 changed files with 40912 additions and 7544 deletions
|
|
@ -1,5 +1,12 @@
|
|||
package com.tangem.features.send.v2
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultSendFeatureToggles : SendFeatureToggles
|
||||
internal class DefaultSendFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : SendFeatureToggles {
|
||||
override val isGaslessTransactionsEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("GASLESS_TRANSACTIONS_ENABLED")
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
|||
import com.tangem.features.send.v2.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun FeeBlock(feeSelectorUM: FeeSelectorUM) {
|
||||
fun FeeBlockSuccess(feeSelectorUM: FeeSelectorUM) {
|
||||
if (feeSelectorUM !is FeeSelectorUM.Content) return
|
||||
val feeExtraInfo = feeSelectorUM.feeExtraInfo
|
||||
val feeFiatRateUM = feeSelectorUM.feeFiatRateUM
|
||||
|
|
@ -8,25 +8,20 @@ import com.tangem.features.send.v2.sendnft.DefaultNFTSendComponent
|
|||
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
|
||||
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 SendFeatureModule {
|
||||
|
||||
@Provides
|
||||
fun provideSendFeatureToggles(): SendFeatureToggles {
|
||||
return DefaultSendFeatureToggles()
|
||||
}
|
||||
}
|
||||
internal object SendFeatureModule
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface SendFeatureModuleBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSendFeatureToggles(impl: DefaultSendFeatureToggles): SendFeatureToggles
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideSendComponentFactory(impl: DefaultSendComponent.Factory): SendComponent.Factory
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ import com.tangem.core.decompose.model.getOrCreateModel
|
|||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.FeeSelectorComponent
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel
|
||||
import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -31,9 +32,10 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
@Assisted private val params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
@Assisted onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
private val feeSelectorComponentFactory: FeeSelectorComponent.Factory,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: FeeSelectorModel = getOrCreateModel(params = params)
|
||||
private val model: FeeSelectorBlockModel = getOrCreateModel(params = params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.feeSelectorBottomSheet,
|
||||
|
|
@ -45,6 +47,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
params = FeeSelectorParams.FeeSelectorDetailsParams(
|
||||
state = model.uiState.value,
|
||||
onLoadFee = params.onLoadFee,
|
||||
onLoadFeeExtended = params.onLoadFeeExtended,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
callback = model,
|
||||
|
|
@ -52,6 +55,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWalletId,
|
||||
),
|
||||
onDismiss = {
|
||||
model.feeSelectorBottomSheet.dismiss()
|
||||
|
|
@ -61,6 +65,10 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
bottomSheetSlot.subscribe {
|
||||
params.bottomSheetShown(it.child != null)
|
||||
}
|
||||
|
||||
model.uiState
|
||||
.onEach { onResult(it) }
|
||||
.launchIn(componentScope)
|
||||
|
|
@ -77,11 +85,14 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
|
||||
val isScreenSource = params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen
|
||||
val isNotSingleFee = (state as? FeeSelectorUM.Content)?.feeItems?.isSingleItem() == false
|
||||
val isGaslessAvailable = state is FeeSelectorUM.Content &&
|
||||
(state as FeeSelectorUM.Content).feeExtraInfo.transactionFeeExtended != null
|
||||
FeeSelectorBlockContent(
|
||||
state = state,
|
||||
onReadMoreClick = model::onReadMoreClicked,
|
||||
isGaslessFeatureEnabled = sendFeatureToggles.isGaslessTransactionsEnabled,
|
||||
modifier = modifier
|
||||
.conditional(isScreenSource && isNotSingleFee) {
|
||||
.conditional(isScreenSource && (isNotSingleFee || isGaslessAvailable)) {
|
||||
Modifier.clickable {
|
||||
model.showFeeSelector()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,23 @@ package com.tangem.features.send.v2.feeselector
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.api.FeeSelectorComponent
|
||||
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
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -17,10 +29,33 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
|||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
@Assisted private val onDismiss: () -> Unit,
|
||||
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)
|
||||
|
||||
val innerRouter = InnerRouter<FeeSelectorRoute>(
|
||||
stackNavigation = model.stackNavigation,
|
||||
popCallback = {
|
||||
model.stackNavigation.pop { success ->
|
||||
if (!success) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val contentStack = childStack(
|
||||
key = "FeeSelectorDetailsStack",
|
||||
source = model.stackNavigation,
|
||||
serializer = FeeSelectorRoute.serializer(),
|
||||
initialConfiguration = model.getInitialRoute(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::screenChild,
|
||||
)
|
||||
|
||||
override fun dismiss() {
|
||||
onDismiss()
|
||||
}
|
||||
|
|
@ -28,14 +63,46 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val contentStack by contentStack.subscribeAsState()
|
||||
|
||||
FeeSelectorModalBottomSheet(
|
||||
onDismiss = ::dismiss,
|
||||
childStack = contentStack,
|
||||
state = state,
|
||||
feeSelectorIntents = model,
|
||||
feeDisplaySource = params.feeDisplaySource,
|
||||
onDismiss = onDismiss,
|
||||
onBack = innerRouter::pop,
|
||||
)
|
||||
}
|
||||
|
||||
private fun screenChild(config: FeeSelectorRoute, componentContext: ComponentContext): ComposableContentComponent {
|
||||
val appComponentContext = childByContext(
|
||||
componentContext = componentContext,
|
||||
router = innerRouter,
|
||||
)
|
||||
|
||||
val componentParams = FeeSelectorComponentParams(
|
||||
parentParams = params,
|
||||
state = model.uiState,
|
||||
intents = model,
|
||||
)
|
||||
|
||||
return when (config) {
|
||||
FeeSelectorRoute.NetworkFee -> feeExtendedSelectorComponentFactory.create(
|
||||
appComponentContext = appComponentContext,
|
||||
params = componentParams,
|
||||
)
|
||||
FeeSelectorRoute.ChooseSpeed -> feeSpeedSelectorComponentFactory.create(
|
||||
appComponentContext = appComponentContext,
|
||||
params = componentParams,
|
||||
)
|
||||
FeeSelectorRoute.ChooseToken -> feeTokenSelectorComponentFactory.create(
|
||||
appComponentContext = appComponentContext,
|
||||
params = componentParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : FeeSelectorComponent.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.feeselector.component
|
||||
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class FeeSelectorComponentParams(
|
||||
val parentParams: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
val state: MutableStateFlow<FeeSelectorUM>,
|
||||
val intents: FeeSelectorIntents,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.extended
|
||||
|
||||
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.extended.model.FeeExtendedSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.component.extended.ui.FeeExtendedSelectorContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class FeeExtendedSelectorComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: FeeSelectorComponentParams,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: FeeExtendedSelectorModel = getOrCreateModel(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
FeeExtendedSelectorContent(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: FeeSelectorComponentParams,
|
||||
): FeeExtendedSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.extended.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
|
||||
@Immutable
|
||||
data class FeeExtendedSelectorUM(
|
||||
val parent: FeeSelectorUM.Content,
|
||||
val token: TokenItemState,
|
||||
val fee: FeeItem,
|
||||
val onFeeClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.extended.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
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.navigation.Router
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
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.route.FeeSelectorRoute
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
class FeeExtendedSelectorModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val router: Router,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<FeeSelectorComponentParams>()
|
||||
private var appCurrency: AppCurrency = AppCurrency.Default
|
||||
|
||||
init {
|
||||
initAppCurrency()
|
||||
}
|
||||
|
||||
val uiState: StateFlow<FeeExtendedSelectorUM>
|
||||
field = MutableStateFlow<FeeExtendedSelectorUM>(getInitialState())
|
||||
|
||||
init {
|
||||
params.state
|
||||
.filterIsInstance<FeeSelectorUM.Content>()
|
||||
.onEach { state -> uiState.value = convertState(state) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun getInitialState(): FeeExtendedSelectorUM {
|
||||
val parentContentState = params.state.value as FeeSelectorUM.Content
|
||||
return convertState(parentContentState)
|
||||
}
|
||||
|
||||
private fun initAppCurrency() {
|
||||
modelScope.launch {
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertState(state: FeeSelectorUM.Content): FeeExtendedSelectorUM {
|
||||
val tokenConverter = SelectedTokenItemConverter(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
onTokenClick = { router.push(FeeSelectorRoute.ChooseToken) },
|
||||
isTokenSelectionAvailable = state.feeExtraInfo.availableFeeCurrencies.orEmpty().size > 1,
|
||||
)
|
||||
|
||||
return FeeExtendedSelectorUM(
|
||||
parent = state,
|
||||
token = tokenConverter.convert(state.feeExtraInfo.feeCryptoCurrencyStatus),
|
||||
fee = state.selectedFeeItem,
|
||||
onFeeClick = { router.push(FeeSelectorRoute.ChooseSpeed) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
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
|
||||
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 SelectedTokenItemConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val onTokenClick: () -> Unit,
|
||||
private val isTokenSelectionAvailable: Boolean,
|
||||
) : 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 = if (isTokenSelectionAvailable) {
|
||||
{ _, _ -> onTokenClick() }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
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 = if (isTokenSelectionAvailable) {
|
||||
TokenItemState.FiatAmountState.Icon(
|
||||
iconRes = R.drawable.ic_select_18_24,
|
||||
tint = IconTint.Informative,
|
||||
)
|
||||
} else {
|
||||
TokenItemState.FiatAmountState.Empty
|
||||
},
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = resourceReference(
|
||||
R.string.common_balance,
|
||||
wrappedList(balance),
|
||||
),
|
||||
isAvailable = false,
|
||||
),
|
||||
subtitle2State = null,
|
||||
)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.extended.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.icons.IconTint
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
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.extended.entity.FeeExtendedSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.component.speed.ui.RegularFeeItemContent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
fun FeeExtendedSelectorContent(state: FeeExtendedSelectorUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
bottom = 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
TokenItem(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.action),
|
||||
state = state.token,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
SpacerH8()
|
||||
|
||||
val feeFiatRateUM = state.parent.feeFiatRateUM
|
||||
val fee = state.fee.fee
|
||||
val isChooseSpeedAvailable = state.parent.feeItems.size > 1
|
||||
|
||||
RegularFeeItemContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.conditional(isChooseSpeedAvailable) {
|
||||
clickable(onClick = state.onFeeClick)
|
||||
}
|
||||
.background(
|
||||
TangemTheme.colors.background.action,
|
||||
RoundedCornerShape(TangemTheme.dimens.radius14),
|
||||
),
|
||||
title = state.fee.title,
|
||||
iconRes = state.fee.iconRes,
|
||||
iconBackgroundColor = TangemTheme.colors.icon.accent.copy(alpha = 0.1F),
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
preDot = stringReference(
|
||||
fee.amount.value.format {
|
||||
crypto(
|
||||
symbol = fee.amount.currencySymbol,
|
||||
decimals = fee.amount.decimals,
|
||||
).fee(canBeLower = state.parent.feeExtraInfo.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (state.parent.feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) {
|
||||
getFiatReference(
|
||||
value = fee.amount.value,
|
||||
rate = feeFiatRateUM.rate,
|
||||
appCurrency = feeFiatRateUM.appCurrency,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
ellipsizeOffset = fee.amount.currencySymbol.length,
|
||||
showSelectorIcon = isChooseSpeedAvailable,
|
||||
isNotEnoughFunds = state.parent.feeExtraInfo.isNotEnoughFunds,
|
||||
isLoading = state.fee.isLoading(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
val tokenItemState = TokenItemState.Content(
|
||||
id = "1",
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Icon(
|
||||
R.drawable.ic_select_18_24,
|
||||
tint = IconTint.Informative,
|
||||
),
|
||||
subtitle2State = null,
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference("Balance: 0,35853044 BTC"),
|
||||
isAvailable = false,
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
||||
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 lowFeeItem =
|
||||
FeeItem.Market(Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)))
|
||||
|
||||
TangemThemePreview {
|
||||
FeeExtendedSelectorContent(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
|
||||
state = FeeExtendedSelectorUM(
|
||||
parent = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
feeItems = persistentListOf(lowFeeItem),
|
||||
selectedFeeItem = lowFeeItem,
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
isNotEnoughFunds = true,
|
||||
),
|
||||
feeNonce = FeeNonce.None,
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("2500"),
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
fees = TransactionFee.Single(lowFeeItem.fee),
|
||||
),
|
||||
token = tokenItemState,
|
||||
fee = FeeItem.Market(
|
||||
fee = Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.02"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFeeClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.speed
|
||||
|
||||
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.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.v2.feeselector.component.speed.model.FeeSpeedSelectorModel
|
||||
import com.tangem.features.send.v2.feeselector.component.speed.ui.FeeSpeedSelectorContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class FeeSpeedSelectorComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: FeeSelectorComponentParams,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: FeeSpeedSelectorModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state is FeeSelectorUM.Content) {
|
||||
FeeSpeedSelectorContent(
|
||||
state = state as FeeSelectorUM.Content,
|
||||
intents = model,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: FeeSelectorComponentParams,
|
||||
): FeeSpeedSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.speed
|
||||
|
||||
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 FeeSpeedSelectorIntents : FeeSelectorIntents {
|
||||
fun onLearnMoreClick()
|
||||
}
|
||||
|
||||
@Stable
|
||||
internal class StubFeeSpeedSelectorIntents : FeeSpeedSelectorIntents, FeeSelectorIntents by StubFeeSelectorIntents() {
|
||||
override fun onLearnMoreClick() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.speed.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
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.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.v2.feeselector.component.speed.FeeSpeedSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class FeeSpeedSelectorModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model(), FeeSpeedSelectorIntents,
|
||||
FeeSelectorIntents by paramsContainer.require<FeeSelectorComponentParams>().intents {
|
||||
|
||||
private val params = paramsContainer.require<FeeSelectorComponentParams>()
|
||||
|
||||
val uiState: StateFlow<FeeSelectorUM>
|
||||
field = params.state
|
||||
|
||||
override fun onLearnMoreClick() {
|
||||
modelScope.launch {
|
||||
urlOpener.openUrl(
|
||||
TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,578 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.speed.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
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.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnter
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmountV2
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
|
||||
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.speed.FeeSpeedSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.component.speed.StubFeeSpeedSelectorIntents
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
internal fun FeeSpeedSelectorContent(
|
||||
state: FeeSelectorUM.Content,
|
||||
intents: FeeSpeedSelectorIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(
|
||||
bottom = 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Title(onLearnMoreClick = intents::onLearnMoreClick)
|
||||
SpacerH8()
|
||||
FeeSelectorItems(
|
||||
state = state,
|
||||
intents = intents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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_speed_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
|
||||
private fun FeeSelectorItems(
|
||||
state: FeeSelectorUM.Content,
|
||||
intents: FeeSpeedSelectorIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
val feeFiatRateUM = state.feeFiatRateUM
|
||||
state.feeItems.fastForEachIndexed { index, item ->
|
||||
val isSelected = item.isSameClass(state.selectedFeeItem)
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary,
|
||||
label = "Fee selector icon tint change",
|
||||
)
|
||||
val iconBackgroundColor by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
TangemTheme.colors.icon.accent.copy(alpha = 0.1F)
|
||||
} else {
|
||||
TangemTheme.colors.background.secondary
|
||||
},
|
||||
label = "Fee selector icon background change",
|
||||
)
|
||||
val itemModifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.selectedBorder(isSelected = isSelected)
|
||||
.clickableSingle(onClick = { intents.onFeeItemSelected(item) })
|
||||
.background(TangemTheme.colors.background.action)
|
||||
when (item) {
|
||||
is FeeItem.Custom -> CustomFeeBlock(
|
||||
modifier = itemModifier,
|
||||
customFee = item,
|
||||
isSelected = isSelected,
|
||||
iconBackgroundColor = iconBackgroundColor,
|
||||
iconTint = iconTint,
|
||||
onValueChange = intents::onCustomFeeValueChange,
|
||||
nonce = state.feeNonce,
|
||||
)
|
||||
else -> RegularFeeItemContent(
|
||||
modifier = itemModifier,
|
||||
title = item.title,
|
||||
iconRes = item.iconRes,
|
||||
iconBackgroundColor = iconBackgroundColor,
|
||||
iconTint = iconTint,
|
||||
preDot = stringReference(
|
||||
item.fee.amount.value.format {
|
||||
crypto(
|
||||
symbol = item.fee.amount.currencySymbol,
|
||||
decimals = item.fee.amount.decimals,
|
||||
).fee(canBeLower = state.feeExtraInfo.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (state.feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) {
|
||||
getFiatReference(
|
||||
value = item.fee.amount.value,
|
||||
rate = feeFiatRateUM.rate,
|
||||
appCurrency = feeFiatRateUM.appCurrency,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
ellipsizeOffset = item.fee.amount.currencySymbol.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun CustomFeeBlock(
|
||||
customFee: FeeItem.Custom,
|
||||
isSelected: Boolean,
|
||||
iconBackgroundColor: Color,
|
||||
iconTint: Color,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
nonce: FeeNonce,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val textHeight = with(LocalDensity.current) {
|
||||
textMeasurer.measure("S", style = TangemTheme.typography.subtitle2).size.height.toDp() +
|
||||
textMeasurer.measure("S", style = TangemTheme.typography.caption1).size.height.toDp() + 2.dp
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 15.dp)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_FEE_ITEM),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(color = iconBackgroundColor, shape = CircleShape)
|
||||
.padding(6.dp)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_ICON),
|
||||
painter = painterResource(R.drawable.ic_edit_v2_24),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
)
|
||||
Box(Modifier.heightIn(min = textHeight)) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_custom),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_TITLE),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isSelected,
|
||||
label = "Custom Fee Selected Animation",
|
||||
enter = expandVertically().plus(fadeIn()),
|
||||
exit = shrinkVertically().plus(fadeOut()),
|
||||
) {
|
||||
ExpandedCustomFeeItems(
|
||||
customFeeFields = customFee.customValues,
|
||||
onValueChange = onValueChange,
|
||||
nonce = nonce,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandedCustomFeeItems(
|
||||
customFeeFields: ImmutableList<CustomFeeFieldUM>,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
nonce: FeeNonce,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
customFeeFields.fastForEachIndexed { index, field ->
|
||||
val isShowDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce
|
||||
if (field.label != null) {
|
||||
InputRowEnterInfoAmountV2(
|
||||
text = field.value,
|
||||
decimals = field.decimals,
|
||||
symbol = field.symbol,
|
||||
title = field.title,
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
info = field.label,
|
||||
description = field.footer,
|
||||
keyboardOptions = field.keyboardOptions,
|
||||
keyboardActions = field.keyboardActions,
|
||||
onValueChange = { onValueChange(index, it) },
|
||||
showDivider = isShowDivider,
|
||||
isReadOnly = field.isReadonly,
|
||||
)
|
||||
} else {
|
||||
InputRowEnterInfoAmountV2(
|
||||
text = field.value,
|
||||
decimals = field.decimals,
|
||||
title = field.title,
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
symbol = field.symbol,
|
||||
description = field.footer,
|
||||
onValueChange = { onValueChange(index, it) },
|
||||
keyboardOptions = field.keyboardOptions,
|
||||
keyboardActions = field.keyboardActions,
|
||||
showDivider = isShowDivider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (nonce is FeeNonce.Nonce) {
|
||||
InputRowEnter(
|
||||
text = nonce.nonce?.toString().orEmpty(),
|
||||
title = resourceReference(R.string.send_nonce),
|
||||
description = resourceReference(R.string.send_nonce_footer),
|
||||
onValueChange = nonce.onNonceChange,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
placeholder = resourceReference(R.string.send_nonce_hint),
|
||||
titleColor = TangemTheme.colors.text.secondary,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_ITEM),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun RegularFeeItemContent(
|
||||
title: TextReference,
|
||||
@DrawableRes iconRes: Int,
|
||||
iconBackgroundColor: Color,
|
||||
iconTint: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
preDot: TextReference? = null,
|
||||
postDot: TextReference? = null,
|
||||
ellipsizeOffset: Int? = null,
|
||||
showSelectorIcon: Boolean = false,
|
||||
isLoading: Boolean = false,
|
||||
isNotEnoughFunds: Boolean = false,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 15.dp)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_FEE_ITEM),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(color = iconBackgroundColor, shape = CircleShape)
|
||||
.padding(6.dp)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_ICON),
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
)
|
||||
FeeDescription(
|
||||
title = title,
|
||||
preDot = preDot,
|
||||
postDot = postDot,
|
||||
ellipsizeOffset = ellipsizeOffset,
|
||||
isLoading = isLoading,
|
||||
isNotEnoughFunds = isNotEnoughFunds,
|
||||
)
|
||||
|
||||
if (showSelectorIcon) {
|
||||
SpacerWMax()
|
||||
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_select_18_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeDescription(
|
||||
title: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
preDot: TextReference? = null,
|
||||
postDot: TextReference? = null,
|
||||
ellipsizeOffset: Int? = null,
|
||||
isLoading: Boolean = false,
|
||||
isNotEnoughFunds: Boolean = false,
|
||||
) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE),
|
||||
)
|
||||
when {
|
||||
isNotEnoughFunds -> Text(
|
||||
text = stringResourceSafe(R.string.gasless_not_enough_funds_to_cover_token_fee),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
isLoading -> TextShimmer(
|
||||
modifier = Modifier.width(122.dp),
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
preDot != null -> FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeValueContent(preDot: TextReference, postDot: TextReference?, ellipsizeOffset: Int? = null) {
|
||||
val ellipsis = if (ellipsizeOffset == null) {
|
||||
TextEllipsis.End
|
||||
} else {
|
||||
TextEllipsis.OffsetEnd(ellipsizeOffset)
|
||||
}
|
||||
val textColor = TangemTheme.colors.text.tertiary
|
||||
val textStyle = TangemTheme.typography.caption1
|
||||
Row {
|
||||
EllipsisText(
|
||||
text = preDot.resolveReference(),
|
||||
style = textStyle,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.End,
|
||||
ellipsis = ellipsis,
|
||||
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.TOKEN_AMOUNT),
|
||||
)
|
||||
if (postDot != null) {
|
||||
Text(
|
||||
text = StringsSigns.DOT,
|
||||
style = textStyle,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing4)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.DOT_SIGN),
|
||||
)
|
||||
Text(
|
||||
text = postDot.resolveReference(),
|
||||
style = textStyle,
|
||||
color = textColor,
|
||||
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.FIAT_AMOUNT),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview(
|
||||
@PreviewParameter(FeeSpeedSelectorUMContentProvider::class)
|
||||
state: FeeSelectorUM.Content,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
FeeSpeedSelectorContent(
|
||||
state = state,
|
||||
intents = StubFeeSpeedSelectorIntents(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FeeSpeedSelectorUMContentProvider : CollectionPreviewParameterProvider<FeeSelectorUM.Content>(
|
||||
collection = listOf(
|
||||
FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Suggested(
|
||||
title = resourceReference(
|
||||
id = R.string.wc_fee_suggested,
|
||||
formatArgs = wrappedList("Tangem"),
|
||||
),
|
||||
fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)),
|
||||
),
|
||||
FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))),
|
||||
FeeItem.Market(
|
||||
fee = Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.02"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))),
|
||||
customFeeItem,
|
||||
),
|
||||
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(customFeeItem.fee),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val cryptoCurrencyStatus
|
||||
get() = 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,
|
||||
)
|
||||
|
||||
private val customFeeItem
|
||||
get() = FeeItem.Custom(
|
||||
fee = Fee.Common(Amount(value = BigDecimal("0.05"), blockchain = Blockchain.Ethereum)),
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.119806",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
keyboardActions = KeyboardActions(),
|
||||
symbol = "ETH",
|
||||
decimals = 8,
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_amount_fee_footer),
|
||||
label = stringReference("~ 0,03 \$"),
|
||||
isReadonly = false,
|
||||
),
|
||||
CustomFeeFieldUM(
|
||||
value = "40000",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
keyboardActions = KeyboardActions(),
|
||||
symbol = "GWEI",
|
||||
decimals = 8,
|
||||
title = resourceReference(R.string.send_gas_price),
|
||||
footer = resourceReference(R.string.send_gas_price_footer),
|
||||
label = null,
|
||||
isReadonly = false,
|
||||
),
|
||||
CustomFeeFieldUM(
|
||||
value = "31400",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
keyboardActions = KeyboardActions(),
|
||||
symbol = null,
|
||||
decimals = 8,
|
||||
title = resourceReference(R.string.send_gas_limit),
|
||||
footer = resourceReference(R.string.send_gas_limit_footer),
|
||||
label = null,
|
||||
isReadonly = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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,8 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token.entity
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
|
||||
internal data class FeeTokenItemState(
|
||||
val state: TokenItemState,
|
||||
val isAvailable: Boolean = true,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.send.v2.feeselector.component.token.entity
|
||||
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class FeeTokenSelectorUM(
|
||||
val parent: FeeSelectorUM.Content,
|
||||
val selectedToken: FeeTokenItemState,
|
||||
val tokens: ImmutableList<FeeTokenItemState>,
|
||||
)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
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.feeselector.component.token.entity.FeeTokenItemState
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
|
||||
internal class FeeTokenForListConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val onTokenClick: () -> Unit,
|
||||
) : Converter<CryptoCurrencyStatus, FeeTokenItemState> {
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): FeeTokenItemState {
|
||||
if (value.value !is CryptoCurrencyStatus.Loaded) {
|
||||
return FeeTokenItemState(TokenItemState.Loading(id = value.currency.id.value))
|
||||
}
|
||||
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val isBalanceZero = value.value.amount?.isZero() == true
|
||||
val tokenItemConverter = TokenItemStateConverter(
|
||||
appCurrency = appCurrencyProvider(),
|
||||
onItemClick = if (isBalanceZero.not()) {
|
||||
{ _, _ -> onTokenClick() }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
val state = tokenItemConverter.convert(value)
|
||||
|
||||
val updatedState = 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
|
||||
}
|
||||
|
||||
return FeeTokenItemState(
|
||||
state = updatedState,
|
||||
isAvailable = isBalanceZero.not(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
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.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.FeeTokenItemState
|
||||
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.filterIsInstance
|
||||
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
|
||||
.filterIsInstance<FeeSelectorUM.Content>()
|
||||
.onEach { state -> uiState.value = stateFromParent(state) }
|
||||
.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): FeeTokenItemState {
|
||||
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,270 @@
|
|||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
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.FeeTokenItemState
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
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,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
internal fun TokenListContent(state: FeeTokenSelectorUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val (availableTokens, notAvailableTokens) = remember(state.tokens) {
|
||||
state.tokens.partition { it.isAvailable }.let { pair ->
|
||||
Pair(
|
||||
first = pair.first.map { it.state }.toImmutableList(),
|
||||
second = pair.second.map { it.state }.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
availableTokens.fastForEach { item ->
|
||||
key(item.id) {
|
||||
TokenItem(
|
||||
modifier = Modifier
|
||||
.selectedBorder(isSelected = state.selectedToken.state.id == item.id)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.action),
|
||||
state = item,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (notAvailableTokens.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 12.dp, top = 8.dp),
|
||||
text = stringResourceSafe(R.string.common_not_available),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
notAvailableTokens.fastForEach { item ->
|
||||
key(item.id) {
|
||||
TokenItem(
|
||||
modifier = Modifier
|
||||
.graphicsLayer { alpha = 0.5f }
|
||||
.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 feeTokenItemState = FeeTokenItemState(tokenItemState)
|
||||
|
||||
val state = FeeTokenSelectorUM(
|
||||
parent = parentUM,
|
||||
selectedToken = feeTokenItemState,
|
||||
tokens = persistentListOf(
|
||||
feeTokenItemState,
|
||||
feeTokenItemState.copy(state = tokenItemState.copy(id = "2")),
|
||||
feeTokenItemState.copy(state = tokenItemState.copy(id = "3"), isAvailable = false),
|
||||
feeTokenItemState.copy(state = tokenItemState.copy(id = "4"), isAvailable = false),
|
||||
),
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
FeeTokenSelectorContent(
|
||||
state = state,
|
||||
intents = StubFeeTokenSelectorIntents(),
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,10 @@ package com.tangem.features.send.v2.feeselector.di
|
|||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
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
|
||||
|
|
@ -16,4 +20,24 @@ internal interface FeeSelectorModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(FeeSelectorModel::class)
|
||||
fun bindModel(model: FeeSelectorModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeeExtendedSelectorModel::class)
|
||||
fun bindExtendedModel(model: FeeExtendedSelectorModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeeSelectorBlockModel::class)
|
||||
fun bindBlockModel(model: FeeSelectorBlockModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeeSpeedSelectorModel::class)
|
||||
fun bindFeeSpeedSelectorModel(model: FeeSpeedSelectorModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeeTokenSelectorModel::class)
|
||||
fun bindFeeTokenSelectorModel(model: FeeTokenSelectorModel): Model
|
||||
}
|
||||
|
|
@ -92,6 +92,7 @@ internal class FeeSelectorAlertFactory @Inject constructor(
|
|||
is FeeItem.Slow -> newTransactionFee.minimum
|
||||
is FeeItem.Market -> newTransactionFee.normal
|
||||
is FeeItem.Fast -> newTransactionFee.priority
|
||||
FeeItem.Loading -> newTransactionFee.normal
|
||||
is FeeItem.Custom -> return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.features.send.v2.feeselector.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.TangemBlogUrlBuilder
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.domain.settings.NeverShowTapHelpUseCase
|
||||
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.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class FeeSelectorBlockModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model(), FeeSelectorModelCallback {
|
||||
|
||||
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorBlockParams>()
|
||||
private val feeSelectorLogic = feeSelectorLogicFactory.create(
|
||||
params = params,
|
||||
modelScope = modelScope,
|
||||
)
|
||||
|
||||
val feeSelectorBottomSheet = SlotNavigation<Unit>()
|
||||
val uiState: StateFlow<FeeSelectorUM>
|
||||
field = feeSelectorLogic.uiState
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
uiState.value = feeSelectorUM
|
||||
feeSelectorBottomSheet.dismiss()
|
||||
}
|
||||
|
||||
fun updateState(feeSelectorUM: FeeSelectorUM) {
|
||||
uiState.value = feeSelectorUM
|
||||
}
|
||||
|
||||
fun onReadMoreClicked() {
|
||||
modelScope.launch {
|
||||
urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee))
|
||||
}
|
||||
}
|
||||
|
||||
fun showFeeSelector() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.FeeScreenOpened(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = params.analyticsSendSource,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ScreenReopened(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Fee,
|
||||
),
|
||||
)
|
||||
modelScope.launch {
|
||||
neverShowTapHelpUseCase()
|
||||
feeSelectorBottomSheet.activate(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +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()
|
||||
|
|
@ -11,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() {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,322 @@
|
|||
package com.tangem.features.send.v2.feeselector.model
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.flatMap
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
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
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
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.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.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
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
|
||||
import com.tangem.features.send.v2.api.entity.FeeNonce
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter
|
||||
import com.tangem.features.send.v2.feeselector.model.transformers.*
|
||||
import com.tangem.utils.transformer.update
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeeSelectorLogic @AssistedInject constructor(
|
||||
@Assisted private val params: FeeSelectorParams,
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val feeSelectorReloadListener: FeeSelectorReloadListener,
|
||||
private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener,
|
||||
private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger,
|
||||
private val feeSelectorAlertFactory: FeeSelectorAlertFactory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getAvailableFeeTokensUseCase: GetAvailableFeeTokensUseCase,
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
) : FeeSelectorIntents {
|
||||
|
||||
private var appCurrency: AppCurrency = AppCurrency.Default
|
||||
val uiState = MutableStateFlow<FeeSelectorUM>(params.state)
|
||||
|
||||
val isGaslessEnabled = sendFeatureToggles.isGaslessTransactionsEnabled &&
|
||||
params.onLoadFeeExtended != null &&
|
||||
isGaslessFeeSupportedForNetwork(params.feeCryptoCurrencyStatus.currency.network) &&
|
||||
params.cryptoCurrencyStatus.currency is CryptoCurrency.Token
|
||||
|
||||
init {
|
||||
initAppCurrency()
|
||||
subscribeOnFeeReloadTriggerUpdates()
|
||||
subscribeOnFeeCheckReloadTriggerUpdates()
|
||||
subscribeOnFeeLoadingStateTriggerUpdates()
|
||||
loadFee()
|
||||
}
|
||||
|
||||
private fun initAppCurrency() {
|
||||
modelScope.launch {
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFee() {
|
||||
if (uiState.value is FeeSelectorUM.Error) {
|
||||
return
|
||||
}
|
||||
|
||||
if (uiState.value !is FeeSelectorUM.Content) {
|
||||
uiState.update(FeeSelectorLoadingTransformer)
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
callLoadFee()
|
||||
.fold(
|
||||
ifLeft = { error -> uiState.update(FeeSelectorErrorTransformer(error)) },
|
||||
ifRight = { fee ->
|
||||
uiState.update(
|
||||
FeeSelectorLoadedTransformer(
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = fee.selectedTokenOrNull ?: params.feeCryptoCurrencyStatus,
|
||||
appCurrency = appCurrency,
|
||||
fees = fee,
|
||||
feeStateConfiguration = params.feeStateConfiguration,
|
||||
isFeeApproximate = isFeeApproximate(fee.transactionFee.normal.amount.type),
|
||||
feeSelectorIntents = this@FeeSelectorLogic,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFeeApproximate(amountType: AmountType): Boolean {
|
||||
val networkId = params.feeCryptoCurrencyStatus.currency.network.id
|
||||
return isFeeApproximateUseCase(networkId = networkId, amountType = amountType)
|
||||
}
|
||||
|
||||
override fun onFeeItemSelected(feeItem: FeeItem) {
|
||||
if (feeItem is FeeItem.Custom) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = params.analyticsCategoryName),
|
||||
)
|
||||
}
|
||||
uiState.update(FeeItemSelectedTransformer(feeItem))
|
||||
}
|
||||
|
||||
override fun onTokenSelected(status: CryptoCurrencyStatus) {
|
||||
uiState.update(FeeSelectorTokenSelectedTransformer(status))
|
||||
loadFee()
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState.update(
|
||||
FeeSelectorCustomValueChangedTransformer(
|
||||
index = index,
|
||||
value = value,
|
||||
intents = this,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNonceChange(value: String) {
|
||||
uiState.update(FeeSelectorNonceChangeTransformer(value = value))
|
||||
}
|
||||
|
||||
override fun onDoneClick() {
|
||||
val feeSelectorUM = uiState.value as? FeeSelectorUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
CommonSendFeeAnalyticEvents.SelectedFee(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
feeType = feeSelectorUM.toAnalyticType(),
|
||||
source = params.analyticsSendSource,
|
||||
),
|
||||
)
|
||||
val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value
|
||||
if (feeSelectorUM.selectedFeeItem is FeeItem.Custom && isCustomFeeEdited) {
|
||||
analyticsEventHandler.send(GasPriceInserter(categoryName = params.analyticsCategoryName))
|
||||
}
|
||||
if (feeSelectorUM.feeNonce is FeeNonce.Nonce) {
|
||||
analyticsEventHandler.send(
|
||||
NonceInserted(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
token = params.feeCryptoCurrencyStatus.currency.symbol,
|
||||
blockchain = params.feeCryptoCurrencyStatus.currency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeReloadTriggerUpdates() {
|
||||
feeSelectorReloadListener.reloadTriggerFlow
|
||||
.onEach { data ->
|
||||
if (data.isRemoveSuggestedFee) {
|
||||
uiState.update(FeeSelectorRemoveSuggestedTransformer)
|
||||
}
|
||||
loadFee()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeLoadingStateTriggerUpdates() {
|
||||
feeSelectorReloadListener.loadingStateTriggerFlow
|
||||
.onEach { uiState.update(FeeSelectorLoadingTransformer) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeCheckReloadTriggerUpdates() {
|
||||
feeSelectorCheckReloadListener.checkReloadTriggerFlow
|
||||
.onEach { checkLoadFee() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun checkLoadFee() {
|
||||
modelScope.launch {
|
||||
callLoadFee().fold(
|
||||
ifRight = { newFee ->
|
||||
feeSelectorAlertFactory.getFeeUpdatedAlert(
|
||||
newTransactionFee = newFee.transactionFee,
|
||||
feeSelectorUM = uiState.value,
|
||||
proceedAction = {
|
||||
modelScope.launch {
|
||||
feeSelectorCheckReloadTrigger.callbackCheckResult(true)
|
||||
}
|
||||
},
|
||||
stopAction = {
|
||||
modelScope.launch {
|
||||
feeSelectorCheckReloadTrigger.callbackCheckResult(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
ifLeft = { feeError ->
|
||||
feeSelectorCheckReloadTrigger.callbackCheckResult(false)
|
||||
feeSelectorAlertFactory.getFeeUnreachableErrorState(::loadFee)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnreachableCode")
|
||||
private suspend fun callLoadFee(): Either<GetFeeError, LoadedFeeResult> {
|
||||
val extended = params.onLoadFeeExtended
|
||||
if (extended == null || !isGaslessEnabled) {
|
||||
return params.onLoadFee().map { LoadedFeeResult.Basic(it) }
|
||||
}
|
||||
|
||||
val selectedTokenOrNull = (uiState.value as? FeeSelectorUM.Content)?.feeExtraInfo?.feeCryptoCurrencyStatus
|
||||
|
||||
if (selectedTokenOrNull?.currency is CryptoCurrency.Coin) {
|
||||
return params.onLoadFee().flatMap { loadedFee ->
|
||||
val feeExtended = TransactionFeeExtended(
|
||||
transactionFee = loadedFee,
|
||||
feeTokenId = selectedTokenOrNull.currency.id,
|
||||
)
|
||||
populateExtendedFee(feeExtended)
|
||||
}
|
||||
}
|
||||
|
||||
return extended(selectedTokenOrNull).fold(
|
||||
ifLeft = { error ->
|
||||
when (error) {
|
||||
is GetFeeError.GaslessError.NotEnoughFunds -> error.left()
|
||||
is GetFeeError.GaslessError -> {
|
||||
// Something wrong with gasless fee, fallback to basic fee
|
||||
params.onLoadFee().map { LoadedFeeResult.Basic(it) }
|
||||
}
|
||||
else -> error.left()
|
||||
}
|
||||
},
|
||||
ifRight = { fee ->
|
||||
populateExtendedFee(fee)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun populateExtendedFee(
|
||||
fee: TransactionFeeExtended,
|
||||
): Either<GetFeeError, LoadedFeeResult.Extended> = either {
|
||||
LoadedFeeResult.Extended(
|
||||
fee = fee,
|
||||
selectedToken = getSelectedTokenStatus(fee.feeTokenId).bind(),
|
||||
availableTokens = getAvailableFeeTokens().bind(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getSelectedTokenStatus(tokenId: CryptoCurrency.ID): Either<GetFeeError, CryptoCurrencyStatus> =
|
||||
either {
|
||||
val feeTokenId = tokenId
|
||||
if (params.feeCryptoCurrencyStatus.currency.id != feeTokenId) {
|
||||
getSingleCryptoCurrencyStatusUseCase
|
||||
.invokeMultiWalletSync(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyId = feeTokenId,
|
||||
).getOrElse {
|
||||
raise(GetFeeError.DataError(IllegalStateException("No token found for id: $tokenId")))
|
||||
}
|
||||
} else {
|
||||
params.feeCryptoCurrencyStatus
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAvailableFeeTokens(): Either<GetFeeError, List<CryptoCurrencyStatus>> = either {
|
||||
val userWallet = getUserWalletUseCase(params.userWalletId).mapLeft {
|
||||
GetFeeError.DataError(IllegalStateException("No wallet found for id: ${params.userWalletId}"))
|
||||
}.bind()
|
||||
|
||||
getAvailableFeeTokensUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
).bind()
|
||||
}
|
||||
|
||||
sealed class LoadedFeeResult {
|
||||
data class Extended(
|
||||
val fee: TransactionFeeExtended,
|
||||
val selectedToken: CryptoCurrencyStatus?,
|
||||
val availableTokens: List<CryptoCurrencyStatus>,
|
||||
) : LoadedFeeResult()
|
||||
|
||||
data class Basic(val fee: TransactionFee) : LoadedFeeResult()
|
||||
|
||||
val selectedTokenOrNull: CryptoCurrencyStatus?
|
||||
get() = when (this) {
|
||||
is Extended -> selectedToken
|
||||
is Basic -> null
|
||||
}
|
||||
|
||||
val transactionFee: TransactionFee
|
||||
get() = when (this) {
|
||||
is Extended -> fee.transactionFee
|
||||
is Basic -> fee
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(params: FeeSelectorParams, modelScope: CoroutineScope): FeeSelectorLogic
|
||||
}
|
||||
}
|
||||
|
|
@ -1,246 +1,93 @@
|
|||
package com.tangem.features.send.v2.feeselector.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
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.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.core.decompose.utils.stack
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeNonce
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter
|
||||
import com.tangem.features.send.v2.feeselector.model.transformers.*
|
||||
import com.tangem.utils.TangemBlogUrlBuilder
|
||||
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class FeeSelectorModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
|
||||
private val feeSelectorReloadListener: FeeSelectorReloadListener,
|
||||
private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener,
|
||||
private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger,
|
||||
private val feeSelectorAlertFactory: FeeSelectorAlertFactory,
|
||||
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model(), FeeSelectorIntents, FeeSelectorModelCallback {
|
||||
) : Model(), FeeSelectorIntents {
|
||||
|
||||
private val params = paramsContainer.require<FeeSelectorParams>()
|
||||
private var appCurrency: AppCurrency = AppCurrency.Default
|
||||
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorDetailsParams>()
|
||||
private val feeSelectorLogic = feeSelectorLogicFactory.create(
|
||||
params = params,
|
||||
modelScope = modelScope,
|
||||
)
|
||||
|
||||
val feeSelectorBottomSheet = SlotNavigation<Unit>()
|
||||
|
||||
val uiState: StateFlow<FeeSelectorUM>
|
||||
field = MutableStateFlow<FeeSelectorUM>(params.state)
|
||||
|
||||
init {
|
||||
initAppCurrency()
|
||||
subscribeOnFeeReloadTriggerUpdates()
|
||||
subscribeOnFeeCheckReloadTriggerUpdates()
|
||||
subscribeOnFeeLoadingStateTriggerUpdates()
|
||||
loadFee()
|
||||
}
|
||||
|
||||
fun updateState(feeSelectorUM: FeeSelectorUM) {
|
||||
uiState.value = feeSelectorUM
|
||||
}
|
||||
|
||||
fun onReadMoreClicked() {
|
||||
urlOpener.openUrl(TangemBlogUrlBuilder.FEE_BLOG_LINK)
|
||||
}
|
||||
|
||||
private fun initAppCurrency() {
|
||||
modelScope.launch {
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFee() {
|
||||
if (uiState.value !is FeeSelectorUM.Content) {
|
||||
uiState.update(FeeSelectorLoadingTransformer)
|
||||
}
|
||||
modelScope.launch {
|
||||
params.onLoadFee()
|
||||
.fold(
|
||||
ifLeft = { error -> uiState.update(FeeSelectorErrorTransformer(error)) },
|
||||
ifRight = { fee ->
|
||||
uiState.update(
|
||||
FeeSelectorLoadedTransformer(
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
appCurrency = appCurrency,
|
||||
fees = fee,
|
||||
feeStateConfiguration = params.feeStateConfiguration,
|
||||
isFeeApproximate = isFeeApproximate(fee.normal.amount.type),
|
||||
feeSelectorIntents = this@FeeSelectorModel,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFeeApproximate(amountType: AmountType): Boolean {
|
||||
val networkId = params.feeCryptoCurrencyStatus.currency.network.id
|
||||
return isFeeApproximateUseCase(networkId = networkId, amountType = amountType)
|
||||
}
|
||||
val stackNavigation = StackNavigation<FeeSelectorRoute>()
|
||||
val uiState = feeSelectorLogic.uiState
|
||||
|
||||
override fun onFeeItemSelected(feeItem: FeeItem) {
|
||||
if (feeItem is FeeItem.Custom) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = params.analyticsCategoryName),
|
||||
)
|
||||
feeSelectorLogic.onFeeItemSelected(feeItem)
|
||||
|
||||
if (contentState().selectedFeeItem is FeeItem.Custom) {
|
||||
return
|
||||
}
|
||||
uiState.update(FeeItemSelectedTransformer(feeItem))
|
||||
if (feeItem !is FeeItem.Custom) {
|
||||
onDoneClick()
|
||||
|
||||
modelScope.launch {
|
||||
val stack = stackNavigation.stack()
|
||||
if (stack.isSingleItem() && stack[0] is FeeSelectorRoute.ChooseSpeed) {
|
||||
// In case of only speed selection, we finish the flow immediately after selection
|
||||
feeSelectorLogic.onDoneClick()
|
||||
params.callback.onFeeResult(uiState.value)
|
||||
} else {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTokenSelected(status: CryptoCurrencyStatus) {
|
||||
feeSelectorLogic.onTokenSelected(status)
|
||||
stackNavigation.pop()
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState.update(
|
||||
FeeSelectorCustomValueChangedTransformer(
|
||||
index = index,
|
||||
value = value,
|
||||
intents = this,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
feeSelectorLogic.onCustomFeeValueChange(index, value)
|
||||
}
|
||||
|
||||
override fun onNonceChange(value: String) {
|
||||
uiState.update(FeeSelectorNonceChangeTransformer(value = value))
|
||||
feeSelectorLogic.onNonceChange(value)
|
||||
}
|
||||
|
||||
override fun onDoneClick() {
|
||||
val feeSelectorUM = uiState.value as? FeeSelectorUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
CommonSendFeeAnalyticEvents.SelectedFee(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
feeType = feeSelectorUM.toAnalyticType(),
|
||||
source = params.analyticsSendSource,
|
||||
),
|
||||
)
|
||||
val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value
|
||||
if (feeSelectorUM.selectedFeeItem is FeeItem.Custom && isCustomFeeEdited) {
|
||||
analyticsEventHandler.send(GasPriceInserter(categoryName = params.analyticsCategoryName))
|
||||
}
|
||||
if (feeSelectorUM.feeNonce is FeeNonce.Nonce) {
|
||||
analyticsEventHandler.send(
|
||||
NonceInserted(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
token = params.feeCryptoCurrencyStatus.currency.symbol,
|
||||
blockchain = params.feeCryptoCurrencyStatus.currency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
(params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value)
|
||||
}
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
uiState.value = feeSelectorUM
|
||||
feeSelectorBottomSheet.dismiss()
|
||||
}
|
||||
|
||||
fun showFeeSelector() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.FeeScreenOpened(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = params.analyticsSendSource,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ScreenReopened(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Fee,
|
||||
),
|
||||
)
|
||||
modelScope.launch {
|
||||
neverShowTapHelpUseCase()
|
||||
feeSelectorBottomSheet.activate(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeReloadTriggerUpdates() {
|
||||
feeSelectorReloadListener.reloadTriggerFlow
|
||||
.onEach { data ->
|
||||
if (data.isRemoveSuggestedFee) {
|
||||
uiState.update(FeeSelectorRemoveSuggestedTransformer)
|
||||
}
|
||||
loadFee()
|
||||
val stack = stackNavigation.stack()
|
||||
if (stack.isSingleItem()) {
|
||||
feeSelectorLogic.onDoneClick()
|
||||
params.callback.onFeeResult(uiState.value)
|
||||
} else {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeLoadingStateTriggerUpdates() {
|
||||
feeSelectorReloadListener.loadingStateTriggerFlow
|
||||
.onEach { uiState.update(FeeSelectorLoadingTransformer) }
|
||||
.launchIn(modelScope)
|
||||
private fun contentState(): FeeSelectorUM.Content {
|
||||
val currentState = uiState.value
|
||||
require(currentState is FeeSelectorUM.Content) { "Current state must be FeeSelectorUM.Content" }
|
||||
return currentState
|
||||
}
|
||||
|
||||
private fun subscribeOnFeeCheckReloadTriggerUpdates() {
|
||||
feeSelectorCheckReloadListener.checkReloadTriggerFlow
|
||||
.onEach { checkLoadFee() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun checkLoadFee() {
|
||||
modelScope.launch {
|
||||
params.onLoadFee().fold(
|
||||
ifRight = { newFee ->
|
||||
feeSelectorAlertFactory.getFeeUpdatedAlert(
|
||||
newTransactionFee = newFee,
|
||||
feeSelectorUM = uiState.value,
|
||||
proceedAction = {
|
||||
modelScope.launch {
|
||||
feeSelectorCheckReloadTrigger.callbackCheckResult(true)
|
||||
}
|
||||
},
|
||||
stopAction = {
|
||||
modelScope.launch {
|
||||
feeSelectorCheckReloadTrigger.callbackCheckResult(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
ifLeft = { feeError ->
|
||||
feeSelectorCheckReloadTrigger.callbackCheckResult(false)
|
||||
feeSelectorAlertFactory.getFeeUnreachableErrorState(::loadFee)
|
||||
},
|
||||
)
|
||||
fun getInitialRoute(): FeeSelectorRoute {
|
||||
return when {
|
||||
feeSelectorLogic.isGaslessEnabled -> FeeSelectorRoute.NetworkFee
|
||||
else -> FeeSelectorRoute.ChooseSpeed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,13 @@ import com.tangem.utils.transformer.Transformer
|
|||
internal class FeeSelectorErrorTransformer(private val error: GetFeeError) : Transformer<FeeSelectorUM> {
|
||||
|
||||
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
|
||||
if (prevState is FeeSelectorUM.Content && error is GetFeeError.GaslessError.NotEnoughFunds) {
|
||||
return prevState.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
feeExtraInfo = prevState.feeExtraInfo.copy(isNotEnoughFunds = true),
|
||||
)
|
||||
}
|
||||
|
||||
return FeeSelectorUM.Error(error = error)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,24 @@
|
|||
package com.tangem.features.send.v2.feeselector.model.transformers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorLogic.LoadedFeeResult
|
||||
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(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val fees: TransactionFee,
|
||||
private val fees: LoadedFeeResult,
|
||||
private val feeStateConfiguration: FeeSelectorParams.FeeStateConfiguration,
|
||||
private val isFeeApproximate: Boolean,
|
||||
private val feeSelectorIntents: FeeSelectorIntents,
|
||||
|
|
@ -25,7 +26,7 @@ internal class FeeSelectorLoadedTransformer(
|
|||
|
||||
private val feeItemsConverter = FeeItemConverter(
|
||||
feeStateConfiguration = feeStateConfiguration,
|
||||
normalFee = fees.normal,
|
||||
normalFee = fees.transactionFee.normal,
|
||||
feeSelectorIntents = feeSelectorIntents,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
|
|
@ -37,10 +38,17 @@ internal class FeeSelectorLoadedTransformer(
|
|||
} else {
|
||||
null
|
||||
}
|
||||
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(FeeItemConverter.Input(fees, prevCustomFee))
|
||||
|
||||
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(
|
||||
FeeItemConverter.Input(fees.transactionFee, prevCustomFee),
|
||||
)
|
||||
|
||||
val selectedFee = when (prevState) {
|
||||
is FeeSelectorUM.Content -> feeItems.first { it.isSameClass(prevState.selectedFeeItem) }
|
||||
is FeeSelectorUM.Content -> if (prevState.selectedFeeItem is FeeItem.Loading) {
|
||||
feeItems.firstOrNull { it is FeeItem.Market }
|
||||
} else {
|
||||
feeItems.firstOrNull { it.isSameClass(prevState.selectedFeeItem) }
|
||||
} ?: FeeItem.Loading
|
||||
is FeeSelectorUM.Error,
|
||||
FeeSelectorUM.Loading,
|
||||
-> feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market }
|
||||
|
|
@ -49,8 +57,8 @@ internal class FeeSelectorLoadedTransformer(
|
|||
val nonce = ((prevState as? FeeSelectorUM.Content)?.feeNonce as? FeeNonce.Nonce)?.nonce
|
||||
|
||||
return FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = fees,
|
||||
isPrimaryButtonEnabled = selectedFee !is FeeItem.Loading,
|
||||
fees = fees.transactionFee,
|
||||
feeItems = feeItems,
|
||||
selectedFeeItem = selectedFee,
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
|
|
@ -58,6 +66,9 @@ internal class FeeSelectorLoadedTransformer(
|
|||
isFeeConvertibleToFiat = feeCryptoCurrencyStatus.currency.network.hasFiatFeeRate,
|
||||
isTronToken = cryptoCurrencyStatus.currency is CryptoCurrency.Token &&
|
||||
isTron(cryptoCurrencyStatus.currency.network.rawId),
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
availableFeeCurrencies = getAvailableFeeCurrencies(),
|
||||
transactionFeeExtended = (fees as? LoadedFeeResult.Extended)?.fee,
|
||||
),
|
||||
feeFiatRateUM = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
|
||||
FeeFiatRateUM(
|
||||
|
|
@ -65,7 +76,7 @@ internal class FeeSelectorLoadedTransformer(
|
|||
appCurrency = appCurrency,
|
||||
)
|
||||
},
|
||||
feeNonce = if (fees.normal is Fee.Ethereum) {
|
||||
feeNonce = if (fees.transactionFee.normal is Fee.Ethereum) {
|
||||
FeeNonce.Nonce(
|
||||
nonce = nonce,
|
||||
onNonceChange = feeSelectorIntents::onNonceChange,
|
||||
|
|
@ -75,4 +86,9 @@ internal class FeeSelectorLoadedTransformer(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAvailableFeeCurrencies(): ImmutableList<CryptoCurrencyStatus>? {
|
||||
if (fees !is LoadedFeeResult.Extended) return null
|
||||
return fees.availableTokens.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.features.send.v2.feeselector.model.transformers
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
class FeeSelectorTokenSelectedTransformer(
|
||||
private val selectedToken: CryptoCurrencyStatus,
|
||||
) : Transformer<FeeSelectorUM> {
|
||||
|
||||
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
|
||||
return if (prevState is FeeSelectorUM.Content) {
|
||||
prevState.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
selectedFeeItem = FeeItem.Loading,
|
||||
feeItems = persistentListOf(FeeItem.Loading),
|
||||
feeExtraInfo = prevState.feeExtraInfo.copy(
|
||||
feeCryptoCurrencyStatus = selectedToken,
|
||||
isNotEnoughFunds = false,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.send.v2.feeselector.route
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Immutable
|
||||
@Serializable
|
||||
internal sealed interface FeeSelectorRoute : Route {
|
||||
|
||||
val title: TextReference
|
||||
|
||||
@Serializable
|
||||
data object NetworkFee : FeeSelectorRoute {
|
||||
override val title: TextReference = resourceReference(R.string.common_network_fee_title)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object ChooseSpeed : FeeSelectorRoute {
|
||||
override val title: TextReference = resourceReference(R.string.fee_selector_choose_speed_title)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object ChooseToken : FeeSelectorRoute {
|
||||
override val title: TextReference = resourceReference(R.string.fee_selector_choose_token_title)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,11 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
import com.tangem.common.ui.amountScreen.utils.getFiatString
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.audits.AuditLabel
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.tooltip.TangemTooltip
|
||||
import com.tangem.core.ui.extensions.annotatedReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
|
|
@ -38,6 +42,9 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.FeeSelectorBlockTestTags
|
||||
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.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
|
|
@ -50,9 +57,14 @@ private const val READ_MORE_TAG = "READ_MORE"
|
|||
@Composable
|
||||
internal fun FeeSelectorBlockContent(
|
||||
state: FeeSelectorUM,
|
||||
isGaslessFeatureEnabled: Boolean,
|
||||
onReadMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state is FeeSelectorUM.Error && state.isHidden) {
|
||||
return
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
|
|
@ -68,16 +80,25 @@ internal fun FeeSelectorBlockContent(
|
|||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
FeeSelectorDescription(state = state, onReadMoreClick = onReadMoreClick)
|
||||
FeeSelectorDescription(
|
||||
state = state,
|
||||
isGaslessFeatureEnabled = isGaslessFeatureEnabled,
|
||||
onReadMoreClick = onReadMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeSelectorDescription(state: FeeSelectorUM, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
private fun FeeSelectorDescription(
|
||||
state: FeeSelectorUM,
|
||||
isGaslessFeatureEnabled: Boolean,
|
||||
onReadMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
FeeSelectorStaticPart(modifier = Modifier.weight(1f), onReadMoreClick = onReadMoreClick)
|
||||
when (state) {
|
||||
is FeeSelectorUM.Content -> FeeContent(state)
|
||||
is FeeSelectorUM.Content -> FeeContent(state, isGaslessFeatureEnabled)
|
||||
is FeeSelectorUM.Loading -> FeeLoading()
|
||||
is FeeSelectorUM.Error -> FeeError()
|
||||
}
|
||||
|
|
@ -122,7 +143,7 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie
|
|||
}
|
||||
}
|
||||
TangemTooltip(
|
||||
text = annotatedString,
|
||||
text = annotatedReference(annotatedString),
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing6)
|
||||
.size(TangemTheme.dimens.size16)
|
||||
|
|
@ -160,9 +181,18 @@ private fun FeeLoading() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) {
|
||||
private fun FeeContent(state: FeeSelectorUM.Content, isGaslessFeatureEnabled: Boolean, modifier: Modifier = Modifier) {
|
||||
val fiatRate = state.feeFiatRateUM
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
if (isGaslessFeatureEnabled) {
|
||||
AuditLabel(
|
||||
state = AuditLabelUM(
|
||||
text = stringReference(state.selectedFeeItem.fee.amount.currencySymbol),
|
||||
type = AuditLabelUM.Type.General,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
EllipsisText(
|
||||
text = if (state.feeExtraInfo.isFeeConvertibleToFiat && fiatRate != null) {
|
||||
getFiatString(
|
||||
|
|
@ -187,7 +217,10 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi
|
|||
.padding(start = TangemTheme.dimens.spacing4)
|
||||
.testTag(FeeSelectorBlockTestTags.FEE_AMOUNT),
|
||||
)
|
||||
if (!state.feeItems.isSingleItem()) {
|
||||
|
||||
val isGaslessAvailable = isGaslessFeatureEnabled && state.feeExtraInfo.transactionFeeExtended != null
|
||||
|
||||
if (!state.feeItems.isSingleItem() || isGaslessAvailable) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(width = 18.dp, height = 24.dp)
|
||||
|
|
@ -205,16 +238,58 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi
|
|||
@Composable
|
||||
private fun FeeSelectorBlockContent_Preview(@PreviewParameter(FeeSelectorUMProvider::class) state: FeeSelectorUM) {
|
||||
TangemThemePreview {
|
||||
FeeSelectorBlockContent(modifier = Modifier.fillMaxWidth(), state = state, onReadMoreClick = {})
|
||||
FeeSelectorBlockContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isGaslessFeatureEnabled = true,
|
||||
state = state,
|
||||
onReadMoreClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
|
||||
private val maxFeeItem = FeeItem.Market(
|
||||
fee = Fee.Common(amount = Amount(value = BigDecimal("100000000"), blockchain = Blockchain.Ethereum)),
|
||||
)
|
||||
private val lowFeeItem =
|
||||
FeeItem.Market(Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)))
|
||||
private val maxFeeItem
|
||||
get() = FeeItem.Market(
|
||||
fee = Fee.Common(amount = Amount(value = BigDecimal("100000000"), blockchain = Blockchain.Hedera)),
|
||||
)
|
||||
private val lowFeeItem
|
||||
get() = FeeItem.Market(
|
||||
Fee.Common(
|
||||
amount = Amount(
|
||||
value = BigDecimal("0.0002876"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val cryptoCurrencyStatus
|
||||
get() = 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,
|
||||
)
|
||||
|
||||
override val values: Sequence<FeeSelectorUM> = sequenceOf(
|
||||
FeeSelectorUM.Content(
|
||||
|
|
@ -225,6 +300,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeNonce = FeeNonce.None,
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
|
|
@ -241,6 +317,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeNonce = FeeNonce.None,
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
|
|
|
|||
|
|
@ -1,67 +1,41 @@
|
|||
package com.tangem.features.send.v2.feeselector.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
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.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnter
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmountV2
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
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.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun FeeSelectorModalBottomSheet(
|
||||
childStack: ChildStack<FeeSelectorRoute, ComposableContentComponent>,
|
||||
state: FeeSelectorUM,
|
||||
feeSelectorIntents: FeeSelectorIntents,
|
||||
feeDisplaySource: FeeSelectorParams.FeeDisplaySource,
|
||||
onDismiss: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
if (state !is FeeSelectorUM.Content) return
|
||||
|
||||
|
|
@ -71,426 +45,98 @@ internal fun FeeSelectorModalBottomSheet(
|
|||
onDismissRequest = onDismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
onBack = if (childStack.backStack.isNotEmpty()) onBack else null,
|
||||
title = {
|
||||
FeeTitle(feeDisplaySource = feeDisplaySource, onDismiss = onDismiss)
|
||||
},
|
||||
content = {
|
||||
FeeSelectorItems(
|
||||
state = state,
|
||||
feeSelectorIntents = feeSelectorIntents,
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
FeeTitle(
|
||||
childStack = childStack,
|
||||
feeDisplaySource = feeDisplaySource,
|
||||
onBack = onBack,
|
||||
)
|
||||
},
|
||||
footer = if (state.selectedFeeItem is FeeItem.Custom) {
|
||||
{
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
text = stringResourceSafe(R.string.common_done),
|
||||
onClick = feeSelectorIntents::onDoneClick,
|
||||
)
|
||||
content = {
|
||||
Content(childStack = childStack)
|
||||
},
|
||||
footer = when {
|
||||
childStack.active.configuration is FeeSelectorRoute.NetworkFee -> {
|
||||
{
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
text = stringResourceSafe(R.string.common_confirm),
|
||||
onClick = feeSelectorIntents::onDoneClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
childStack.active.configuration is FeeSelectorRoute.ChooseSpeed &&
|
||||
state.selectedFeeItem is FeeItem.Custom -> {
|
||||
{
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
text = stringResourceSafe(R.string.common_done),
|
||||
onClick = feeSelectorIntents::onDoneClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDismiss: () -> Unit) {
|
||||
when (feeDisplaySource) {
|
||||
FeeSelectorParams.FeeDisplaySource.Screen -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(R.string.common_network_fee_title),
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = onDismiss,
|
||||
)
|
||||
}
|
||||
FeeSelectorParams.FeeDisplaySource.BottomSheet -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(R.string.common_network_fee_title),
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeSelectorItems(
|
||||
state: FeeSelectorUM.Content,
|
||||
feeSelectorIntents: FeeSelectorIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
private fun FeeTitle(
|
||||
childStack: ChildStack<FeeSelectorRoute, ComposableContentComponent>,
|
||||
feeDisplaySource: FeeSelectorParams.FeeDisplaySource,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
val feeFiatRateUM = state.feeFiatRateUM
|
||||
state.feeItems.fastForEachIndexed { index, item ->
|
||||
val isSelected = item.isSameClass(state.selectedFeeItem)
|
||||
val isLastItem = index == state.feeItems.size - 1
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary,
|
||||
label = "Fee selector icon tint change",
|
||||
)
|
||||
val iconBackgroundColor by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
TangemTheme.colors.icon.accent.copy(alpha = 0.1F)
|
||||
Children(
|
||||
stack = childStack,
|
||||
animation = stackAnimation(fade(animationSpec = tween(durationMillis = 100))),
|
||||
) { current ->
|
||||
when (feeDisplaySource) {
|
||||
FeeSelectorParams.FeeDisplaySource.Screen -> {
|
||||
if (childStack.backStack.isNotEmpty()) {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = current.configuration.title,
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onBack,
|
||||
)
|
||||
} else {
|
||||
TangemTheme.colors.background.secondary
|
||||
},
|
||||
label = "Fee selector icon background change",
|
||||
)
|
||||
val itemModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectedBorder(isSelected = isSelected)
|
||||
.clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) })
|
||||
when (item) {
|
||||
is FeeItem.Custom -> CustomFeeBlock(
|
||||
modifier = itemModifier,
|
||||
customFee = item,
|
||||
isSelected = isSelected,
|
||||
iconBackgroundColor = iconBackgroundColor,
|
||||
iconTint = iconTint,
|
||||
onValueChange = feeSelectorIntents::onCustomFeeValueChange,
|
||||
nonce = state.feeNonce,
|
||||
)
|
||||
else -> RegularFeeItemContent(
|
||||
modifier = itemModifier,
|
||||
title = item.title,
|
||||
iconRes = item.iconRes,
|
||||
iconBackgroundColor = iconBackgroundColor,
|
||||
iconTint = iconTint,
|
||||
preDot = stringReference(
|
||||
item.fee.amount.value.format {
|
||||
crypto(
|
||||
symbol = item.fee.amount.currencySymbol,
|
||||
decimals = item.fee.amount.decimals,
|
||||
).fee(canBeLower = state.feeExtraInfo.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (state.feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) {
|
||||
getFiatReference(
|
||||
value = item.fee.amount.value,
|
||||
rate = feeFiatRateUM.rate,
|
||||
appCurrency = feeFiatRateUM.appCurrency,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
ellipsizeOffset = item.fee.amount.currencySymbol.length,
|
||||
showDivider = !isSelected && !isLastItem,
|
||||
TangemModalBottomSheetTitle(
|
||||
title = current.configuration.title,
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = onBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
FeeSelectorParams.FeeDisplaySource.BottomSheet -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = current.configuration.title,
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun CustomFeeBlock(
|
||||
customFee: FeeItem.Custom,
|
||||
isSelected: Boolean,
|
||||
iconBackgroundColor: Color,
|
||||
iconTint: Color,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
nonce: FeeNonce,
|
||||
private fun Content(
|
||||
childStack: ChildStack<FeeSelectorRoute, ComposableContentComponent>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(color = iconBackgroundColor, shape = CircleShape)
|
||||
.padding(6.dp),
|
||||
painter = painterResource(R.drawable.ic_edit_v2_24),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_custom),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isSelected,
|
||||
label = "Custom Fee Selected Animation",
|
||||
enter = expandVertically().plus(fadeIn()),
|
||||
exit = shrinkVertically().plus(fadeOut()),
|
||||
) {
|
||||
ExpandedCustomFeeItems(
|
||||
customFeeFields = customFee.customValues,
|
||||
onValueChange = onValueChange,
|
||||
nonce = nonce,
|
||||
)
|
||||
}
|
||||
Children(
|
||||
stack = childStack,
|
||||
animation = stackAnimation(fade(animationSpec = tween(durationMillis = 100))),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.animateContentSize(),
|
||||
) {
|
||||
it.instance.Content(Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandedCustomFeeItems(
|
||||
customFeeFields: ImmutableList<CustomFeeFieldUM>,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
nonce: FeeNonce,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
customFeeFields.fastForEachIndexed { index, field ->
|
||||
val isShowDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce
|
||||
if (field.label != null) {
|
||||
InputRowEnterInfoAmountV2(
|
||||
text = field.value,
|
||||
decimals = field.decimals,
|
||||
symbol = field.symbol,
|
||||
title = field.title,
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
info = field.label,
|
||||
description = field.footer,
|
||||
keyboardOptions = field.keyboardOptions,
|
||||
keyboardActions = field.keyboardActions,
|
||||
onValueChange = { onValueChange(index, it) },
|
||||
showDivider = isShowDivider,
|
||||
isReadOnly = field.isReadonly,
|
||||
)
|
||||
} else {
|
||||
InputRowEnterInfoAmountV2(
|
||||
text = field.value,
|
||||
decimals = field.decimals,
|
||||
title = field.title,
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
symbol = field.symbol,
|
||||
description = field.footer,
|
||||
onValueChange = { onValueChange(index, it) },
|
||||
keyboardOptions = field.keyboardOptions,
|
||||
keyboardActions = field.keyboardActions,
|
||||
showDivider = isShowDivider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (nonce is FeeNonce.Nonce) {
|
||||
InputRowEnter(
|
||||
text = nonce.nonce?.toString().orEmpty(),
|
||||
title = resourceReference(R.string.send_nonce),
|
||||
description = resourceReference(R.string.send_nonce_footer),
|
||||
onValueChange = nonce.onNonceChange,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
placeholder = resourceReference(R.string.send_nonce_hint),
|
||||
titleColor = TangemTheme.colors.text.secondary,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RegularFeeItemContent(
|
||||
title: TextReference,
|
||||
@DrawableRes iconRes: Int,
|
||||
iconBackgroundColor: Color,
|
||||
iconTint: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
preDot: TextReference? = null,
|
||||
postDot: TextReference? = null,
|
||||
ellipsizeOffset: Int? = null,
|
||||
showDivider: Boolean = true,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(color = iconBackgroundColor, shape = CircleShape)
|
||||
.padding(6.dp),
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
)
|
||||
FeeDescription(
|
||||
title = title,
|
||||
preDot = preDot,
|
||||
postDot = postDot,
|
||||
ellipsizeOffset = ellipsizeOffset,
|
||||
)
|
||||
}
|
||||
if (showDivider) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 60.dp, end = 12.dp),
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
thickness = 0.5.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeDescription(
|
||||
title: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
preDot: TextReference? = null,
|
||||
postDot: TextReference? = null,
|
||||
ellipsizeOffset: Int? = null,
|
||||
) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
if (preDot != null) {
|
||||
FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeValueContent(preDot: TextReference, postDot: TextReference?, ellipsizeOffset: Int? = null) {
|
||||
val ellipsis = if (ellipsizeOffset == null) {
|
||||
TextEllipsis.End
|
||||
} else {
|
||||
TextEllipsis.OffsetEnd(ellipsizeOffset)
|
||||
}
|
||||
val textColor = TangemTheme.colors.text.tertiary
|
||||
val textStyle = TangemTheme.typography.caption1
|
||||
Row {
|
||||
EllipsisText(
|
||||
text = preDot.resolveReference(),
|
||||
style = textStyle,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.End,
|
||||
ellipsis = ellipsis,
|
||||
)
|
||||
if (postDot != null) {
|
||||
Text(
|
||||
text = StringsSigns.DOT,
|
||||
style = textStyle,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
Text(text = postDot.resolveReference(), style = textStyle, color = textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun FeeSelectorBS_Preview(
|
||||
@PreviewParameter(FeeSelectorUMContentProvider::class)
|
||||
state: FeeSelectorUM.Content,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
FeeSelectorModalBottomSheet(
|
||||
onDismiss = {},
|
||||
state = state,
|
||||
feeSelectorIntents = StubFeeSelectorIntents(),
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<FeeSelectorUM.Content>(
|
||||
collection = listOf(
|
||||
FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Suggested(
|
||||
title = resourceReference(
|
||||
id = R.string.wc_fee_suggested,
|
||||
formatArgs = wrappedList("Tangem"),
|
||||
),
|
||||
fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)),
|
||||
),
|
||||
FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))),
|
||||
FeeItem.Market(
|
||||
fee = Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.02"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))),
|
||||
customFeeItem,
|
||||
),
|
||||
selectedFeeItem = FeeItem.Slow(
|
||||
fee = Fee.Common(
|
||||
Amount(
|
||||
value = BigDecimal("0.01"),
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = true,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal.TEN,
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
feeNonce = FeeNonce.None,
|
||||
fees = TransactionFee.Single(customFeeItem.fee),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val customFeeItem = FeeItem.Custom(
|
||||
fee = Fee.Common(Amount(value = BigDecimal("0.05"), blockchain = Blockchain.Ethereum)),
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.119806",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
keyboardActions = KeyboardActions(),
|
||||
symbol = "ETH",
|
||||
decimals = 8,
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_amount_fee_footer),
|
||||
label = stringReference("~ 0,03 \$"),
|
||||
isReadonly = false,
|
||||
),
|
||||
CustomFeeFieldUM(
|
||||
value = "40000",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
keyboardActions = KeyboardActions(),
|
||||
symbol = "GWEI",
|
||||
decimals = 8,
|
||||
title = resourceReference(R.string.send_gas_price),
|
||||
footer = resourceReference(R.string.send_gas_price_footer),
|
||||
label = null,
|
||||
isReadonly = false,
|
||||
),
|
||||
CustomFeeFieldUM(
|
||||
value = "31400",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
keyboardActions = KeyboardActions(),
|
||||
symbol = null,
|
||||
decimals = 8,
|
||||
title = resourceReference(R.string.send_gas_limit),
|
||||
footer = resourceReference(R.string.send_gas_limit_footer),
|
||||
label = null,
|
||||
isReadonly = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -216,6 +216,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
callback = model,
|
||||
predefinedValues = model.predefinedValues,
|
||||
onLoadFee = model::loadFee,
|
||||
onLoadFeeExtended = model::loadFeeExtended,
|
||||
onSendTransaction = {
|
||||
innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
|
|
@ -86,12 +87,14 @@ internal class SendConfirmComponent(
|
|||
params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = model.uiState.value.feeSelectorUM,
|
||||
onLoadFee = params.onLoadFee,
|
||||
onLoadFeeExtended = params.onLoadFeeExtended,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeStateConfiguration = model.feeStateConfiguration,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
|
@ -102,7 +105,6 @@ internal class SendConfirmComponent(
|
|||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
appCurrency = params.appCurrency,
|
||||
callback = model,
|
||||
notificationData = NotificationData(
|
||||
|
|
@ -113,6 +115,7 @@ internal class SendConfirmComponent(
|
|||
isIgnoreReduce = model.confirmData.isIgnoreReduce,
|
||||
fee = model.confirmData.fee,
|
||||
feeError = model.confirmData.feeError,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -163,6 +166,7 @@ internal class SendConfirmComponent(
|
|||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
val onLoadFeeExtended: suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended>,
|
||||
val onSendTransaction: () -> Unit,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
|
|
@ -29,6 +30,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
|||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
|
|
@ -36,6 +38,7 @@ import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
|
|||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent
|
||||
|
|
@ -111,6 +114,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase,
|
||||
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
|
||||
) : Model(), SendConfirmClickIntents, FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback {
|
||||
|
||||
|
|
@ -153,10 +157,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
internal var feeStateConfiguration: FeeStateConfiguration = FeeStateConfiguration.None
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
isAmountSubtractAvailable =
|
||||
isAmountSubtractAvailableUseCase(userWallet.walletId, cryptoCurrency).getOrElse { false }
|
||||
}
|
||||
updateAmountSubtractAvailability()
|
||||
configConfirmNavigation()
|
||||
subscribeOnNotificationsUpdateTrigger()
|
||||
subscribeOnCheckFeeResultUpdates()
|
||||
|
|
@ -355,7 +356,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
|
||||
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = confirmData.reduceAmountBy.orZero(),
|
||||
|
|
@ -388,11 +389,23 @@ internal class SendConfirmModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
|
||||
val result = sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
|
||||
|
||||
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||
|
||||
val result = if (isFeeInTokenCurrency) {
|
||||
createAndSendGaslessTransactionUseCase(
|
||||
userWallet = userWallet,
|
||||
transactionData = txData,
|
||||
fee = feeExtended,
|
||||
)
|
||||
} else {
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
}
|
||||
|
||||
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
|
||||
|
||||
|
|
@ -412,8 +425,8 @@ internal class SendConfirmModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
},
|
||||
ifRight = {
|
||||
updateTransactionStatus(txData)
|
||||
ifRight = { txHash ->
|
||||
updateTransactionStatus(txData, txHash)
|
||||
addTokenToWalletIfNeeded()
|
||||
sendBalanceUpdater.scheduleUpdates()
|
||||
sendAnalyticHelper.sendSuccessAnalytics(
|
||||
|
|
@ -427,6 +440,16 @@ internal class SendConfirmModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getCurrencyStatusForFeePayment(): CryptoCurrencyStatus {
|
||||
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
|
||||
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||
return if (isFeeInTokenCurrency) {
|
||||
feeUMV2?.feeExtraInfo?.feeCryptoCurrencyStatus ?: cryptoCurrencyStatus
|
||||
} else {
|
||||
params.feeCryptoCurrencyStatus
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTokenToWalletIfNeeded() {
|
||||
if (cryptoCurrency !is CryptoCurrency.Token) return
|
||||
val wallets = destinationUM?.wallets ?: return
|
||||
|
|
@ -456,9 +479,9 @@ internal class SendConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateTransactionStatus(txData: TransactionData.Uncompiled) {
|
||||
private fun updateTransactionStatus(txData: TransactionData.Uncompiled, txHash: String) {
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = txData.hash.orEmpty(),
|
||||
txHash = txHash.ifEmpty { txData.hash.orEmpty() },
|
||||
networkId = cryptoCurrency.network.id,
|
||||
currency = cryptoCurrency,
|
||||
).getOrNull().orEmpty()
|
||||
|
|
@ -498,6 +521,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
isIgnoreReduce = confirmData.isIgnoreReduce,
|
||||
fee = confirmData.fee,
|
||||
feeError = confirmData.feeError,
|
||||
feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||
),
|
||||
)
|
||||
_uiState.update { state ->
|
||||
|
|
@ -599,9 +623,23 @@ internal class SendConfirmModel @Inject constructor(
|
|||
override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) {
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
_uiState.update { it.copy(feeSelectorUM = feeSelectorUM) }
|
||||
updateAmountSubtractAvailability()
|
||||
updateConfirmNotifications()
|
||||
}
|
||||
|
||||
private fun updateAmountSubtractAvailability() {
|
||||
modelScope.launch {
|
||||
val fee = feeUMV2?.feeExtraInfo?.transactionFeeExtended?.transactionFee?.normal
|
||||
// we assume if feeExtraInfo is empty then pay fee in the main currency
|
||||
val feeTokenId = feeUMV2?.feeExtraInfo?.transactionFeeExtended?.feeTokenId ?: cryptoCurrency.id
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrency,
|
||||
maybeGaslessFee = fee?.let { feeTokenId to fee },
|
||||
).getOrElse { false }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CHECK_FEE_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
|
|||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
|
|
@ -37,8 +38,11 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
|||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
|
|
@ -95,6 +99,8 @@ internal class SendModel @Inject constructor(
|
|||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
|
||||
private val getFeeForTokenUseCase: GetFeeForTokenUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
|
||||
|
|
@ -245,10 +251,10 @@ internal class SendModel @Inject constructor(
|
|||
showAlertError()
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
suspend fun prepareTransferTransaction(): Either<Throwable, TransactionData> {
|
||||
val predefinedValues = predefinedValues
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value
|
||||
val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) {
|
||||
return if (predefinedValues is PredefinedValues.Content.Deeplink) {
|
||||
val predefinedAmount = predefinedValues.amount.parseBigDecimalOrNull()
|
||||
createTransferTransactionUseCase(
|
||||
amount = predefinedAmount?.convertToSdkAmount(cryptoCurrencyStatus) ?: error("Invalid amount"),
|
||||
|
|
@ -271,9 +277,12 @@ internal class SendModel @Inject constructor(
|
|||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
}.getOrElse {
|
||||
return GetFeeError.DataError(it).left()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val transferTransaction = prepareTransferTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return getFeeUseCase(
|
||||
transactionData = transferTransaction,
|
||||
|
|
@ -282,6 +291,25 @@ internal class SendModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
|
||||
val transferTransaction = prepareTransferTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return if (maybeToken == null) {
|
||||
getFeeForGaslessUseCase(
|
||||
transactionData = transferTransaction,
|
||||
userWallet = userWallet,
|
||||
network = params.currency.network,
|
||||
)
|
||||
} else {
|
||||
getFeeForTokenUseCase(
|
||||
transactionData = transferTransaction,
|
||||
userWallet = userWallet,
|
||||
token = maybeToken.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertError() {
|
||||
sendConfirmAlertFactory.getGenericErrorState(
|
||||
onFailedTxEmailClick = ::onFailedTxEmailClick,
|
||||
|
|
@ -334,15 +362,15 @@ internal class SendModel @Inject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
).onEach { (account, cryptoCurrencyStatus) ->
|
||||
isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync()
|
||||
accountFlow.value = account
|
||||
|
||||
cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus
|
||||
feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
).getOrNull() ?: cryptoCurrencyStatus
|
||||
|
||||
isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync()
|
||||
accountFlow.value = account
|
||||
|
||||
if (params.amount != null) {
|
||||
router.replaceAll(Confirm)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import com.tangem.core.ui.utils.DateTimeFormatters
|
|||
import com.tangem.core.ui.utils.toPx
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.v2.common.ui.FeeBlock
|
||||
import com.tangem.features.send.v2.common.ui.FeeBlockSuccess
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
|
|
@ -106,7 +106,7 @@ private fun SuccessContent(
|
|||
onClick = {},
|
||||
)
|
||||
destinationBlockComponent.Content(modifier = Modifier)
|
||||
FeeBlock(feeSelectorUM = sendUM.feeSelectorUM)
|
||||
FeeBlockSuccess(feeSelectorUM = sendUM.feeSelectorUM)
|
||||
SpacerH(16.dp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -101,7 +102,6 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
appCurrency = params.appCurrency,
|
||||
callback = model,
|
||||
notificationData = SendNotificationsComponent.Params.NotificationData(
|
||||
|
|
@ -112,6 +112,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
isIgnoreReduce = false,
|
||||
fee = model.confirmData.fee,
|
||||
feeError = model.confirmData.feeError,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
isIgnoreReduce = false,
|
||||
fee = confirmData.fee,
|
||||
feeError = confirmData.feeError,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, // TODO change when gasless implement
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import com.tangem.core.ui.utils.toPx
|
|||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.features.nft.component.NFTDetailsBlockComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.v2.common.ui.FeeBlock
|
||||
import com.tangem.features.send.v2.common.ui.FeeBlockSuccess
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
|
||||
|
|
@ -109,7 +109,7 @@ private fun SuccessContent(
|
|||
}
|
||||
nftDetailsBlockComponent.Content(modifier = Modifier)
|
||||
destinationBlockComponent.Content(modifier = Modifier)
|
||||
FeeBlock(feeSelectorUM = nftSendUM.feeSelectorUM)
|
||||
FeeBlockSuccess(feeSelectorUM = nftSendUM.feeSelectorUM)
|
||||
SpacerH(16.dp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ private fun SendConvertTokenButton(onConvertToAnother: () -> Unit) {
|
|||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.background(TangemTheme.colors.control.unchecked, CircleShape)
|
||||
.background(TangemTheme.colors.control.default, CircleShape)
|
||||
.padding(2.dp),
|
||||
)
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
|
|||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class EthereumCustomFeeConverter(
|
||||
|
|
@ -38,11 +39,16 @@ internal class EthereumCustomFeeConverter(
|
|||
)
|
||||
|
||||
override fun convert(value: Fee.Ethereum): ImmutableList<CustomFeeFieldUM> {
|
||||
if (value is Fee.Ethereum.TokenCurrency) {
|
||||
return persistentListOf()
|
||||
}
|
||||
|
||||
return buildList {
|
||||
add(convertFeeValue(value))
|
||||
|
||||
addAll(
|
||||
when (value) {
|
||||
is Fee.Ethereum.TokenCurrency -> error("unreachable")
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value)
|
||||
},
|
||||
|
|
@ -55,6 +61,7 @@ internal class EthereumCustomFeeConverter(
|
|||
|
||||
override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList<CustomFeeFieldUM>): Fee.Ethereum {
|
||||
return when (normalFee) {
|
||||
is Fee.Ethereum.TokenCurrency -> error("unreachable")
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.convertBack(normalFee = normalFee, value = value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.convertBack(normalFee = normalFee, value = value)
|
||||
}
|
||||
|
|
@ -62,6 +69,7 @@ internal class EthereumCustomFeeConverter(
|
|||
|
||||
override fun getGasLimitIndex(feeValue: Fee.Ethereum): Int {
|
||||
return when (feeValue) {
|
||||
is Fee.Ethereum.TokenCurrency -> error("unreachable")
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.getGasLimitIndex(feeValue)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.getGasLimitIndex(feeValue)
|
||||
}
|
||||
|
|
@ -74,6 +82,7 @@ internal class EthereumCustomFeeConverter(
|
|||
value: String,
|
||||
): ImmutableList<CustomFeeFieldUM> {
|
||||
return when (feeValue) {
|
||||
is Fee.Ethereum.TokenCurrency -> error("unreachable")
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange(
|
||||
feeValue = feeValue,
|
||||
customValues = customValues,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ internal class NotificationsModel @Inject constructor(
|
|||
private val analyticsCategoryName = params.analyticsCategoryName
|
||||
private val userWalletId = params.userWalletId
|
||||
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
private val feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus
|
||||
private val feeCryptoCurrencyStatus = params.notificationData.feeCryptoCurrencyStatus
|
||||
private val currency = cryptoCurrencyStatus.currency
|
||||
private val appCurrency = params.appCurrency
|
||||
|
||||
|
|
@ -92,7 +92,10 @@ internal class NotificationsModel @Inject constructor(
|
|||
|
||||
init {
|
||||
subscribeToNotificationUpdateTrigger()
|
||||
checkIfSubtractAvailable()
|
||||
modelScope.launch {
|
||||
checkIfSubtractAvailable()
|
||||
buildNotifications()
|
||||
}
|
||||
incrementNotificationsShowCount()
|
||||
}
|
||||
|
||||
|
|
@ -102,11 +105,14 @@ internal class NotificationsModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun checkIfSubtractAvailable() {
|
||||
modelScope.launch {
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, currency).getOrElse { false }
|
||||
buildNotifications()
|
||||
}
|
||||
private suspend fun checkIfSubtractAvailable() {
|
||||
val feeCurrencyId = notificationData.feeCryptoCurrencyStatus.currency.id
|
||||
val fee = notificationData.fee
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
maybeGaslessFee = fee?.let { feeCurrencyId to fee },
|
||||
).getOrElse { false }
|
||||
}
|
||||
|
||||
private fun incrementNotificationsShowCount() {
|
||||
|
|
@ -115,16 +121,19 @@ internal class NotificationsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun updateState(data: NotificationData) {
|
||||
private fun updateState(data: NotificationData) {
|
||||
notificationData = data
|
||||
buildNotifications()
|
||||
modelScope.launch {
|
||||
checkIfSubtractAvailable()
|
||||
buildNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildNotifications() = with(notificationData) {
|
||||
val feeValue = fee?.amount?.value
|
||||
val sendingAmount = checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
|
|
@ -226,7 +235,7 @@ internal class NotificationsModel @Inject constructor(
|
|||
fee = feeValue,
|
||||
userWalletId = userWalletId,
|
||||
tokenStatus = cryptoCurrencyStatus,
|
||||
coinStatus = feeCryptoCurrencyStatus,
|
||||
feeStatus = notificationData.feeCryptoCurrencyStatus,
|
||||
).getOrNull()
|
||||
|
||||
addExceedBalanceNotification(
|
||||
|
|
@ -369,6 +378,15 @@ internal class NotificationsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCurrencyStatusForFeePayment(): CryptoCurrencyStatus {
|
||||
val isFeeInTokenCurrency = notificationData.fee is Fee.Ethereum.TokenCurrency
|
||||
return if (isFeeInTokenCurrency) {
|
||||
notificationData.feeCryptoCurrencyStatus
|
||||
} else {
|
||||
cryptoCurrencyStatus
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT = 3
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import com.tangem.common.ui.notifications.NotificationUM
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2
|
||||
|
|
@ -28,6 +30,34 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
|
|||
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
|
||||
private val analyticsCategoryName = "test_category"
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
|
||||
// GIVEN
|
||||
|
|
@ -191,6 +221,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
@ -274,6 +305,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
@ -345,6 +377,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
@ -428,6 +461,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import com.tangem.common.ui.notifications.NotificationUM
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.common.ui.state.ConfirmUM
|
||||
import io.mockk.mockk
|
||||
|
|
@ -30,6 +32,34 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
|
||||
private val analyticsCategoryName = "test_category"
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
|
||||
// GIVEN
|
||||
|
|
@ -247,6 +277,7 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
@ -330,6 +361,7 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
@ -401,6 +433,7 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
@ -484,6 +517,7 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue