diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index becf5056a6..608acd39e8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -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, private val cryptoCurrencyStatusProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, coinCryptoCurrencyStatusProvider: Provider, ) { @@ -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 } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 3ff13c540a..a64cbfa738 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -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> = MutableStateFlow(PagingData.empty()), val currentState: MutableStateFlow, ) @@ -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, 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> = 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 = persistentListOf(), - val isPrimaryButtonEnabled: Boolean = false, ) : SendStates() /** Send state */ data class SendState( override val type: SendUiStateType = SendUiStateType.Send, - val isSending: MutableStateFlow = MutableStateFlow(false), - val isSuccess: MutableStateFlow = MutableStateFlow(false), - val transactionDate: MutableStateFlow = MutableStateFlow(0L), - val txUrl: MutableStateFlow = MutableStateFlow(""), + override val isPrimaryButtonEnabled: Boolean = true, + val isSending: Boolean = false, + val isSuccess: Boolean = false, + val transactionDate: Long = 0L, + val txUrl: String = "", ) : SendStates() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index 263f4df2c0..dfbbab99e8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -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 + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index f9706b0d17..fa63899356 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 279e2355b9..90193bd7d5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -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 } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index f64e8a85a8..9b11c51df3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index e20a0b7779..eae5388921 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -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 diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index e488e1a3e0..479fc9843a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -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, 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, 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, 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, o modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .clickable(enabled = !isSuccess.value) { onClick() }, + .clickable(enabled = !isSuccess) { onClick() }, ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 9be54b3b66..36e8b54f06 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -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 = "" } } \ No newline at end of file