Updated on 2026-08-14
This commit is contained in:
parent
b2cadc30bc
commit
ec0a3a136d
9 changed files with 207 additions and 179 deletions
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state
|
|||
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -9,6 +10,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.features.send.impl.R
|
||||
|
|
@ -36,6 +38,7 @@ internal class SendStateFactory(
|
|||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
|
|
@ -94,7 +97,6 @@ internal class SendStateFactory(
|
|||
amountState = amountStateConverter.convert(Unit),
|
||||
recipientState = recipientStateConverter.convert(Unit),
|
||||
feeState = feeStateConverter.convert(Unit),
|
||||
sendState = SendStates.SendState(),
|
||||
)
|
||||
//endregion
|
||||
|
||||
|
|
@ -227,17 +229,21 @@ internal class SendStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee, isMaxAmount: Boolean): SendUiState {
|
||||
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
receivedAmount = feeSelectorState.updateReceiveAmount(),
|
||||
isSubtract = isMaxAmount,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
|
|
@ -251,9 +257,15 @@ internal class SendStateFactory(
|
|||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
|
||||
val updatedState = feeState.copy(
|
||||
receivedAmount = updatedFeeSelectorState.updateReceiveAmount(),
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
|
|
@ -274,9 +286,15 @@ internal class SendStateFactory(
|
|||
set(index, feeSelectorState.customValues[index].copy(value = value))
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
receivedAmount = updatedFeeSelectorState.updateReceiveAmount(),
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
return state.copy(
|
||||
|
|
@ -290,10 +308,15 @@ internal class SendStateFactory(
|
|||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
val updatedState = feeState.copy(
|
||||
isSubtract = value,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = if (value) {
|
||||
feeSelectorState.updateReceiveAmount()
|
||||
getFormattedValue(receivedAmount)
|
||||
} else {
|
||||
feeState.receivedAmount
|
||||
},
|
||||
|
|
@ -309,15 +332,9 @@ internal class SendStateFactory(
|
|||
return when (this) {
|
||||
is FeeSelectorState.Loading -> false
|
||||
is FeeSelectorState.Content -> {
|
||||
val choosableFee = fees as? TransactionFee.Choosable
|
||||
val customValue = customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = when (selectedFee) {
|
||||
FeeType.SLOW -> choosableFee?.minimum?.amount?.value
|
||||
FeeType.MARKET -> fees.normal.amount.value
|
||||
FeeType.FAST -> choosableFee?.priority?.amount?.value
|
||||
FeeType.CUSTOM -> customValue
|
||||
} ?: BigDecimal.ZERO
|
||||
val fee = getFee().amount.value ?: BigDecimal.ZERO
|
||||
|
||||
val isNotEmptyCustom = !customValue.isNullOrZero() && selectedFee == FeeType.CUSTOM
|
||||
val isNotCustom = selectedFee != FeeType.CUSTOM
|
||||
|
|
@ -326,13 +343,37 @@ internal class SendStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun FeeSelectorState.Content.updateReceiveAmount(): String {
|
||||
private fun getFormattedValue(value: BigDecimal): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = calculateReceiveAmount(currentStateProvider()),
|
||||
cryptoAmount = value,
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region send
|
||||
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(sendState = state.sendState.copy(isSending = isSending))
|
||||
}
|
||||
|
||||
fun getTransactionSendState(txData: TransactionData): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = txData.hash.orEmpty(),
|
||||
networkId = cryptoCurrency.network.id,
|
||||
)
|
||||
return state.copy(
|
||||
sendState = state.sendState.copy(
|
||||
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
|
||||
isSuccess = true,
|
||||
txUrl = txUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -16,6 +17,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Ui states of the send screen
|
||||
|
|
@ -26,7 +28,7 @@ internal data class SendUiState(
|
|||
val amountState: SendStates.AmountState? = null,
|
||||
val recipientState: SendStates.RecipientState? = null,
|
||||
val feeState: SendStates.FeeState? = null,
|
||||
val sendState: SendStates.SendState? = null,
|
||||
val sendState: SendStates.SendState = SendStates.SendState(),
|
||||
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val currentState: MutableStateFlow<SendUiStateType>,
|
||||
)
|
||||
|
|
@ -36,9 +38,12 @@ internal sealed class SendStates {
|
|||
|
||||
abstract val type: SendUiStateType
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
override val type: SendUiStateType = SendUiStateType.Amount,
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val walletName: String,
|
||||
|
|
@ -47,38 +52,40 @@ internal sealed class SendStates {
|
|||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
val isPrimaryButtonEnabled: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val type: SendUiStateType = SendUiStateType.Recipient,
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val addressTextField: SendTextField.RecipientAddress,
|
||||
val memoTextField: SendTextField.RecipientMemo?,
|
||||
val recipients: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val network: String,
|
||||
val isPrimaryButtonEnabled: Boolean,
|
||||
val isValidating: Boolean = false,
|
||||
) : SendStates()
|
||||
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val type: SendUiStateType = SendUiStateType.Fee,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading,
|
||||
val isSubtract: Boolean = false,
|
||||
val fee: Fee? = null,
|
||||
val receivedAmountValue: BigDecimal = BigDecimal.ZERO,
|
||||
val receivedAmount: String = "",
|
||||
val notifications: ImmutableList<SendFeeNotification> = persistentListOf(),
|
||||
val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : SendStates()
|
||||
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
override val type: SendUiStateType = SendUiStateType.Send,
|
||||
val isSending: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val isSuccess: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val transactionDate: MutableStateFlow<Long> = MutableStateFlow(0L),
|
||||
val txUrl: MutableStateFlow<String> = MutableStateFlow(""),
|
||||
override val isPrimaryButtonEnabled: Boolean = true,
|
||||
val isSending: Boolean = false,
|
||||
val isSuccess: Boolean = false,
|
||||
val transactionDate: Long = 0L,
|
||||
val txUrl: String = "",
|
||||
) : SendStates()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -7,20 +8,33 @@ import java.math.BigDecimal
|
|||
/**
|
||||
* Calculate receiving amount when fee is subtracted from sending amount
|
||||
*/
|
||||
internal fun FeeSelectorState.Content.calculateReceiveAmount(uiState: SendUiState): BigDecimal {
|
||||
internal fun calculateReceiveAmount(uiState: SendUiState, feeAmount: Fee): BigDecimal {
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO
|
||||
val fee = feeAmount.amount.value ?: return BigDecimal.ZERO
|
||||
return BigDecimal(amount).minus(fee)
|
||||
}
|
||||
|
||||
val fee = when (fees) {
|
||||
/**
|
||||
* Returns fee amount depending on current state
|
||||
*/
|
||||
internal fun FeeSelectorState.Content.getFee(): Fee {
|
||||
return when (fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (selectedFee) {
|
||||
FeeType.SLOW -> fees.minimum.amount.value
|
||||
FeeType.MARKET -> fees.normal.amount.value
|
||||
FeeType.FAST -> fees.priority.amount.value
|
||||
FeeType.CUSTOM -> customValues.firstOrNull()?.value?.let { BigDecimal(it.ifEmpty { "0" }) }
|
||||
FeeType.SLOW -> fees.minimum
|
||||
FeeType.MARKET -> fees.normal
|
||||
FeeType.FAST -> fees.priority
|
||||
FeeType.CUSTOM -> {
|
||||
val feeAmount =
|
||||
customValues.firstOrNull()?.value?.let { BigDecimal(it.ifEmpty { "0" }) } ?: BigDecimal.ZERO
|
||||
Fee.Common(
|
||||
fees.normal.amount.copy(
|
||||
value = feeAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> fees.normal.amount.value
|
||||
} ?: BigDecimal.ZERO
|
||||
|
||||
return BigDecimal(amount).minus(fee)
|
||||
is TransactionFee.Single -> fees.normal
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,7 @@ internal class FeeNotificationFactory(
|
|||
feeSelectorState: FeeSelectorState.Content,
|
||||
) {
|
||||
val coinCryptoCurrency = coinCryptoCurrencyStatusProvider()
|
||||
val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO
|
||||
val choosableFee = feeSelectorState.fees as? TransactionFee.Choosable
|
||||
val fee = when (feeSelectorState.selectedFee) {
|
||||
FeeType.SLOW -> choosableFee?.minimum?.amount?.value
|
||||
|
|
@ -76,7 +77,7 @@ internal class FeeNotificationFactory(
|
|||
FeeType.CUSTOM -> feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
} ?: return
|
||||
|
||||
if (fee > coinCryptoCurrency.value.amount) {
|
||||
if (fee > cryptoAmount) {
|
||||
add(
|
||||
SendFeeNotification.Error.ExceedsBalance(
|
||||
coinCryptoCurrency.currency.networkIconResId,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ internal class SendAmountFieldChangeConverter(
|
|||
override fun convert(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
val feeState = state.feeState ?: return state
|
||||
|
||||
if (value.checkDecimalSeparatorDuplicate()) return state
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
|
@ -40,7 +42,8 @@ internal class SendAmountFieldChangeConverter(
|
|||
trimmedValue
|
||||
}
|
||||
|
||||
val isExceedBalance = value.checkExceedBalance(amountState.cryptoCurrencyStatus, amountState)
|
||||
val isExceedBalance = cryptoValue.checkExceedBalance(amountState)
|
||||
val isMaxAmount = cryptoValue.checkMaxAmount(amountState)
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = !isExceedBalance,
|
||||
|
|
@ -50,6 +53,9 @@ internal class SendAmountFieldChangeConverter(
|
|||
isError = isExceedBalance,
|
||||
),
|
||||
),
|
||||
feeState = feeState.copy(
|
||||
isSubtract = isMaxAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -73,15 +79,26 @@ internal class SendAmountFieldChangeConverter(
|
|||
return decimalSeparatorCount > 1
|
||||
}
|
||||
|
||||
private fun String.checkExceedBalance(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
state: SendStates.AmountState,
|
||||
): Boolean {
|
||||
val currencyStatus = cryptoCurrencyStatus.value
|
||||
private fun String.checkExceedBalance(state: SendStates.AmountState): Boolean {
|
||||
val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() > currencyStatus.fiatAmount
|
||||
toBigDecimal() > currencyFiatAmount
|
||||
} else {
|
||||
toBigDecimal() > currencyStatus.amount
|
||||
toBigDecimal() > currencyCryptoAmount
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.checkMaxAmount(state: SendStates.AmountState): Boolean {
|
||||
// If current currency is Token
|
||||
if (state.cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false
|
||||
|
||||
val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() == currencyFiatAmount
|
||||
} else {
|
||||
toBigDecimal() == currencyCryptoAmount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,9 +72,9 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) {
|
|||
@Composable
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()?.value ?: false
|
||||
val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false
|
||||
val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty()
|
||||
val isSuccess = uiState.sendState.isSuccess
|
||||
val isSending = uiState.sendState.isSending
|
||||
val txUrl = uiState.sendState.txUrl
|
||||
|
||||
val (buttonTextId, buttonClick) = getButtonData(
|
||||
currentState = currentState,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import com.tangem.features.send.impl.presentation.ui.send.SendContent
|
|||
@Composable
|
||||
internal fun SendScreen(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()
|
||||
val isSuccess = uiState.sendState.isSuccess
|
||||
BackHandler { uiState.clickIntents.onBackClick() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -41,7 +41,7 @@ internal fun SendScreen(uiState: SendUiState) {
|
|||
SendUiStateType.Amount -> R.string.send_amount_label
|
||||
SendUiStateType.Recipient -> R.string.send_recipient_label
|
||||
SendUiStateType.Fee -> R.string.common_fee_selector_title
|
||||
SendUiStateType.Send -> if (isSuccess?.value == false) R.string.send_confirm_label else null
|
||||
SendUiStateType.Send -> if (!isSuccess) R.string.send_confirm_label else null
|
||||
}
|
||||
val iconRes = if (currentState.value == SendUiStateType.Recipient) {
|
||||
R.drawable.ic_qrcode_scan_24
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ package com.tangem.features.send.impl.presentation.ui.send
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -17,8 +14,6 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
|
||||
import com.tangem.core.ui.components.inputrow.InputRowDefault
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImage
|
||||
|
|
@ -31,8 +26,6 @@ import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
|
|||
import com.tangem.features.send.impl.R
|
||||
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.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
|
@ -40,43 +33,48 @@ internal fun SendContent(uiState: SendUiState) {
|
|||
val amountState = uiState.amountState ?: return
|
||||
val recipientState = uiState.recipientState ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.sendState
|
||||
|
||||
val isSuccess = sendState.isSuccess.collectAsStateWithLifecycle()
|
||||
val timestamp = sendState.transactionDate.collectAsStateWithLifecycle()
|
||||
val isSuccess = sendState.isSuccess
|
||||
val timestamp = sendState.transactionDate
|
||||
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AnimatedVisibility(visible = isSuccess.value) {
|
||||
TransactionDoneTitle(
|
||||
titleRes = R.string.sent_transaction_sent_title,
|
||||
date = timestamp.value,
|
||||
)
|
||||
item {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12)) {
|
||||
AnimatedVisibility(visible = isSuccess) {
|
||||
TransactionDoneTitle(
|
||||
titleRes = R.string.sent_transaction_sent_title,
|
||||
date = timestamp,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = !isSuccess) {
|
||||
FromWallet(
|
||||
walletName = amountState.walletName,
|
||||
walletBalance = amountState.walletBalance,
|
||||
)
|
||||
}
|
||||
AmountBlock(
|
||||
amountState = amountState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showAmount,
|
||||
)
|
||||
RecipientBlock(
|
||||
recipientState = recipientState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showRecipient,
|
||||
)
|
||||
FeeBlock(
|
||||
feeState = feeState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showFee,
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = !isSuccess.value) {
|
||||
FromWallet(
|
||||
walletName = amountState.walletName,
|
||||
walletBalance = amountState.walletBalance,
|
||||
)
|
||||
}
|
||||
AmountBlock(
|
||||
amountState = amountState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showAmount,
|
||||
)
|
||||
RecipientBlock(
|
||||
recipientState = recipientState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showRecipient,
|
||||
)
|
||||
FeeBlock(
|
||||
feeState = feeState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showFee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +111,7 @@ private fun FromWallet(walletName: String, walletBalance: String) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State<Boolean>, onClick: () -> Unit) {
|
||||
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean, onClick: () -> Unit) {
|
||||
val amount = amountState.amountTextField
|
||||
|
||||
val cryptoAmount = formatCryptoAmount(
|
||||
|
|
@ -134,12 +132,12 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State<Bo
|
|||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess.value) { onClick() },
|
||||
.clickable(enabled = !isSuccess) { onClick() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State<Boolean>, onClick: () -> Unit) {
|
||||
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: Boolean, onClick: () -> Unit) {
|
||||
val address = recipientState.addressTextField
|
||||
val memo = recipientState.memoTextField
|
||||
|
||||
|
|
@ -147,7 +145,7 @@ private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess:
|
|||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess.value) { onClick() },
|
||||
.clickable(enabled = !isSuccess) { onClick() },
|
||||
) {
|
||||
val showMemo = memo != null && memo.value.isNotBlank()
|
||||
InputRowRecipientDefault(
|
||||
|
|
@ -165,22 +163,12 @@ private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess:
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State<Boolean>, onClick: () -> Unit) {
|
||||
val feeSelector = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val customValue = feeSelector.customValues.getOrNull(0)
|
||||
val selectedFee = feeSelector.selectedFee
|
||||
|
||||
private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) {
|
||||
val fee = feeState.fee ?: return
|
||||
val feeValue = formatCryptoAmount(
|
||||
cryptoCurrency = feeState.cryptoCurrencyStatus.currency,
|
||||
cryptoAmount = when (val fees = feeSelector.fees) {
|
||||
is TransactionFee.Single -> fees.normal.amount.value
|
||||
is TransactionFee.Choosable -> when (selectedFee) {
|
||||
FeeType.SLOW -> fees.minimum.amount.value
|
||||
FeeType.MARKET -> fees.normal.amount.value
|
||||
FeeType.FAST -> fees.priority.amount.value
|
||||
FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault()
|
||||
}
|
||||
},
|
||||
cryptoAmount = fee.amount.value,
|
||||
cryptoCurrency = fee.amount.currencySymbol,
|
||||
decimals = fee.amount.decimals,
|
||||
)
|
||||
InputRowDefault(
|
||||
title = TextReference.Res(R.string.send_network_fee_title),
|
||||
|
|
@ -188,6 +176,6 @@ private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State<Boolean>, o
|
|||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess.value) { onClick() },
|
||||
.clickable(enabled = !isSuccess) { onClick() },
|
||||
)
|
||||
}
|
||||
|
|
@ -8,8 +8,6 @@ import androidx.paging.PagingData
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
|
|
@ -35,12 +33,10 @@ 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.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.SendStateFactory
|
||||
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.*
|
||||
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.getFee
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -52,7 +48,6 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
|
@ -71,9 +66,9 @@ internal class SendViewModel @Inject constructor(
|
|||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
|
@ -98,6 +93,7 @@ internal class SendViewModel @Inject constructor(
|
|||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
)
|
||||
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
|
|
@ -292,7 +288,7 @@ internal class SendViewModel @Inject constructor(
|
|||
.onEach { maybeFee ->
|
||||
maybeFee.fold(
|
||||
ifRight = {
|
||||
uiState = stateFactory.onFeeOnLoadedState(it, true)
|
||||
uiState = stateFactory.onFeeOnLoadedState(it)
|
||||
},
|
||||
ifLeft = {
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
|
|
@ -316,7 +312,7 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
// region amount state clicks
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
|
|
@ -336,7 +332,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE)
|
||||
}
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
// region recipient state clicks
|
||||
override fun onRecipientAddressValueChange(value: String) {
|
||||
|
|
@ -382,7 +378,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
// endregion
|
||||
|
||||
//region fee
|
||||
// region fee
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
uiState = stateFactory.onFeeSelectedState(feeType)
|
||||
}
|
||||
|
|
@ -394,28 +390,38 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onSubtractSelect(value: Boolean) {
|
||||
uiState = stateFactory.onSubtractSelect(value)
|
||||
}
|
||||
//endregion
|
||||
// endregion
|
||||
|
||||
// region send state clicks
|
||||
override fun onSendClick() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.sendState
|
||||
if (sendState.isSuccess) popBackStack()
|
||||
|
||||
if (sendState.isSuccess.value) popBackStack()
|
||||
sendState.isSending.update { true }
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
verifyAndSendTransaction()
|
||||
}
|
||||
uiState = stateFactory.getSendingStateUpdate(true)
|
||||
viewModelScope.launch(dispatchers.io) { verifyAndSendTransaction() }
|
||||
}
|
||||
|
||||
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
|
||||
|
||||
override fun showRecipient() = stateRouter.showRecipient(isFromSend = true)
|
||||
|
||||
override fun showFee() = stateRouter.showFee(isFromSend = true)
|
||||
|
||||
override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl)
|
||||
|
||||
private suspend fun verifyAndSendTransaction() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return
|
||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val fee = getFee(feeState) ?: return
|
||||
val fee = feeSelectorState.getFee()
|
||||
|
||||
val amountToSend = amount.toBigDecimal().convertToAmount(cryptoCurrency)
|
||||
val amountToSend = if (feeState.isSubtract) {
|
||||
feeState.receivedAmountValue.convertToAmount(cryptoCurrency)
|
||||
} else {
|
||||
amount.toBigDecimal().convertToAmount(cryptoCurrency)
|
||||
}
|
||||
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
// val transactionErrors = walletManagersFacade.validateTransaction(
|
||||
|
|
@ -444,66 +450,20 @@ internal class SendViewModel @Inject constructor(
|
|||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
sendState.isSending.update { false }
|
||||
uiState = stateFactory.getSendingStateUpdate(false)
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = {
|
||||
sendState.transactionDate.update {
|
||||
txData.date?.timeInMillis ?: System.currentTimeMillis()
|
||||
}
|
||||
sendState.isSuccess.update { true }
|
||||
sendState.txUrl.update {
|
||||
getTxUrl(txData.hash.orEmpty())
|
||||
}
|
||||
uiState = stateFactory.getTransactionSendState(txData)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFee(feeState: FeeSelectorState.Content): Fee? {
|
||||
return when (val selectedFee = feeState.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeState.selectedFee) {
|
||||
FeeType.SLOW -> selectedFee.minimum
|
||||
FeeType.MARKET -> selectedFee.normal
|
||||
FeeType.FAST -> selectedFee.priority
|
||||
FeeType.CUSTOM -> {
|
||||
val feeAmount = feeState.customValues.firstOrNull()?.value
|
||||
?.let { BigDecimal(it) } ?: return null
|
||||
Fee.Common(feeAmount.convertToAmount(cryptoCurrency))
|
||||
}
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> selectedFee.normal
|
||||
}
|
||||
}
|
||||
|
||||
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
|
||||
|
||||
override fun showRecipient() = stateRouter.showRecipient(isFromSend = true)
|
||||
|
||||
override fun showFee() = stateRouter.showFee(isFromSend = true)
|
||||
|
||||
override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl)
|
||||
|
||||
private fun getTxUrl(hash: String): String {
|
||||
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
|
||||
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
|
||||
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
|
||||
EMPTY
|
||||
} else {
|
||||
getExplorerTransactionUrlUseCase(
|
||||
txHash = hash,
|
||||
networkId = cryptoCurrency.network.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val DEFAULT_VALUE = "0.00"
|
||||
private const val EMPTY = ""
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue