Updated on 2026-08-14
This commit is contained in:
parent
1ab4a6ab31
commit
ee85f09aa0
13 changed files with 389 additions and 351 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -49,4 +49,8 @@ internal object FeeStatePreviewData {
|
|||
isFeeApproximate = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
|
||||
val errorFeeState = feeState.copy(
|
||||
feeSelectorState = FeeSelectorState.Error,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>) {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue