Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-19 18:11:56 +00:00
commit b76e1bbc45
219 changed files with 4585 additions and 5271 deletions

View file

@ -5,10 +5,10 @@ import androidx.fragment.app.Fragment
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.features.send.impl.presentation.SendFragment
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter

View file

@ -13,8 +13,8 @@ import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.feature.qrscanning.SourceType
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.state.StateRouter

View file

@ -23,12 +23,12 @@ internal sealed class SendNotification(val config: NotificationConfig) {
),
) {
object TotalExceedsBalance : Error(
data object TotalExceedsBalance : Error(
title = resourceReference(R.string.send_notification_exceed_balance_title),
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
)
object InvalidAmount : Error(
data object InvalidAmount : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
)
@ -49,7 +49,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
val amountLimit: String,
val onConfirmClick: () -> Unit,
) : Error(
title = resourceReference(R.string.send_notifiaction_transaction_limit_title),
title = resourceReference(R.string.send_notification_transaction_limit_title),
subtitle = resourceReference(
R.string.send_notification_transaction_limit_text,
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
@ -94,17 +94,6 @@ internal sealed class SendNotification(val config: NotificationConfig) {
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
)
data class NetworkCoverage(
val amountReducedBy: String,
val amountReduced: String,
) : Warning(
title = resourceReference(id = R.string.send_network_fee_warning_title),
subtitle = resourceReference(
id = R.string.send_network_fee_warning_content,
formatArgs = wrappedList(amountReducedBy, amountReduced),
),
)
data object FeeTooLow : Warning(
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),

View file

@ -52,7 +52,6 @@ internal class SendNotificationFactory(
addDustWarningNotification(feeAmount, sendAmount)
addTransactionLimitErrorNotification(feeAmount, sendAmount)
// warnings
addFeeCoverageNotification(sendState.isSubtract, sendAmount)
addExistentialWarningNotification(feeAmount, sendAmount)
addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce)
addTooLowNotification(feeState)
@ -257,29 +256,6 @@ internal class SendNotificationFactory(
}
}
private fun MutableList<SendNotification>.addFeeCoverageNotification(
isSubtract: Boolean,
amountValue: BigDecimal,
) {
val state = currentStateProvider()
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
val feeAmount = state.feeState?.fee?.amount?.value ?: BigDecimal.ZERO
val amountReducedValue = amountValue.minus(feeAmount)
val amountReducedByValue = amountValue.minus(amountReducedValue)
val amountReducedBy = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = amountReducedByValue,
cryptoCurrency = cryptoCurrency,
)
val amountReduced = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = amountReducedValue,
cryptoCurrency = cryptoCurrency,
)
if (isSubtract) {
add(SendNotification.Warning.NetworkCoverage(amountReducedBy, amountReduced))
}
}
private fun MutableList<SendNotification>.addTooLowNotification(feeState: SendStates.FeeState) {
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.txhistory.models.TxHistoryItem
@ -31,6 +32,7 @@ internal class SendStateFactory(
private val userWalletProvider: Provider<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
) {
@ -62,7 +64,7 @@ internal class SendStateFactory(
private val feeStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendFeeStateConverter(
appCurrencyProvider = appCurrencyProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
@ -85,16 +87,18 @@ internal class SendStateFactory(
val state = currentStateProvider()
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(""),
recipientState = state.recipientState ?: recipientStateConverter.convert(""),
recipientState = state.recipientState
?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
)
}
fun getReadyState(amount: String, destinationAddress: String): SendUiState {
fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(amount),
recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress),
recipientState = state.recipientState
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
isEditingDisabled = true,
)
@ -214,9 +218,20 @@ internal class SendStateFactory(
//endregion
//region send
fun onSubtractSelect(isSubtract: Boolean): SendUiState {
fun onSubtractSelect(isSubtract: Boolean, isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()
val fee = state.feeState?.fee ?: return state
val amountState = state.amountState ?: return state
val amount = amountState.amountTextField.cryptoAmount
val amountValue = amount.value ?: return state
val amountToSend = if (isSubtract && isAmountSubtractAvailable) {
val feeValue = fee.amount.value ?: return state
amountValue.minus(feeValue)
} else {
amountValue
}
return state.copy(
amountState = amountStateConverter.convert(amountToSend.parseBigDecimal(amount.decimals)),
sendState = state.sendState.copy(isSubtract = isSubtract),
)
}

View file

@ -14,6 +14,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import kotlinx.collections.immutable.persistentListOf
internal class SendAmountStateConverter(
@ -37,18 +38,22 @@ internal class SendAmountStateConverter(
tokenIconState = iconStateConverter.convert(status),
amountTextField = sendAmountFieldConverter.convert(value),
isPrimaryButtonEnabled = false,
segmentedButtonConfig = persistentListOf(
SendAmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol),
iconState = iconStateConverter.convert(status),
isFiat = false,
),
SendAmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
),
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
persistentListOf()
} else {
persistentListOf(
SendAmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol),
iconState = iconStateConverter.convert(status),
isFiat = false,
),
SendAmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
)
},
)
}
}

