Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-29 15:27:28 +05:00
commit 68920e11b5
13 changed files with 389 additions and 351 deletions

View file

@ -243,7 +243,12 @@ internal class SendStateFactory(
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
val state = currentStateProvider()
return state.copy(sendState = state.sendState?.copy(isSending = isSending))
return state.copy(
sendState = state.sendState?.copy(
isSending = isSending,
isPrimaryButtonEnabled = !isSending,
),
)
}
fun getTransactionSendState(txData: TransactionData): SendUiState {

View file

@ -36,8 +36,8 @@ internal class StateRouter(
}
else -> when (type) {
SendUiStateType.Amount -> continueToSend(::showRecipient)
SendUiStateType.Fee -> continueToSend(::showAmount)
SendUiStateType.Send -> continueToSend(::showFee)
SendUiStateType.Fee -> showSend()
SendUiStateType.Send -> continueToSend(::showAmount)
else -> continueToSend(::popBackStack)
}
}
@ -46,8 +46,9 @@ internal class StateRouter(
fun onNextClick() {
when (currentState.value.type) {
SendUiStateType.Recipient -> continueToSend(::showAmount)
SendUiStateType.Amount -> continueToSend(::showFee)
SendUiStateType.Fee -> showSend()
SendUiStateType.Amount,
SendUiStateType.Fee,
-> showSend()
SendUiStateType.Send -> onBackClick()
else -> popBackStack()
}
@ -59,7 +60,6 @@ internal class StateRouter(
} else {
when (currentState.value.type) {
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
else -> popBackStack()
}
}

View file

@ -30,7 +30,6 @@ internal class FeeNotificationFactory(
val feeState = state.feeState ?: return@map persistentListOf()
buildList {
when (val feeSelectorState = feeState.feeSelectorState) {
FeeSelectorState.Loading -> Unit
FeeSelectorState.Error -> {
addFeeUnreachableNotification(feeSelectorState)
}

View file

@ -9,15 +9,13 @@ import kotlinx.collections.immutable.persistentListOf
@Immutable
internal sealed class FeeSelectorState {
object Loading : FeeSelectorState()
data class Content(
val fees: TransactionFee,
val selectedFee: FeeType = FeeType.Market,
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
) : FeeSelectorState()
object Error : FeeSelectorState()
data object Error : FeeSelectorState()
}
enum class FeeType {

View file

@ -45,8 +45,8 @@ internal class FeeStateFactory(
val state = currentStateProvider()
val feeState = state.feeState ?: return state
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = true),
feeState = feeState.copy(
feeSelectorState = FeeSelectorState.Loading,
notifications = persistentListOf(),
isPrimaryButtonEnabled = false,
),
@ -65,6 +65,7 @@ internal class FeeStateFactory(
val fee = feeConverter.convert(feeSelectorState)
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
feeState = feeState.copy(
feeSelectorState = feeSelectorState,
fee = fee,
@ -76,6 +77,7 @@ internal class FeeStateFactory(
fun onFeeOnErrorState(): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(isFeeLoading = false),
feeState = state.feeState?.copy(
feeSelectorState = FeeSelectorState.Error,
),

View file

@ -14,7 +14,7 @@ internal class SendFeeStateConverter(
override fun convert(value: Unit): SendStates.FeeState {
return SendStates.FeeState(
feeSelectorState = FeeSelectorState.Loading,
feeSelectorState = FeeSelectorState.Error,
fee = null,
notifications = persistentListOf(),
rate = feeCryptoCurrencyStatusProvider().value.fiatRate,

View file

@ -49,4 +49,8 @@ internal object FeeStatePreviewData {
isFeeApproximate = false,
notifications = persistentListOf(),
)
val errorFeeState = feeState.copy(
feeSelectorState = FeeSelectorState.Error,
)
}

View file

@ -0,0 +1,63 @@
package com.tangem.features.send.impl.presentation.state.previewdata
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
@Suppress("TooManyFunctions")
internal object SendClickIntentsStub : SendClickIntents {
override fun popBackStack() {}
override fun onBackClick() {}
override fun onNextClick() {}
override fun onPrevClick() {}
override fun onQrCodeScanClick() {}
override fun onFailedTxEmailClick(errorMessage: String) {}
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) {}
override fun onAmountValueChange(value: String) {}
override fun onCurrencyChangeClick(isFiat: Boolean) {}
override fun onMaxValueClick() {}
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {}
override fun onRecipientMemoValueChange(value: String) {}
override fun feeReload() {}
override fun onFeeSelectorClick(feeType: FeeType) {}
override fun onCustomFeeValueChange(index: Int, value: String) {}
override fun onSubtractSelect() {}
override fun onReadMoreClick() {}
override fun onSendClick() {}
override fun showAmount() {}
override fun showRecipient() {}
override fun showFee() {}
override fun showSend() {}
override fun onExploreClick() {}
override fun onShareClick() {}
override fun onAmountReduceClick(reducedAmount: String, clazz: Class<out SendNotification>) {}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {}
}

View file

@ -69,14 +69,17 @@ private fun SendNavigationButton(
val sendState = uiState.sendState ?: return
val isEditingDisabled = uiState.isEditingDisabled
val isSuccess = sendState.isSuccess
val isSending = sendState.isSending
val isFromConfirmation = currentState.isFromConfirmation
val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess && !isSending
val showProgress = uiState.amountState?.isFeeLoading == true
val (buttonTextId, buttonClick) = getButtonData(
currentState = currentState,
isSuccess = isSuccess,
isSending = isSending,
uiState = uiState,
)
val isButtonEnabled = isButtonEnabled(currentState, uiState)
@ -101,7 +104,7 @@ private fun SendNavigationButton(
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.button.secondary)
.clickable { uiState.clickIntents.onPrevClick() }
.clickable(enabled = !showProgress) { uiState.clickIntents.onPrevClick() }
.padding(TangemTheme.dimens.spacing12),
)
}
@ -113,7 +116,7 @@ private fun SendNavigationButton(
if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
buttonClick()
},
showProgress = sendState.isSending,
showProgress = showProgress,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)
@ -204,6 +207,7 @@ private fun getButtonData(
uiState: SendUiState,
currentState: SendUiCurrentScreen,
isSuccess: Boolean,
isSending: Boolean,
): Pair<Int, () -> Unit> {
return when (currentState.type) {
SendUiStateType.None,
@ -215,10 +219,10 @@ private fun getButtonData(
} else {
R.string.common_next to uiState.clickIntents::onNextClick
}
SendUiStateType.Send -> if (isSuccess) {
R.string.common_close
} else {
R.string.common_send
SendUiStateType.Send -> when {
isSuccess -> R.string.common_close
isSending -> R.string.send_sending
else -> R.string.common_send
} to uiState.clickIntents::onSendClick
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.impl.presentation.ui.fee
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
@ -55,17 +54,13 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents:
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.notifications(
configs: ImmutableList<SendNotification>,
modifier: Modifier = Modifier,
isLast: Boolean = false,
) {
private fun LazyListScope.notifications(configs: ImmutableList<SendNotification>, modifier: Modifier = Modifier) {
items(
items = configs,
key = { it::class.java },
contentType = { it::class.java },
itemContent = {
val bottomPadding = if (isLast) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0
val bottomPadding = if (it == configs.last()) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0
Notification(
config = it.config,
modifier = modifier
@ -75,21 +70,9 @@ private fun LazyListScope.notifications(
)
.animateItemPlacement(),
containerColor = when (it) {
is SendNotification.Error.ExceedsBalance,
is SendNotification.Warning.NetworkFeeUnreachable,
-> TangemTheme.colors.background.action
is SendNotification.Warning.NetworkFeeUnreachable -> TangemTheme.colors.background.action
else -> TangemTheme.colors.button.disabled
},
iconTint = when (it) {
is SendNotification.Error.ExceedsBalance -> {
if (it.config.buttonsState == null) {
TangemTheme.colors.icon.warning
} else {
null
}
}
else -> null
},
)
},
)
@ -97,24 +80,19 @@ private fun LazyListScope.notifications(
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: Modifier = Modifier) {
item(
key = FEE_CUSTOM_KEY,
) {
AnimatedVisibility(
visible = feeSendState is FeeSelectorState.Content,
modifier = modifier
.fillMaxWidth()
.animateItemPlacement()
.background(TangemTheme.colors.background.tertiary),
if (feeSendState is FeeSelectorState.Content) {
item(
key = FEE_CUSTOM_KEY,
) {
(feeSendState as? FeeSelectorState.Content)?.let { fee ->
val customValues = fee.customValues
SendCustomFeeEthereum(
customValues = customValues,
selectedFee = fee.selectedFee,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
}
SendCustomFeeEthereum(
customValues = feeSendState.customValues,
selectedFee = feeSendState.selectedFee,
modifier = modifier
.fillMaxWidth()
.animateItemPlacement()
.background(TangemTheme.colors.background.tertiary)
.padding(top = TangemTheme.dimens.spacing12),
)
}
}
}

View file

@ -1,56 +1,29 @@
package com.tangem.features.send.impl.presentation.ui.fee
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.BigDecimalFormatter.CAN_BE_LOWER_SIGN
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import java.math.BigDecimal
private val DEFAULT_FEE_OPTIONS = listOf(
R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24,
R.string.common_fee_selector_option_market to R.drawable.ic_bird_24,
R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24,
)
@Suppress("LongMethod")
@Composable
@ -66,79 +39,34 @@ internal fun SendSpeedSelector(
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
when (val feeSelectorState = state.feeSelectorState) {
FeeSelectorState.Error -> {
SendSpeedSelectorItemError()
}
FeeSelectorState.Loading -> {
SendSpeedSelectorItemLoading()
}
is FeeSelectorState.Content -> {
when (val fees = feeSelectorState.fees) {
is TransactionFee.Choosable -> {
val isSelected = feeSelectorState.selectedFee
val minimumAmount = fees.minimum.amount
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_slow,
iconRes = R.drawable.ic_tortoise_24,
amount = getCryptoReference(minimumAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(minimumAmount, state.rate, state.appCurrency),
symbolLength = minimumAmount.currencySymbol.length,
isSelected = isSelected == FeeType.Slow,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) },
)
val normalAmount = fees.normal.amount
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_market,
iconRes = R.drawable.ic_bird_24,
amount = getCryptoReference(normalAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency),
symbolLength = normalAmount.currencySymbol.length,
isSelected = isSelected == FeeType.Market,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
)
val priorityAmount = fees.priority.amount
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_fast,
iconRes = R.drawable.ic_hare_24,
amount = getCryptoReference(priorityAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(priorityAmount, state.rate, state.appCurrency),
symbolLength = priorityAmount.currencySymbol.length,
isSelected = isSelected == FeeType.Fast,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) },
showDivider = fees.normal is Fee.Ethereum,
)
AnimatedVisibility(
visible = fees.normal is Fee.Ethereum,
label = "Custom fee appearance animation",
) {
val showWarning = state.notifications.any { it is SendNotification.Warning.TooHigh }
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_custom,
iconRes = R.drawable.ic_edit_24,
isSelected = isSelected == FeeType.Custom,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) },
showDivider = fees.normal !is Fee.Ethereum,
showWarning = showWarning,
)
}
}
is TransactionFee.Single -> {
val normalAmount = feeSelectorState.fees.normal.amount
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_market,
iconRes = R.drawable.ic_bird_24,
isSelected = true,
amount = getCryptoReference(normalAmount, state.isFeeApproximate),
fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency),
symbolLength = normalAmount.currencySymbol.length,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
showDivider = false,
)
}
}
}
}
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_slow,
iconRes = R.drawable.ic_tortoise_24,
feeType = FeeType.Slow,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) },
)
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_market,
iconRes = R.drawable.ic_bird_24,
feeType = FeeType.Market,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
)
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_fast,
iconRes = R.drawable.ic_hare_24,
feeType = FeeType.Fast,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) },
)
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_custom,
iconRes = R.drawable.ic_edit_24,
feeType = FeeType.Custom,
state = state,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) },
)
}
FooterText(clickIntents::onReadMoreClick)
}
@ -177,179 +105,32 @@ private fun FooterText(onReadMoreClick: () -> Unit) {
)
}
// todo remove after refactoring [REDACTED_JIRA]
private fun getCryptoReference(amount: Amount, isFeeApproximate: Boolean) = combinedReference(
if (isFeeApproximate) stringReference("$CAN_BE_LOWER_SIGN ") else TextReference.EMPTY,
stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = amount.value,
cryptoCurrency = amount.currencySymbol,
decimals = amount.decimals,
),
),
)
// todo remove after refactoring [REDACTED_JIRA]
private fun getFiatReference(amount: Amount, rate: BigDecimal?, appCurrency: AppCurrency) = stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = rate?.let { amount.value?.multiply(it) },
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
)
@Composable
private fun SendSpeedSelectorItemLoading() {
repeat(DEFAULT_FEE_OPTIONS.size) {
val (text, iconRes) = DEFAULT_FEE_OPTIONS[it]
Row(modifier = Modifier.fillMaxWidth()) {
SelectorTitleContent(
titleRes = text,
iconRes = iconRes,
)
SpacerWMax()
RectangleShimmer(
radius = TangemTheme.dimens.radius3,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing18,
bottom = TangemTheme.dimens.spacing18,
end = TangemTheme.dimens.spacing12,
)
.size(
width = TangemTheme.dimens.size90,
height = TangemTheme.dimens.size12,
),
)
}
}
}
@Composable
private fun SendSpeedSelectorItemError() {
repeat(DEFAULT_FEE_OPTIONS.size) {
val (text, iconRes) = DEFAULT_FEE_OPTIONS[it]
Row(modifier = Modifier.fillMaxWidth()) {
SelectorTitleContent(
titleRes = text,
iconRes = iconRes,
)
SpacerWMax()
Text(
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing12,
),
)
}
}
}
@Composable
private fun SendSpeedSelectorItem(
@StringRes titleRes: Int,
@DrawableRes iconRes: Int,
onSelect: () -> Unit,
modifier: Modifier = Modifier,
amount: TextReference? = null,
fiatAmount: TextReference? = null,
symbolLength: Int? = null,
isSelected: Boolean = false,
showDivider: Boolean = true,
showWarning: Boolean = false,
) {
Box(
modifier = modifier
.fillMaxWidth()
.clickable { onSelect() },
) {
SelectorRowItem(
titleRes = titleRes,
iconRes = iconRes,
onSelect = onSelect,
modifier = modifier,
preDot = amount,
postDot = fiatAmount,
ellipsizeOffset = symbolLength,
isSelected = isSelected,
showDivider = showDivider,
)
WarningIcon(showWarning = showWarning)
}
}
@Composable
private fun SelectorTitleContent(
@StringRes titleRes: Int,
@DrawableRes iconRes: Int,
iconTint: Color = TangemTheme.colors.icon.informative,
textStyle: TextStyle = TangemTheme.typography.body2,
) {
Icon(
painter = painterResource(iconRes),
tint = iconTint,
contentDescription = null,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
)
Text(
text = stringResource(titleRes),
style = textStyle,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing8,
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing14,
),
)
}
@Composable
private fun WarningIcon(showWarning: Boolean = false) {
AnimatedVisibility(
visible = showWarning,
label = "Custom fee warning indicator",
enter = fadeIn(),
exit = fadeOut(),
) {
Row {
SpacerWMax()
Image(
painter = painterResource(R.drawable.ic_alert_triangle_20),
contentDescription = null,
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing12,
horizontal = TangemTheme.dimens.spacing14,
),
)
}
}
}
//region preview
// region Preview
@Preview
@Composable
private fun FeeSelectorPreview_Light() {
private fun SendSpeedSelectorPreview_Light(
@PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState,
) {
TangemTheme {
SendSpeedSelectorItemLoading()
SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub)
}
}
@Preview
@Composable
private fun FeeSelectorPreview_Dark() {
private fun SendSpeedSelectorPreview_Dark(
@PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState,
) {
TangemTheme(isDark = true) {
SendSpeedSelectorItemLoading()
SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub)
}
}
//endregion
private class SendSpeedSelectorPreviewProvider : PreviewParameterProvider<SendStates.FeeState> {
override val values: Sequence<SendStates.FeeState>
get() = sequenceOf(
FeeStatePreviewData.feeState,
FeeStatePreviewData.errorFeeState,
)
}
// endregion

