diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index b5f1d0f72e..f50a061117 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -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 { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index de09a0570e..079dbfdb6f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -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() } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index aee69c85fb..217631c38e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -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) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt index e0cda82bfa..67fe86cf8e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -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 = persistentListOf(), ) : FeeSelectorState() - object Error : FeeSelectorState() + data object Error : FeeSelectorState() } enum class FeeType { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index d2339a3de9..997c39d2f1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -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, ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index 864ae1b501..caf0ee5683 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt index 7cec508963..c07c45a39b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt @@ -49,4 +49,8 @@ internal object FeeStatePreviewData { isFeeApproximate = false, notifications = persistentListOf(), ) + + val errorFeeState = feeState.copy( + feeSelectorState = FeeSelectorState.Error, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt new file mode 100644 index 0000000000..d70b281ffe --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -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) {} + + override fun onNotificationCancel(clazz: Class) {} +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 59028beb85..f7252345fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -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 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 } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index f9e410b2ee..c17e9c4458 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -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, - modifier: Modifier = Modifier, - isLast: Boolean = false, -) { +private fun LazyListScope.notifications(configs: ImmutableList, 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), + ) } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index dfaed7cb57..270d4954f9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -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 \ No newline at end of file + +private class SendSpeedSelectorPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + FeeStatePreviewData.feeState, + FeeStatePreviewData.errorFeeState, + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt new file mode 100644 index 0000000000..2caaa43e1b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index fd799169cc..decc2cc4d6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -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) { uiState = amountStateFactory.getOnAmountValueChange(reducedAmount) uiState = sendNotificationFactory.dismissNotificationState(clazz) - loadFee() + onCheckFeeUpdate() } override fun onNotificationCancel(clazz: Class) {