View file

@ -13,14 +13,14 @@ import com.tangem.utils.converter.Converter
internal class FeeConverter(
private val clickIntents: SendClickIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<FeeSelectorState.Content, Fee> {
private val ethereumCustomFeeConverter by lazy {
EthereumCustomFeeConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}

View file

@ -20,7 +20,7 @@ import kotlinx.collections.immutable.persistentListOf
internal class FeeStateFactory(
private val clickIntents: SendClickIntents,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
) {
@ -28,7 +28,7 @@ internal class FeeStateFactory(
SendFeeCustomFieldConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
@ -36,7 +36,7 @@ internal class FeeStateFactory(
FeeConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
@ -140,7 +140,7 @@ internal class FeeStateFactory(
}
private fun isFeeApproximate(fee: Fee): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
return isFeeApproximateUseCase(
networkId = cryptoCurrencyStatus.currency.network.id,
amountType = fee.amount.type,

View file

@ -14,14 +14,14 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendFeeCustomFieldConverter(
private val clickIntents: SendClickIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Fee, ImmutableList<SendTextField.CustomFee>> {
private val ethereumCustomFeeConverter by lazy {
EthereumCustomFeeConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}

View file

@ -9,16 +9,15 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendFeeStateConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Unit, SendStates.FeeState> {
override fun convert(value: Unit): SendStates.FeeState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
return SendStates.FeeState(
feeSelectorState = FeeSelectorState.Loading,
fee = null,
notifications = persistentListOf(),
rate = cryptoCurrencyStatus.value.fiatRate,
rate = feeCryptoCurrencyStatusProvider().value.fiatRate,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
)

View file

@ -28,7 +28,7 @@ import java.math.RoundingMode
internal class EthereumCustomFeeConverter(
private val clickIntents: SendClickIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Fee.Ethereum, ImmutableList<SendTextField.CustomFee>> {
override fun convert(value: Fee.Ethereum): ImmutableList<SendTextField.CustomFee> {
@ -152,7 +152,7 @@ internal class EthereumCustomFeeConverter(
private fun getFeeFormatted(fee: BigDecimal?): TextReference {
val appCurrency = appCurrencyProvider()
val rate = cryptoCurrencyStatusProvider().value.fiatRate
val rate = feeCryptoCurrencyStatusProvider().value.fiatRate
val fiatFee = rate?.let { fee?.multiply(it) }
return stringReference(
BigDecimalFormatter.formatFiatAmount(
@ -164,7 +164,7 @@ internal class EthereumCustomFeeConverter(
}
private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount

View file

@ -31,10 +31,11 @@ internal class SendAmountFieldConverter(
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val cryptoDecimal = value.toBigDecimalOrDefault()
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val fiatValue = if (value.isEmpty()) {
""
} else {
val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
fiatDecimal.parseBigDecimal(FIAT_DECIMALS)
}
val isDoneActionEnabled = !cryptoDecimal.isZero()
@ -52,6 +53,7 @@ internal class SendAmountFieldConverter(
fiatAmount = getAppCurrencyAmount(appCurrencyProvider()),
isError = false,
error = TextReference.Res(R.string.swapping_insufficient_funds),
isFiatUnavailable = fiatRate == null,
)
}

View file

@ -27,6 +27,7 @@ internal sealed class SendTextField {
val fiatAmount: Amount,
val isFiatValue: Boolean,
val fiatValue: String,
val isFiatUnavailable: Boolean,
val isError: Boolean,
val error: TextReference,
) : SendTextField()

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.state.recipient
import androidx.annotation.StringRes
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@ -15,13 +16,18 @@ import com.tangem.utils.converter.Converter
internal class SendRecipientMemoFieldConverter(
private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>,
) : Converter<Int, SendTextField.RecipientMemo> {
) : Converter<SendRecipientMemoFieldConverter.Data, SendTextField.RecipientMemo> {
fun convertOrNull(): SendTextField.RecipientMemo? {
fun convertOrNull(memoValue: String?): SendTextField.RecipientMemo? {
val cryptoCurrency = cryptoCurrencyStatus().currency
val memo = memoValue ?: ""
return when (cryptoCurrency.network.id.value) {
Blockchain.XRP.id -> convert(R.string.send_destination_tag_field)
Blockchain.XRP.id -> {
convert(
value = Data(memo = memo, label = R.string.send_destination_tag_field),
)
}
Blockchain.Binance.id,
Blockchain.TON.id,
Blockchain.Cosmos.id,
@ -30,24 +36,30 @@ internal class SendRecipientMemoFieldConverter(
Blockchain.Stellar.id,
Blockchain.Hedera.id,
Blockchain.Algorand.id,
-> convert(R.string.send_extras_hint_memo)
-> {
convert(
value = Data(memo = memo, label = R.string.send_extras_hint_memo),
)
}
else -> null
}
}
override fun convert(value: Int): SendTextField.RecipientMemo {
override fun convert(value: Data): SendTextField.RecipientMemo {
return SendTextField.RecipientMemo(
value = "",
value = value.memo,
onValueChange = clickIntents::onRecipientMemoValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text,
),
placeholder = resourceReference(R.string.send_optional_field),
label = resourceReference(value),
label = resourceReference(value.label),
error = resourceReference(R.string.send_memo_destination_tag_error),
disabledText = resourceReference(R.string.send_additional_field_already_included),
isEnabled = true,
)
}
data class Data(val memo: String, @StringRes val label: Int)
}

View file

@ -10,7 +10,7 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendRecipientStateConverter(
private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<String, SendStates.RecipientState> {
) : Converter<SendRecipientStateConverter.Data, SendStates.RecipientState> {
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
private val memoFieldConverter by lazy {
@ -20,14 +20,16 @@ internal class SendRecipientStateConverter(
)
}
override fun convert(value: String): SendStates.RecipientState {
override fun convert(value: Data): SendStates.RecipientState {
return SendStates.RecipientState(
addressTextField = addressFieldConverter.convert(value),
memoTextField = memoFieldConverter.convertOrNull(),
addressTextField = addressFieldConverter.convert(value.address),
memoTextField = memoFieldConverter.convertOrNull(value.memo),
network = cryptoCurrencyStatusProvider().currency.network.name,
isPrimaryButtonEnabled = false,
wallets = persistentListOf(),
recent = persistentListOf(),
)
}
data class Data(val address: String, val memo: String? = null)
}

View file

@ -11,10 +11,12 @@ import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import com.tangem.core.ui.components.fields.AmountTextField
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.defaultFormat
import com.tangem.core.ui.utils.rememberDecimalFormat
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
@ -62,10 +64,14 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean)
end = TangemTheme.dimens.spacing12,
),
) {
val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
val text = if (sendField.isFiatUnavailable) {
BigDecimalFormatter.EMPTY_BALANCE_SIGN
} else {
"${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
}
Text(
text = text,
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier

View file

@ -7,15 +7,17 @@ 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.SendStates
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
@ -28,6 +30,7 @@ internal fun SendAmountContent(
clickIntents: SendClickIntents,
) {
if (amountState == null) return
val hapticFeedback = LocalHapticFeedback.current
Column(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary),
@ -41,19 +44,29 @@ internal fun SendAmountContent(
end = TangemTheme.dimens.spacing16,
),
) {
SegmentedButtons(
modifier = Modifier
.weight(1f)
.height(TangemTheme.dimens.size40),
config = amountState.segmentedButtonConfig,
showIndication = false,
onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) },
) {
SendAmountCurrencyButton(it)
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 = clickIntents::onMaxValueClick,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clickIntents.onMaxValueClick()
},
size = TangemButtonSize.Text,
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
modifier = Modifier

View file

@ -18,7 +18,6 @@ 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
import kotlinx.collections.immutable.toImmutableList
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
@ -37,9 +36,8 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
),
) {
feeSelector(state, clickIntents)
topNotifications(notifications)
customFee(feeSendState)
middleNotifications(notifications)
notifications(notifications)
}
}
@ -56,29 +54,6 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents:
}
}
private fun LazyListScope.topNotifications(
configs: ImmutableList<SendFeeNotification>,
modifier: Modifier = Modifier,
) {
notifications(
configs = configs.filter {
it is SendFeeNotification.Error.ExceedsBalance ||
it is SendFeeNotification.Warning.NetworkFeeUnreachable
}.toImmutableList(),
modifier = modifier,
)
}
private fun LazyListScope.middleNotifications(
configs: ImmutableList<SendFeeNotification>,
modifier: Modifier = Modifier,
) {
notifications(
configs = configs.filterIsInstance<SendFeeNotification.Warning.TooHigh>().toImmutableList(),
modifier = modifier,
)
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.notifications(
configs: ImmutableList<SendFeeNotification>,

View file

@ -12,9 +12,12 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
@ -30,7 +33,10 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
@ -66,17 +72,19 @@ internal class SendViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val reduxStateHolder: ReduxStateHolder,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
currencyChecksRepository: CurrencyChecksRepository,
isFeeApproximateUseCase: IsFeeApproximateUseCase,
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -95,6 +103,7 @@ internal class SendViewModel @Inject constructor(
private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY]
private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY]
private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY]
private val memo: String? = savedStateHandle[SendRouter.TAG_KEY]
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
@ -108,6 +117,7 @@ internal class SendViewModel @Inject constructor(
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
validateWalletMemoUseCase = validateWalletMemoUseCase,
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
)
@ -120,7 +130,7 @@ internal class SendViewModel @Inject constructor(
private val feeStateFactory = FeeStateFactory(
clickIntents = this,
currentStateProvider = Provider { uiState },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
isFeeApproximateUseCase = isFeeApproximateUseCase,
)
@ -165,12 +175,14 @@ internal class SendViewModel @Inject constructor(
private var isAmountSubtractAvailable: Boolean = false
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var balanceJobHolder = JobHolder()
private var balanceHidingJobHolder = JobHolder()
private var recipientsJobHolder = JobHolder()
private var feeJobHolder = JobHolder()
private var addressValidationJobHolder = JobHolder()
private var memoValidationJobHolder = JobHolder()
private var sendNotificationsJobHolder = JobHolder()
private var feeNotificationsJobHolder = JobHolder()
private var qrScannerJobHolder = JobHolder()
@ -204,7 +216,13 @@ internal class SendViewModel @Inject constructor(
ifRight = { wallet ->
userWallet = wallet
checkIfSubtractAvailable()
getCurrenciesStatusUpdates(wallet)
val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
val isMultiCurrency = wallet.isMultiCurrency
getCurrenciesStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
)
},
ifLeft = {
uiState = eventStateFactory.getGenericErrorState(
@ -227,31 +245,38 @@ internal class SendViewModel @Inject constructor(
.saveIn(balanceHidingJobHolder)
}
private fun getCurrenciesStatusUpdates(wallet: UserWallet) {
val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
if (cryptoCurrency is CryptoCurrency.Coin) {
getCurrencyStatusUpdates(isSingleWallet = isSingleWallet)
.onEach { currencyStatus ->
currencyStatus.onRight {
onDataLoaded(
currencyStatus = it,
coinCurrencyStatus = it,
)
}
getCurrencyStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
).onEach { currencyStatus ->
currencyStatus.onRight {
onDataLoaded(
currencyStatus = it,
coinCurrencyStatus = it,
feeCurrencyStatus = getFeeCurrencyStatusSync(it, isMultiCurrency),
)
}
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(balanceJobHolder)
} else {
combine(
flow = getCoinCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
) { coinStatus, currencyStatus ->
if (coinStatus.isRight() && currencyStatus.isRight()) {
flow = getCoinCurrencyStatusUpdates(isSingleWalletWithToken),
flow2 = getCurrencyStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
),
) { maybeCoinStatus, maybeCurrencyStatus ->
if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) {
val currencyStatus = maybeCurrencyStatus.getOrElse { error("Currency status is unreachable") }
val coinStatus = maybeCoinStatus.getOrElse { error("Coin status is unreachable") }
onDataLoaded(
currencyStatus = currencyStatus.getOrElse { error("Currency status is unreachable") },
coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") },
currencyStatus = currencyStatus,
coinCurrencyStatus = coinStatus,
feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency),
)
}
}
@ -261,18 +286,41 @@ internal class SendViewModel @Inject constructor(
}
}
private fun getCoinCurrencyStatusUpdates(isSingleWallet: Boolean) = getNetworkCoinStatusUseCase(
private fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase(
userWalletId = userWalletId,
networkId = cryptoCurrency.network.id,
derivationPath = cryptoCurrency.network.derivationPath,
isSingleWalletWithTokens = isSingleWallet,
isSingleWalletWithTokens = isSingleWalletWithToken,
).conflate().distinctUntilChanged()
private fun getCurrencyStatusUpdates(isSingleWallet: Boolean) = getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = isSingleWallet,
).conflate().distinctUntilChanged()
private fun getCurrencyStatusUpdates(
isSingleWalletWithToken: Boolean,
isMultiCurrency: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return if (isMultiCurrency) {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = isSingleWalletWithToken,
).conflate().distinctUntilChanged()
} else {
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
}
}
private suspend fun getFeeCurrencyStatusSync(
cryptoCurrencyStatus: CryptoCurrencyStatus,
isMultiCurrency: Boolean,
): CryptoCurrencyStatus {
return if (isMultiCurrency) {
getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull() ?: error("Fee currency is unreachable")
} else {
cryptoCurrencyStatus
}
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
@ -286,19 +334,31 @@ internal class SendViewModel @Inject constructor(
)
}
private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus) {
private fun onDataLoaded(
currencyStatus: CryptoCurrencyStatus,
coinCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus,
) {
cryptoCurrencyStatus = currencyStatus
coinCryptoCurrencyStatus = coinCurrencyStatus
feeCryptoCurrencyStatus = feeCurrencyStatus
if (transactionId != null && amount != null && destinationAddress != null) {
uiState = stateFactory.getReadyState(amount, destinationAddress)
stateRouter.showFee()
} else {
getWalletsAndRecent()
uiState = stateFactory.getReadyState()
stateRouter.showRecipient()
when {
uiState.sendState.isSuccess -> {
stateRouter.showSend()
}
transactionId != null && amount != null && destinationAddress != null -> {
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
stateRouter.showFee()
updateNotifications()
}
else -> {
getWalletsAndRecent()
uiState = stateFactory.getReadyState()
stateRouter.showRecipient()
updateNotifications()
}
}
updateNotifications()
}
private fun getWalletsAndRecent() {
@ -406,7 +466,7 @@ internal class SendViewModel @Inject constructor(
)
return
} else {
uiState = stateFactory.onSubtractSelect(false)
uiState = stateFactory.onSubtractSelect(false, isAmountSubtractAvailable)
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
if (checkIfFeeTooLow(uiState)) {
@ -454,13 +514,14 @@ internal class SendViewModel @Inject constructor(
// region recipient state clicks
fun onRecipientAddressScanned(address: String) {
viewModelScope.launch(dispatchers.main) {
parseSharedAddressUseCase(address, cryptoCurrency.network).fold(
parseQrCodeUseCase(address, cryptoCurrency).fold(
ifRight = { parsedCode ->
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) }
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
},
ifLeft = {
onRecipientAddressValueChange(address, EnterAddressSource.QRCode)
Timber.w(it)
},
)
@ -475,6 +536,7 @@ internal class SendViewModel @Inject constructor(
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) }
autoNextFromRecipient(type, isValidAddress)
}
}.saveIn(addressValidationJobHolder)
}
@ -487,7 +549,7 @@ internal class SendViewModel @Inject constructor(
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
}
}.saveIn(addressValidationJobHolder)
}.saveIn(memoValidationJobHolder)
}
private suspend fun validateAddress(value: String): Boolean {
@ -519,6 +581,12 @@ internal class SendViewModel @Inject constructor(
),
)
}
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
val isRecent = type == EnterAddressSource.RecentAddress
val isAddressOnly = uiState.recipientState?.memoTextField == null
if (isRecent && isAddressOnly && isValidAddress) onNextClick()
}
// endregion
// region fee
@ -538,7 +606,7 @@ internal class SendViewModel @Inject constructor(
}
override fun onSubtractSelect() {
uiState = stateFactory.onSubtractSelect(true)
uiState = stateFactory.onSubtractSelect(true, isAmountSubtractAvailable)
stateRouter.showSend()
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true))
}
@ -645,16 +713,10 @@ internal class SendViewModel @Inject constructor(
val fee = feeState.fee ?: return
val memo = uiState.recipientState?.memoTextField?.value
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
val amountToSend = if (uiState.sendState.isSubtract && isAmountSubtractAvailable) {
val feeValue = fee.amount.value ?: return
amountValue.minus(feeValue)
} else {
amountValue
}
viewModelScope.launch(dispatchers.main) {
createTransactionUseCase(
amount = amountToSend.convertToAmount(cryptoCurrency),
amount = amountValue.convertToAmount(cryptoCurrency),
fee = fee,
memo = memo,
destination = recipient,