Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-28 16:34:17 +05:00
parent dd2b494f85
commit 244af774f4
17 changed files with 424 additions and 338 deletions

View file

@ -11,13 +11,14 @@ internal sealed class SendNotification(val config: NotificationConfig) {
sealed class Error(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_24,
buttonState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : SendNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = R.drawable.ic_alert_24,
iconResId = iconResId,
buttonsState = buttonState,
onCloseClick = onCloseClick,
),
@ -59,6 +60,41 @@ internal sealed class SendNotification(val config: NotificationConfig) {
onClick = onConfirmClick,
),
)
data class ExceedsBalance(
val networkIconId: Int,
val currencyName: String,
val feeName: String,
val feeSymbol: String,
val networkName: String,
val mergeFeeNetworkName: Boolean = false,
val onClick: (() -> Unit)? = null,
) : Error(
title = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_title,
wrappedList(feeName),
),
subtitle = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_message,
formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol),
),
iconResId = networkIconId,
buttonState = onClick?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(
R.string.common_buy_currency,
wrappedList(
if (mergeFeeNetworkName) {
"$currencyName ($feeSymbol)"
} else {
feeName
},
),
),
onClick = onClick,
)
},
)
}
sealed class Warning(
@ -98,5 +134,21 @@ internal sealed class SendNotification(val config: NotificationConfig) {
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
)
data class TooHigh(
val value: String,
) : Warning(
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
)
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onRefresh,
),
)
}
}

View file

@ -10,7 +10,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
@ -50,6 +49,8 @@ internal sealed class SendStates {
val tokenIconState: TokenIconState,
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
val amountTextField: SendTextField.AmountField,
val notifications: ImmutableList<SendNotification>,
val isFeeLoading: Boolean,
) : SendStates()
/** Recipient state */
@ -75,7 +76,7 @@ internal sealed class SendStates {
val rate: BigDecimal?,
val appCurrency: AppCurrency,
val isFeeApproximate: Boolean,
val notifications: ImmutableList<SendFeeNotification>,
val notifications: ImmutableList<SendNotification>,
) : SendStates()
/** Send state */

View file

@ -0,0 +1,38 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
internal class AmountNotificationFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val clickIntents: SendClickIntents,
) {
fun create() = stateRouterProvider().currentState
.filter { it.type == SendUiStateType.Amount }
.map {
buildList {
addFeeUnreachableNotification()
}.toImmutableList()
}
private fun MutableList<SendNotification>.addFeeUnreachableNotification() {
val state = currentStateProvider()
val feeState = state.feeState ?: return
if (feeState.feeSelectorState is FeeSelectorState.Error) {
add(
SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload),
)
}
}
}

View file