View file

@ -0,0 +1,173 @@
package com.tangem.features.send.impl.presentation.ui.fee
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import java.math.BigDecimal
@Composable
internal fun SendSpeedSelectorItem(
@StringRes titleRes: Int,
@DrawableRes iconRes: Int,
feeType: FeeType,
state: SendStates.FeeState,
onSelect: () -> Unit,
modifier: Modifier = Modifier,
) {
val feeSelectorState = state.feeSelectorState
val content = feeSelectorState as? FeeSelectorState.Content
val amount = content?.getAmount(feeType)
val (showDivider, isVisible) = content.getDividerAndVisibility(feeType)
val hasCustomValues = !content?.customValues.isNullOrEmpty()
AnimatedVisibility(
visible = isVisible || hasCustomValues,
label = "Fee Selector Visibility Animation",
enter = expandVertically().plus(fadeIn()),
exit = shrinkVertically().plus(fadeOut()),
) {
Box(
modifier = modifier
.fillMaxWidth()
.clickable { onSelect() },
) {
SelectorRowItem(
titleRes = titleRes,
iconRes = iconRes,
onSelect = onSelect,
modifier = modifier,
preDot = getCryptoReference(amount, state.isFeeApproximate),
postDot = getFiatReference(amount, state.rate, state.appCurrency),
ellipsizeOffset = amount?.currencySymbol?.length,
isSelected = content?.selectedFee == feeType,
showDivider = showDivider,
)
SendSpeedSelectorItemError(isError = feeSelectorState is FeeSelectorState.Error)
if (feeType == FeeType.Custom) {
val showWarning = state.notifications.any { it is SendNotification.Warning.TooHigh }
WarningIcon(showWarning = showWarning)
}
}
}
}
// todo remove after refactoring [REDACTED_JIRA]
private fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? {
if (amount == null) return null
return combinedReference(
if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY,
stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = amount.value,
cryptoCurrency = amount.currencySymbol,
decimals = amount.decimals,
),
),
)
}
// todo remove after refactoring [REDACTED_JIRA]
private fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
if (amount == null) return null
return stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = rate?.let { amount.value?.multiply(it) },
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
)
}
@Composable
private fun SendSpeedSelectorItemError(isError: Boolean) {
Row {
SpacerWMax()
AnimatedVisibility(
visible = isError,
label = "Error state indication animation",
enter = fadeIn(),
exit = fadeOut(),
) {
Text(
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing12,
),
)
}
}
}
@Composable
private fun WarningIcon(showWarning: Boolean = false) {
Row {
SpacerWMax()
AnimatedVisibility(
visible = showWarning,
label = "Custom fee warning indicator",
enter = fadeIn(),
exit = fadeOut(),
) {
Image(
painter = painterResource(R.drawable.ic_alert_triangle_20),
contentDescription = null,
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing12,
horizontal = TangemTheme.dimens.spacing14,
),
)
}
}
}
private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? {
val choosableFees = fees as? TransactionFee.Choosable
return when (feeType) {
FeeType.Slow -> choosableFees?.minimum?.amount
FeeType.Market -> fees.normal.amount
FeeType.Fast -> choosableFees?.priority?.amount
FeeType.Custom -> null
}
}
private fun FeeSelectorState.Content?.getDividerAndVisibility(feeType: FeeType): Pair<Boolean, Boolean> {
val hasCustomValues = !this?.customValues.isNullOrEmpty()
val isNotSingle = this?.fees !is TransactionFee.Single
val isSingle = this?.fees is TransactionFee.Single
return when (feeType) {
FeeType.Slow -> true to isNotSingle
FeeType.Market -> isNotSingle to isSingle
FeeType.Fast -> hasCustomValues to isNotSingle
FeeType.Custom -> false to isNotSingle
}
}