@ -1,10 +1,12 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
/**
* Factory to produce amount state for [SendUiState]
@ -41,4 +43,11 @@ internal class AmountStateFactory(
}
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
fun getAmountNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState?.copy(notifications = notifications),
)
}
}

View file

@ -38,6 +38,8 @@ internal class SendAmountStateConverter(
tokenIconState = iconStateConverter.convert(status),
amountTextField = sendAmountFieldConverter.convert(value),
isPrimaryButtonEnabled = false,
notifications = persistentListOf(),
isFeeLoading = false,
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
persistentListOf()
} else {

View file

@ -1,15 +1,26 @@
package com.tangem.features.send.impl.presentation.state
package com.tangem.features.send.impl.presentation.state.confirm
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.*
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.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
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.viewmodel.SendClickIntents
@ -33,6 +44,7 @@ internal class SendNotificationFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val clickIntents: SendClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
) {
fun create(): Flow<ImmutableList<SendNotification>> = stateRouterProvider().currentState
@ -47,6 +59,7 @@ internal class SendNotificationFactory(
buildList {
// errors
addExceedBalanceNotification(feeAmount, sendAmount)
addExceedsBalanceNotification(feeState.fee)
addInvalidAmountNotification(sendState.isSubtract, sendAmount)
addMinimumAmountErrorNotification(feeAmount, sendAmount)
addDustWarningNotification(feeAmount, sendAmount)
@ -272,6 +285,83 @@ internal class SendNotificationFactory(
}
}
private suspend fun MutableList<SendNotification>.addExceedsBalanceNotification(fee: Fee?) {
val feeValue = fee?.amount?.value ?: BigDecimal.ZERO
val userWalletId = userWalletProvider().walletId
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val warning = getBalanceNotEnoughForFeeWarningUseCase(
fee = feeValue,
userWalletId = userWalletId,
tokenStatus = cryptoCurrencyStatus,
coinStatus = coinCryptoCurrencyStatusProvider(),
).fold(
ifLeft = { null },
ifRight = { it },
) ?: return
val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName()
when (warning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
add(
SendNotification.Error.ExceedsBalance(
networkIconId = warning.coinCurrency.networkIconResId,
networkName = warning.coinCurrency.name,
currencyName = cryptoCurrencyStatus.currency.name,
feeName = warning.coinCurrency.name,
feeSymbol = warning.coinCurrency.symbol,
mergeFeeNetworkName = mergeFeeNetworkName,
onClick = {
clickIntents.onTokenDetailsClick(
userWalletId = userWalletId,
currency = warning.coinCurrency,
)
},
),
)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeNotEnoughFee(
token = cryptoCurrencyStatus.currency.symbol,
blockchain = cryptoCurrencyStatus.currency.network.name,
),
)
}
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val currency = warning.feeCurrency
add(
SendNotification.Error.ExceedsBalance(
networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24,
currencyName = warning.currency.name,
feeName = warning.feeCurrencyName,
feeSymbol = warning.feeCurrencySymbol,
networkName = warning.networkName,
mergeFeeNetworkName = mergeFeeNetworkName,
onClick = currency?.let {
{
clickIntents.onTokenDetailsClick(
userWalletId,
currency,
)
}
},
),
)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeNotEnoughFee(
token = warning.currency.symbol,
blockchain = warning.networkName,
),
)
}
else -> Unit
}
}
// workaround for networks that users have misunderstanding
private fun CryptoCurrencyStatus.shouldMergeFeeNetworkName(): Boolean {
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
}
companion object {
private const val CARDANO_MINIMUM = "1"
private const val DOGECOIN_MINIMUM = "0.01"

View file

@ -1,22 +1,12 @@
package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.toFormattedString
@ -28,14 +18,9 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
internal class FeeNotificationFactory(
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val stateRouterProvider: Provider<StateRouter>,
private val clickIntents: SendClickIntents,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun create() = stateRouterProvider().currentState
@ -53,19 +38,18 @@ internal class FeeNotificationFactory(
val customFee = feeSelectorState.customValues
val selectedFee = feeSelectorState.selectedFee
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
addExceedsBalanceNotification(feeState.fee)
}
}
}.toImmutableList()
}
private fun MutableList<SendFeeNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
private fun MutableList<SendNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
if (feeSelectorState is FeeSelectorState.Error) {
add(SendFeeNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
}
}
private fun MutableList<SendFeeNotification>.addTooHighNotification(
private fun MutableList<SendNotification>.addTooHighNotification(
transactionFee: TransactionFee,
selectedFee: FeeType,
customFee: List<SendTextField.CustomFee>,
@ -76,87 +60,10 @@ internal class FeeNotificationFactory(
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
val diff = customValue / highValue
if (selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
add(SendFeeNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS)))
add(SendNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS)))
}
}
private suspend fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(fee: Fee?) {
val feeValue = fee?.amount?.value ?: BigDecimal.ZERO
val userWalletId = userWalletProvider().walletId
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val warning = getBalanceNotEnoughForFeeWarningUseCase(
fee = feeValue,
userWalletId = userWalletId,
tokenStatus = cryptoCurrencyStatus,
coinStatus = coinCryptoCurrencyStatusProvider(),
).fold(
ifLeft = { null },
ifRight = { it },
) ?: return
val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName()
when (warning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
add(
SendFeeNotification.Error.ExceedsBalance(
networkIconId = warning.coinCurrency.networkIconResId,
networkName = warning.coinCurrency.name,
currencyName = cryptoCurrencyStatus.currency.name,
feeName = warning.coinCurrency.name,
feeSymbol = warning.coinCurrency.symbol,
mergeFeeNetworkName = mergeFeeNetworkName,
onClick = {
clickIntents.onTokenDetailsClick(
userWalletId = userWalletId,
currency = warning.coinCurrency,
)
},
),
)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeNotEnoughFee(
token = cryptoCurrencyStatus.currency.symbol,
blockchain = cryptoCurrencyStatus.currency.network.name,
),
)
}
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val currency = warning.feeCurrency
add(
SendFeeNotification.Error.ExceedsBalance(
networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24,
currencyName = warning.currency.name,
feeName = warning.feeCurrencyName,
feeSymbol = warning.feeCurrencySymbol,
networkName = warning.networkName,
mergeFeeNetworkName = mergeFeeNetworkName,
onClick = currency?.let {
{
clickIntents.onTokenDetailsClick(
userWalletId,
currency,
)
}
},
),
)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeNotEnoughFee(
token = warning.currency.symbol,
blockchain = warning.networkName,
),
)
}
else -> Unit
}
}
// workaround for networks that users have misunderstanding
private fun CryptoCurrencyStatus.shouldMergeFeeNetworkName(): Boolean {
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
}
companion object {
private val FEE_MAX_DIFF = BigDecimal(5)
private const val HIGH_FEE_DIFF_DECIMALS = 0

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
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.SendUiState
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
@ -111,7 +112,7 @@ internal class FeeStateFactory(
)
}
fun getFeeNotificationState(notifications: ImmutableList<SendFeeNotification>): SendUiState {
fun getFeeNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
val state = currentStateProvider()
return state.copy(
feeState = state.feeState?.copy(
@ -123,7 +124,7 @@ internal class FeeStateFactory(
private fun isPrimaryButtonEnabled(
feeState: SendStates.FeeState,
notifications: ImmutableList<SendFeeNotification>,
notifications: ImmutableList<SendNotification>,
): Boolean {
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false
val customValue = feeSelectorState.customValues.firstOrNull()
@ -134,7 +135,7 @@ internal class FeeStateFactory(
} else {
false
}
val noErrors = notifications.none { it is SendFeeNotification.Error }
val noErrors = notifications.none { it is SendNotification.Error }
return noErrors && (isNotEmptyCustom || isNotCustom)
}

View file

@ -1,88 +0,0 @@
package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.send.impl.R
sealed class SendFeeNotification(val config: NotificationConfig) {
sealed class Warning(
val title: TextReference,
val subtitle: TextReference,
val buttonsState: NotificationConfig.ButtonsState? = null,
) : SendFeeNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = R.drawable.img_attention_20,
buttonsState = buttonsState,
),
) {
data class TooHigh(
val value: String,
) : Warning(
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
)
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onRefresh,
),
)
}
sealed class Error(
val title: TextReference,
val subtitle: TextReference,
val iconResId: Int,
val buttonsState: NotificationConfig.ButtonsState? = null,
) : SendFeeNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonsState,
),
) {
data class ExceedsBalance(
val networkIconId: Int,
val currencyName: String,
val feeName: String,
val feeSymbol: String,
val networkName: String,
val mergeFeeNetworkName: Boolean = false,
val onClick: (() -> Unit)? = null,
) : Error(
title = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_title,
wrappedList(feeName),
),
subtitle = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_message,
formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol),
),
iconResId = networkIconId,
buttonsState = onClick?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(
R.string.common_buy_currency,
wrappedList(
if (mergeFeeNetworkName) {
"$currencyName ($feeSymbol)"
} else {
feeName
},
),
),
onClick = onClick,
)
},
)
}
}

View file

@ -22,6 +22,8 @@ internal object AmountStatePreviewData {
walletBalance = stringReference("123.123"),
tokenIconState = TokenIconState.Loading,
segmentedButtonConfig = persistentListOf(),
notifications = persistentListOf(),
isFeeLoading = false,
amountTextField = SendTextField.AmountField(
value = "123.123123123123123123",
onValueChange = {},

View file

@ -0,0 +1,109 @@
package com.tangem.features.send.impl.presentation.ui.amount
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.PersistentList
private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
internal fun LazyListScope.buttons(
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
clickIntents: SendClickIntents,
) {
item(
key = AMOUNT_BUTTONS_KEY,
) {
val hapticFeedback = LocalHapticFeedback.current
Row(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
) {
if (segmentedButtonConfig.isNotEmpty()) {
SegmentedButtons(
modifier = Modifier
.weight(1f)
.height(TangemTheme.dimens.size40),
config = segmentedButtonConfig,
showIndication = false,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onCurrencyChangeClick(it.isFiat)
},
) {
SendAmountCurrencyButton(it)
}
} else {
SpacerWMax()
}
SecondaryButton(
text = stringResource(R.string.send_max_amount),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onMaxValueClick()
},
size = TangemButtonSize.Text,
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.height(TangemTheme.dimens.size40),
)
}
}
}
@Composable
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = TangemTheme.dimens.spacing10,
),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
if (button.isFiat) {
FiatIcon(
url = button.iconUrl,
size = TangemTheme.dimens.size18,
modifier = Modifier.size(TangemTheme.dimens.size18),
)
} else {
button.iconState?.let {
TokenIcon(
state = it,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size18),
)
}
}
Text(
text = button.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.button,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing8,
),
)
}
}

View file

@ -5,9 +5,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -18,50 +18,52 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendStates
@Composable
internal fun AmountFieldContainer(
private const val AMOUNT_FIELD_KEY = "amountFieldKey"
internal fun LazyListScope.amountField(
amountState: SendStates.AmountState,
isBalanceHiding: Boolean,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.action),
) {
Text(
text = amountState.walletName,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing14),
)
val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference()
AnimatedContent(
targetState = balance,
label = "Hide Balance Animation",
item(key = AMOUNT_FIELD_KEY) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.action),
) {
Text(
text = it,
style = TangemTheme.typography.caption2,
text = amountState.walletName,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
.padding(top = TangemTheme.dimens.spacing14),
)
val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference()
AnimatedContent(
targetState = balance,
label = "Hide Balance Animation",
) {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
)
}
TokenIcon(
state = amountState.tokenIconState,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32),
)
AmountField(
sendField = amountState.amountTextField,
isFiat = amountState.amountTextField.isFiatValue,
)
}
TokenIcon(
state = amountState.tokenIconState,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32),
)
AmountField(
sendField = amountState.amountTextField,
isFiat = amountState.amountTextField.isFiatValue,
)
}
}

View file

@ -1,27 +1,19 @@
package com.tangem.features.send.impl.presentation.ui.amount
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
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.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun SendAmountContent(
@ -30,88 +22,38 @@ internal fun SendAmountContent(
clickIntents: SendClickIntents,
) {
if (amountState == null) return
val hapticFeedback = LocalHapticFeedback.current
Column(
LazyColumn(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.background(TangemTheme.colors.background.tertiary),
) {
AmountFieldContainer(amountState = amountState, isBalanceHiding = isBalanceHiding)
Row(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing12,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
),
) {
if (amountState.segmentedButtonConfig.isNotEmpty()) {
SegmentedButtons(
modifier = Modifier
.weight(1f)
.height(TangemTheme.dimens.size40),
config = amountState.segmentedButtonConfig,
showIndication = false,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onCurrencyChangeClick(it.isFiat)
},
) {
SendAmountCurrencyButton(it)
}
} else {
SpacerWMax()
}
SecondaryButton(
text = stringResource(R.string.send_max_amount),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onMaxValueClick()
},
size = TangemButtonSize.Text,
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.height(TangemTheme.dimens.size40),
)
}
amountField(amountState = amountState, isBalanceHiding = isBalanceHiding)
buttons(amountState.segmentedButtonConfig, clickIntents)
notifications(amountState.notifications)
}
}
@Composable
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = TangemTheme.dimens.spacing10,
),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
if (button.isFiat) {
FiatIcon(
url = button.iconUrl,
size = TangemTheme.dimens.size18,
modifier = Modifier.size(TangemTheme.dimens.size18),
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.notifications(configs: ImmutableList<SendNotification>, modifier: Modifier = Modifier) {
items(
items = configs,
key = { it::class.java },
contentType = { it::class.java },
itemContent = {
val bottomPadding = if (it == configs.last()) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0
Notification(
config = it.config,
modifier = modifier
.animateItemPlacement()
.padding(
top = TangemTheme.dimens.spacing12,
bottom = bottomPadding,
),
containerColor = when (it) {
is SendNotification.Warning.NetworkFeeUnreachable -> TangemTheme.colors.background.action
else -> TangemTheme.colors.button.disabled
},
)
} else {
button.iconState?.let {
TokenIcon(
state = it,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size18),
)
}
}
Text(
text = button.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.button,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing8,
),
)
}
},
)
}

View file

@ -13,9 +13,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
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.SendFeeNotification
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
@ -56,7 +56,7 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents:
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.notifications(
configs: ImmutableList<SendFeeNotification>,
configs: ImmutableList<SendNotification>,
modifier: Modifier = Modifier,
isLast: Boolean = false,
) {
@ -75,13 +75,13 @@ private fun LazyListScope.notifications(
)
.animateItemPlacement(),
containerColor = when (it) {
is SendFeeNotification.Error.ExceedsBalance,
is SendFeeNotification.Warning.NetworkFeeUnreachable,
is SendNotification.Error.ExceedsBalance,
is SendNotification.Warning.NetworkFeeUnreachable,
-> TangemTheme.colors.background.action
else -> TangemTheme.colors.button.disabled
},
iconTint = when (it) {
is SendFeeNotification.Error.ExceedsBalance -> {
is SendNotification.Error.ExceedsBalance -> {
if (it.config.buttonsState == null) {
TangemTheme.colors.icon.warning
} else {

View file

@ -13,7 +13,6 @@ 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.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -40,10 +39,10 @@ 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.fee.SendFeeNotification
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import java.math.BigDecimal
@ -113,7 +112,7 @@ internal fun SendSpeedSelector(
visible = fees.normal is Fee.Ethereum,
label = "Custom fee appearance animation",
) {
val showWarning = state.notifications.any { it is SendFeeNotification.Warning.TooHigh }
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,

View file

@ -146,12 +146,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendNotification
config = it.config,
modifier = modifier.animateItemPlacement(),
containerColor = when (it) {
is SendNotification.Warning.HighFeeError -> TangemTheme.colors.background.action
is SendNotification.Error.ExceedsBalance,
is SendNotification.Warning.HighFeeError,
-> TangemTheme.colors.background.action
else -> TangemTheme.colors.button.disabled
},
iconTint = when (it) {
is SendNotification.Error.ExceedsBalance,
is SendNotification.Warning,
-> null
is SendNotification.Error -> TangemTheme.colors.icon.warning
is SendNotification.Warning -> null
},
)
},

View file

@ -47,7 +47,9 @@ import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.amount.AmountNotificationFactory
import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
@ -146,15 +148,16 @@ internal class SendViewModel @Inject constructor(
feeStateFactory = feeStateFactory,
)
private val feeNotificationFactory = FeeNotificationFactory(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
private val amountNotificationFactory = AmountNotificationFactory(
currentStateProvider = Provider { uiState },
stateRouterProvider = Provider { stateRouter },
clickIntents = this,
)
private val feeNotificationFactory = FeeNotificationFactory(
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
stateRouterProvider = Provider { stateRouter },
clickIntents = this,
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
analyticsEventHandler = analyticsEventHandler,
)
private val sendNotificationFactory = SendNotificationFactory(
@ -166,6 +169,7 @@ internal class SendViewModel @Inject constructor(
currencyChecksRepository = currencyChecksRepository,
clickIntents = this,
analyticsEventHandler = analyticsEventHandler,
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
)
private val sendOnNextScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
@ -191,6 +195,7 @@ internal class SendViewModel @Inject constructor(
private var memoValidationJobHolder = JobHolder()
private var sendNotificationsJobHolder = JobHolder()
private var feeNotificationsJobHolder = JobHolder()
private var amountNotificationsJobHolder = JobHolder()
private var qrScannerJobHolder = JobHolder()
private var sendIdleTimer = 0L
@ -465,6 +470,16 @@ internal class SendViewModel @Inject constructor(
.saveIn(feeNotificationsJobHolder)
}
private fun updateAmountNotifications() {
amountNotificationFactory.create()
.conflate()
.distinctUntilChanged()
.onEach { uiState = amountStateFactory.getAmountNotificationState(notifications = it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(amountNotificationsJobHolder)
}
// region screen state navigation
override fun popBackStack() = stateRouter.popBackStack()
override fun onBackClick() = stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true)
@ -643,6 +658,7 @@ internal class SendViewModel @Inject constructor(
ifLeft = { feeStateFactory.onFeeOnErrorState() },
) ?: feeStateFactory.onFeeOnErrorState()
updateFeeNotifications()
updateAmountNotifications()
}.saveIn(feeJobHolder)
}