View file

@ -485,26 +485,22 @@ internal class SendViewModel @Inject constructor(
override fun onBackClick() = stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true)
override fun onNextClick() {
val currentState = stateRouter.currentState.value
val isCurrentFee = currentState.type == SendUiStateType.Fee
if (isCurrentFee) {
val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus)
if (isAmountSubtractAvailable && isFeeCoverage) {
uiState = eventStateFactory.getFeeCoverageAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
return
} else {
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
sendOnNextScreenAnalyticSender.send(currentState.type, uiState)
when (currentState.type) {
SendUiStateType.Fee -> {
if (onFeeNext()) return
}
if (checkIfFeeTooLow(uiState)) {
uiState = eventStateFactory.getFeeTooLowAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
return
SendUiStateType.Amount -> {
if (uiState.feeState?.feeSelectorState is FeeSelectorState.Content) {
if (onFeeCoverageAlert()) return
} else {
loadFee(isToNextState = true)
return
}
}
else -> Unit
}
sendOnNextScreenAnalyticSender.send(currentState.type, uiState)
stateRouter.onNextClick()
}
@ -521,6 +517,30 @@ internal class SendViewModel @Inject constructor(
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
innerRouter.openTokenDetails(userWalletId, currency)
private fun onFeeNext(): Boolean {
if (onFeeCoverageAlert()) return true
if (checkIfFeeTooLow(uiState)) {
uiState = eventStateFactory.getFeeTooLowAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
return true
}
return false
}
private fun onFeeCoverageAlert(): Boolean {
val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus)
return if (isAmountSubtractAvailable && isFeeCoverage) {
uiState = eventStateFactory.getFeeCoverageAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
true
} else {
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
false
}
}
// endregion
// region amount state clicks
@ -648,15 +668,26 @@ internal class SendViewModel @Inject constructor(
innerRouter.openUrl(url)
}
private fun loadFee() {
private fun loadFee(isToNextState: Boolean = false) {
viewModelScope.launch(dispatchers.main) {
if (uiState.feeState?.fee == null) {
val isShowStatus = uiState.feeState?.fee == null
if (isShowStatus) {
uiState = feeStateFactory.onFeeOnLoadingState()
}
uiState = callFeeUseCase()?.fold(
ifRight = feeStateFactory::onFeeOnLoadedState,
ifLeft = { feeStateFactory.onFeeOnErrorState() },
) ?: feeStateFactory.onFeeOnErrorState()
val result = callFeeUseCase()?.fold(
ifRight = {
uiState = feeStateFactory.onFeeOnLoadedState(it)
if (isToNextState && !onFeeCoverageAlert()) {
stateRouter.showSend()
}
},
ifLeft = {
if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState()
},
)
if (result == null && isShowStatus) {
uiState = feeStateFactory.onFeeOnErrorState()
}
updateFeeNotifications()
updateAmountNotifications()
}.saveIn(feeJobHolder)
@ -733,7 +764,7 @@ internal class SendViewModel @Inject constructor(
override fun onAmountReduceClick(reducedAmount: String, clazz: Class<out SendNotification>) {
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount)
uiState = sendNotificationFactory.dismissNotificationState(clazz)
loadFee()
onCheckFeeUpdate()
}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {