Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-25 16:51:41 +01:00
commit 13c92d7ef4
22 changed files with 364 additions and 144 deletions

View file

@ -223,6 +223,10 @@ object TradeCryptoMiddleware {
}
private fun handleSendCoin(action: TradeCryptoAction.SendCoin) {
if (action.transactionInfo?.tag != null) {
// avoid open old send if memo exists
return
}
val cryptoStatus = action.coinStatus
val currency = cryptoStatus.currency
val blockchain = Blockchain.fromId(currency.network.id.value)

View file

@ -197,12 +197,6 @@ class GetCurrencyWarningsUseCase(
coinCurrency = coinStatus.currency,
)
}
feePaidCurrency is FeePaidCurrency.SameCurrency && tokenStatus.value.amount.isZero() -> {
CryptoCurrencyWarning.BalanceNotEnoughForFee(
tokenCurrency = tokenStatus.currency,
coinCurrency = coinStatus.currency,
)
}
feePaidCurrency is FeePaidCurrency.Token -> {
val feePaidTokenBalance = feePaidCurrency.balance
val amount = tokenStatus.value.amount ?: return null

View file

@ -5,18 +5,21 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType
import com.tangem.features.send.impl.presentation.analytics.SelectedFeeType
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.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.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.utils.Provider
internal class SendOnNextScreenAnalyticSender(
private val stateRouterProvider: Provider<StateRouter>,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun send(prevScreen: SendUiStateType, state: SendUiState) {
when (prevScreen) {
SendUiStateType.Fee -> {
val feeState = state.feeState ?: return
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content
feeSelectorState?.selectedFee?.let { selectedFee ->
val isCustomFeeEdited = feeState.fee?.amount?.value != feeSelectorState.fees.normal.amount.value
@ -27,7 +30,8 @@ internal class SendOnNextScreenAnalyticSender(
}
}
SendUiStateType.Amount -> {
val isFiatSelected = state.amountState?.amountTextField?.isFiatValue ?: return
val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return
val isFiatSelected = amountState.amountTextField.isFiatValue
val selectedCurrency = if (!isFiatSelected) {
SelectedCurrencyType.Token
} else {

View file

@ -20,6 +20,7 @@ import java.math.BigDecimal
* @param feeStateFactory [FeeStateFactory]
*/
internal class SendEventStateFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val clickIntents: SendClickIntents,
@ -50,7 +51,8 @@ internal class SendEventStateFactory(
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState {
val state = currentStateProvider()
val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
val feeState = state.getFeeState(stateRouterProvider().isEditState)
val feeSelector = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
val newFee = when (fee) {
is TransactionFee.Single -> fee.normal
is TransactionFee.Choosable -> {

View file

@ -14,6 +14,7 @@ import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
@ -31,6 +32,7 @@ import timber.log.Timber
@Suppress("LongParameterList")
internal class SendStateFactory(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
@ -82,7 +84,9 @@ internal class SendStateFactory(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val sendSyncEditConverter by lazy(LazyThreadSafetyMode.NONE) {
SendSyncEditConverter(currentStateProvider = currentStateProvider)
}
// region UI states
fun getInitialState(): SendUiState = SendUiState(
clickIntents = clickIntents,
@ -117,6 +121,8 @@ internal class SendStateFactory(
)
}
fun syncEditStates(isFromEdit: Boolean) = sendSyncEditConverter.convert(isFromEdit)
fun getOnHideBalanceState(isBalanceHidden: Boolean): SendUiState {
return currentStateProvider().copy(isBalanceHidden = isBalanceHidden)
}
@ -143,8 +149,10 @@ internal class SendStateFactory(
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
addressTextField = recipientState.addressTextField.copy(value = value),
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
@ -155,7 +163,8 @@ internal class SendStateFactory(
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = recipientState.memoTextField?.value.orEmpty(),
@ -167,7 +176,8 @@ internal class SendStateFactory(
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isValidating = false,
@ -185,16 +195,20 @@ internal class SendStateFactory(
fun getOnRecipientAddressValidationStarted(): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(isValidating = true),
)
}
fun getOnRecipientMemoValueChange(value: String): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(value = value),
),
@ -204,7 +218,8 @@ internal class SendStateFactory(
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = value,
@ -216,7 +231,8 @@ internal class SendStateFactory(
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isValidating = false,
@ -230,8 +246,10 @@ internal class SendStateFactory(
fun getOnXAddressMemoState(): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = "",
@ -243,9 +261,11 @@ internal class SendStateFactory(
fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isNotValid = isAddressInWallet || !isValidAddress
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
recent = recipientState.recent.map { recent ->
recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY))
@ -305,7 +325,7 @@ internal class SendStateFactory(
private fun isPrimaryButtonEnabled(state: SendUiState, notifications: ImmutableList<SendNotification>): Boolean {
val sendState = state.sendState ?: return false
val feeState = state.feeState ?: return false
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return false
val hasErrorNotifications = notifications.any { it is SendNotification.Error }
return !hasErrorNotifications && !sendState.isSending && feeState.feeSelectorState is FeeSelectorState.Content
}

View file

@ -28,9 +28,58 @@ internal data class SendUiState(
val recipientState: SendStates.RecipientState? = null,
val feeState: SendStates.FeeState? = null,
val sendState: SendStates.SendState? = null,
val editAmountState: SendStates.AmountState? = null,
val editRecipientState: SendStates.RecipientState? = null,
val editFeeState: SendStates.FeeState? = null,
val isBalanceHidden: Boolean,
val event: StateEvent<SendEvent>,
)
) {
fun getAmountState(isEditState: Boolean): SendStates.AmountState? {
return if (isEditState) {
editAmountState
} else {
amountState
}
}
fun getRecipientState(isEditState: Boolean): SendStates.RecipientState? {
return if (isEditState) {
editRecipientState
} else {
recipientState
}
}
fun getFeeState(isEditState: Boolean): SendStates.FeeState? {
return if (isEditState) {
editFeeState
} else {
feeState
}
}
fun copyWrapped(
isEditState: Boolean,
amountState: SendStates.AmountState? = this.amountState,
feeState: SendStates.FeeState? = this.feeState,
recipientState: SendStates.RecipientState? = this.recipientState,
sendState: SendStates.SendState? = this.sendState,
): SendUiState = if (isEditState) {
copy(
editAmountState = amountState,
editFeeState = feeState,
editRecipientState = recipientState,
)
} else {
copy(
amountState = amountState,
feeState = feeState,
recipientState = recipientState,
sendState = sendState,
)
}
}
@Stable
internal sealed class SendStates {
@ -105,6 +154,9 @@ enum class SendUiStateType {
None,
Recipient,
Amount,
Send,
Fee,
Send,
EditAmount,
EditRecipient,
EditFee,
}

View file

@ -18,6 +18,9 @@ internal class StateRouter(
val currentState: StateFlow<SendUiCurrentScreen>
get() = mutableCurrentState
val isEditState: Boolean
get() = currentState.value.isFromConfirmation
fun clear() {
mutableCurrentState.update { getInitState() }
}
@ -35,21 +38,29 @@ internal class StateRouter(
else -> popBackStack()
}
else -> when (type) {
SendUiStateType.Amount -> continueToSend(::showRecipient)
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showSend()
SendUiStateType.Send -> continueToSend(::showAmount)
else -> continueToSend(::popBackStack)
SendUiStateType.Send -> showAmount()
SendUiStateType.Recipient -> popBackStack()
SendUiStateType.EditAmount -> showSend()
SendUiStateType.EditRecipient -> showSend()
SendUiStateType.EditFee -> showSend()
else -> popBackStack()
}
}
}
fun onNextClick() {
when (currentState.value.type) {
SendUiStateType.Recipient -> continueToSend(::showAmount)
SendUiStateType.Recipient -> showAmount()
SendUiStateType.Amount,
SendUiStateType.Fee,
SendUiStateType.EditAmount,
SendUiStateType.EditRecipient,
SendUiStateType.EditFee,
-> showSend()
SendUiStateType.Send -> onBackClick()
else -> popBackStack()
}
}
@ -67,17 +78,35 @@ internal class StateRouter(
fun showAmount(isFromConfirmation: Boolean = false) {
analyticsEventsHandler.send(SendAnalyticEvents.AmountScreenOpened)
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Amount, isFromConfirmation) }
mutableCurrentState.update {
if (isFromConfirmation) {
SendUiCurrentScreen(SendUiStateType.EditAmount, true)
} else {
SendUiCurrentScreen(SendUiStateType.Amount, false)
}
}
}
fun showRecipient(isFromConfirmation: Boolean = false) {
analyticsEventsHandler.send(SendAnalyticEvents.AddressScreenOpened)
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Recipient, isFromConfirmation) }
mutableCurrentState.update {
if (isFromConfirmation) {
SendUiCurrentScreen(SendUiStateType.EditRecipient, true)
} else {
SendUiCurrentScreen(SendUiStateType.Recipient, false)
}
}
}
fun showFee(isFromConfirmation: Boolean = false) {
analyticsEventsHandler.send(SendAnalyticEvents.FeeScreenOpened)
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Fee, isFromConfirmation) }
mutableCurrentState.update {
if (isFromConfirmation) {
SendUiCurrentScreen(SendUiStateType.EditFee, true)
} else {
SendUiCurrentScreen(SendUiStateType.Fee, false)
}
}
}
fun showSend() {
@ -85,10 +114,6 @@ internal class StateRouter(
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Send, isFromConfirmation = false) }
}
private fun continueToSend(show: () -> Unit) {
if (currentState.value.isFromConfirmation) showSend() else show()
}
private fun getInitState() = if (isEditingDisabled) {
SendUiCurrentScreen(
type = SendUiStateType.None,

View file

@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
import com.tangem.utils.Provider
@ -10,18 +11,21 @@ import com.tangem.utils.Provider
* Factory to produce amount state for [SendUiState]
*/
internal class AmountStateFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) {
private val amountFieldChangeConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountFieldChangeConverter(
stateRouterProvider = stateRouterProvider,
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val amountFieldMaxAmountConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountFieldMaxAmountConverter(
stateRouterProvider = stateRouterProvider,
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
@ -29,12 +33,14 @@ internal class AmountStateFactory(
private val amountCurrencyConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountCurrencyConverter(
stateRouterProvider = stateRouterProvider,
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val amountPasteConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountPastedTriggerDismissConverter(
stateRouterProvider = stateRouterProvider,
currentStateProvider = currentStateProvider,
)
}

View file

@ -2,17 +2,20 @@ package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
internal class SendAmountCurrencyConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Boolean, SendUiState> {
override fun convert(value: Boolean): SendUiState {
val state = currentStateProvider()
val amountState = state.amountState ?: return state
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
@ -20,7 +23,8 @@ internal class SendAmountCurrencyConverter(
return if (amountTextField.isFiatValue == value && !isValidFiatRate) {
state
} else {
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
amountTextField = amountTextField.copy(
isFiatValue = value,

View file

@ -1,16 +1,20 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class SendAmountPastedTriggerDismissConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
) : Converter<Boolean, SendUiState> {
override fun convert(value: Boolean): SendUiState {
val state = currentStateProvider()
val amountState = state.amountState ?: return state
return state.copy(
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
amountTextField = amountState.amountTextField.copy(
isValuePasted = false,

View file

@ -0,0 +1,26 @@
package com.tangem.features.send.impl.presentation.state.common
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class SendSyncEditConverter(
private val currentStateProvider: Provider<SendUiState>,
) : Converter<Boolean, SendUiState> {
override fun convert(value: Boolean): SendUiState {
val state = currentStateProvider()
return if (value) {
state.copy(
amountState = state.editAmountState,
feeState = state.editFeeState,
recipientState = state.editRecipientState,
)
} else {
state.copy(
editAmountState = state.amountState,
editRecipientState = state.recipientState,
editFeeState = state.feeState,
)
}
}
}

View file

@ -49,10 +49,13 @@ internal class SendNotificationFactory(
.filter { it.type == SendUiStateType.Send }
.map {
val state = currentStateProvider()
val sendState = state.sendState ?: return@map persistentListOf()
val feeState = state.feeState ?: return@map persistentListOf()
val isEditState = stateRouterProvider().isEditState
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
val sendState = state.sendState ?: return@map persistentListOf()
val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf()
val amountState = state.getAmountState(isEditState) ?: return@map persistentListOf()
val amountValue = amountState.amountTextField.cryptoAmount.value ?: BigDecimal.ZERO
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isSubtractAvailableProvider(),

View file

@ -6,7 +6,6 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.common.extensions.minimalAmount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.lib.crypto.BlockchainUtils
import java.math.BigDecimal
import java.math.RoundingMode
@ -73,8 +72,7 @@ internal fun calculateSubtractedAmount(
/**
* Check if custom fee is too low
*/
internal fun checkIfFeeTooLow(state: SendUiState): Boolean {
val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
internal fun checkIfFeeTooLow(feeSelectorState: FeeSelectorState.Content): Boolean {
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
val minimumValue = multipleFees.minimum.amount.value ?: return false
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false

View file

@ -1,9 +1,6 @@
package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.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.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.persistentListOf
@ -19,10 +16,10 @@ internal class FeeNotificationFactory(
) {
fun create() = stateRouterProvider().currentState
.filter { it.type == SendUiStateType.Fee }
.filter { it.type == SendUiStateType.Fee || it.type == SendUiStateType.EditFee }
.map {
val state = currentStateProvider()
val feeState = state.feeState ?: return@map persistentListOf()
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return@map persistentListOf()
buildList {
addFeeUnreachableNotification(feeState.feeSelectorState)
}.toImmutableList()

View file

@ -7,9 +7,7 @@ import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
@ -20,6 +18,7 @@ import kotlinx.collections.immutable.persistentListOf
*/
internal class FeeStateFactory(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val appCurrencyProvider: Provider<AppCurrency>,
@ -43,8 +42,10 @@ internal class FeeStateFactory(
fun onFeeOnLoadingState(): SendUiState {
val state = currentStateProvider()
val feeState = state.feeState ?: return state
return state.copy(
val isEditState = stateRouterProvider().isEditState
val feeState = state.getFeeState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
sendState = state.sendState?.copy(
isPrimaryButtonEnabled = false,
),
@ -62,7 +63,8 @@ internal class FeeStateFactory(
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
val state = currentStateProvider()
val feeState = state.feeState ?: return state
val isEditState = stateRouterProvider().isEditState
val feeState = state.getFeeState(isEditState) ?: return state
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content
val isCustomWasSelected = if (feeState.isCustomSelected) {
@ -79,7 +81,8 @@ internal class FeeStateFactory(
)
val fee = feeConverter.convert(updatedFeeSelectorState)
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
sendState = state.sendState?.copy(
isPrimaryButtonEnabled = true,
),
@ -93,8 +96,10 @@ internal class FeeStateFactory(
fun onFeeOnErrorState(): SendUiState {
val state = currentStateProvider()
return state.copy(
feeState = state.feeState?.copy(
val isEditState = stateRouterProvider().isEditState
return state.copyWrapped(
isEditState = isEditState,
feeState = state.getFeeState(isEditState)?.copy(
feeSelectorState = FeeSelectorState.Error,
),
sendState = state.sendState?.copy(
@ -105,13 +110,15 @@ internal class FeeStateFactory(
fun onFeeSelectedState(feeType: FeeType): SendUiState {
val state = currentStateProvider()
val feeState = state.feeState ?: return state
val isEditState = stateRouterProvider().isEditState
val feeState = state.getFeeState(isEditState) ?: return state
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
val fee = feeConverter.convert(updatedFeeSelectorState)
val isCustomFeeWasSelected = feeState.isCustomSelected || updatedFeeSelectorState.selectedFee == FeeType.Custom
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
feeState = feeState.copy(
fee = fee,
isCustomSelected = isCustomFeeWasSelected,
@ -122,12 +129,14 @@ internal class FeeStateFactory(
fun onCustomFeeValueChange(index: Int, value: String): SendUiState {
val state = currentStateProvider()
val feeState = state.feeState ?: return state
val isEditState = stateRouterProvider().isEditState
val feeState = state.getFeeState(isEditState) ?: return state
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
val updatedFeeSelectorState = customFeeFieldConverter.onValueChange(feeSelectorState, index, value)
val fee = feeConverter.convert(updatedFeeSelectorState)
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
feeState = feeState.copy(
feeSelectorState = updatedFeeSelectorState,
fee = fee,
@ -137,10 +146,13 @@ internal class FeeStateFactory(
fun getFeeNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
val state = currentStateProvider()
return state.copy(
feeState = state.feeState?.copy(
val isEditState = stateRouterProvider().isEditState
val feeState = state.getFeeState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
feeState = feeState.copy(
notifications = notifications,
isPrimaryButtonEnabled = isPrimaryButtonEnabled(state.feeState, notifications),
isPrimaryButtonEnabled = isPrimaryButtonEnabled(feeState, notifications),
),
)
}

View file

@ -8,18 +8,21 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
import java.math.RoundingMode
internal class SendAmountFieldChangeConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<String, SendUiState> {
override fun convert(value: String): SendUiState {
val state = currentStateProvider()
val amountState = state.amountState ?: return state
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
if (value.isEmpty()) return state.emptyState()
@ -35,7 +38,8 @@ internal class SendAmountFieldChangeConverter(
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(amountTextField)
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isZero() else decimalCryptoValue.isZero()
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
amountTextField = amountTextField.copy(
@ -75,9 +79,11 @@ internal class SendAmountFieldChangeConverter(
}
private fun SendUiState.emptyState(): SendUiState {
if (amountState == null) return this
val isEditState = stateRouterProvider().isEditState
val amountState = getAmountState(isEditState) ?: return this
val amountTextField = amountState.amountTextField
return copy(
return copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
isPrimaryButtonEnabled = false,
amountTextField = amountTextField.copy(

View file

@ -6,11 +6,13 @@ import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
internal class SendAmountFieldMaxAmountConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Unit, SendUiState> {
@ -18,7 +20,8 @@ internal class SendAmountFieldMaxAmountConverter(
override fun convert(value: Unit): SendUiState {
val state = currentStateProvider()
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val amountState = state.amountState ?: return state
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
val cryptoDecimals = amountTextField.cryptoAmount.decimals
@ -31,7 +34,8 @@ internal class SendAmountFieldMaxAmountConverter(
val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero()
val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty()
val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals).orEmpty()
return state.copy(
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
isPrimaryButtonEnabled = true,
amountTextField = amountTextField.copy(

View file

@ -14,7 +14,7 @@ internal object SendClickIntentsStub : SendClickIntents {
override fun onBackClick() {}
override fun onNextClick() {}
override fun onNextClick(isFromEdit: Boolean) {}
override fun onPrevClick() {}

View file

@ -3,7 +3,10 @@ package com.tangem.features.send.impl.presentation.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -50,7 +53,11 @@ internal fun SendNavigationButtons(
bottom = TangemTheme.dimens.spacing16,
),
) {
SendingText(uiState = uiState, isVisible = isSendingState)
SendingText(
uiState = uiState,
isEditState = currentState.isFromConfirmation,
isVisible = isSendingState,
)
SendDoneButtons(
txUrl = sendState.txUrl,
onExploreClick = uiState.clickIntents::onExploreClick,
@ -130,7 +137,12 @@ private fun SendNavigationButton(
}
@Composable
private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modifier = Modifier) {
private fun SendingText(
uiState: SendUiState,
isEditState: Boolean,
isVisible: Boolean,
modifier: Modifier = Modifier,
) {
var isVisibleProxy by remember { mutableStateOf(isVisible) }
// text appearance delay for smooth screen transitions
@ -148,8 +160,8 @@ private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modi
exit = slideOutVertically().plus(fadeOut()),
label = "Animate show sending state text",
) {
val amountState = uiState.amountState
val feeState = uiState.feeState
val amountState = uiState.getAmountState(isEditState)
val feeState = uiState.getFeeState(isEditState)
val fiatRate = feeState?.rate
val fiatAmount = amountState?.amountTextField?.fiatAmount
val feeFiat = fiatRate?.let { feeState.fee?.amount?.value?.multiply(it) }
@ -230,11 +242,11 @@ private fun getButtonData(
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,
-> if (currentState.isFromConfirmation) {
R.string.common_continue to uiState.clickIntents::onNextClick
} else {
R.string.common_next to uiState.clickIntents::onNextClick
}
-> R.string.common_next to { uiState.clickIntents.onNextClick() }
SendUiStateType.EditFee,
SendUiStateType.EditAmount,
SendUiStateType.EditRecipient,
-> R.string.common_continue to { uiState.clickIntents.onNextClick(isFromEdit = true) }
SendUiStateType.Send -> when {
isSuccess -> R.string.common_close
isSending -> R.string.send_sending
@ -245,10 +257,13 @@ private fun getButtonData(
private fun isButtonEnabled(currentState: SendUiCurrentScreen, uiState: SendUiState): Boolean {
return when (currentState.type) {
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Send -> uiState.sendState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled
SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled
SendUiStateType.Send -> uiState.sendState?.isPrimaryButtonEnabled
SendUiStateType.EditAmount -> uiState.editAmountState?.isPrimaryButtonEnabled
SendUiStateType.EditRecipient -> uiState.editRecipientState?.isPrimaryButtonEnabled
SendUiStateType.EditFee -> uiState.editFeeState?.isPrimaryButtonEnabled
else -> true
}
} ?: false
}

View file

@ -1,8 +1,11 @@
package com.tangem.features.send.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.SnackbarHostState
@ -23,12 +26,13 @@ import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent
import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
import com.tangem.features.send.impl.presentation.ui.send.SendContent
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.withIndex
@Composable
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
val snackbarHostState = remember { SnackbarHostState() }
val sendState = uiState.sendState ?: return
BackHandler { uiState.clickIntents.onBackClick() }
Column(
modifier = Modifier
@ -38,38 +42,9 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen)
.background(color = TangemTheme.colors.background.tertiary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val titleRes = when (currentState.type) {
SendUiStateType.Amount -> resourceReference(R.string.send_amount_label)
SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label)
SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title)
SendUiStateType.Send -> if (!sendState.isSuccess) {
resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName))
} else {
null
}
else -> null
}
val isSending = currentState.type == SendUiStateType.Send && !uiState.sendState.isSuccess
val subtitleRes = if (isSending) {
uiState.amountState?.walletName
} else {
null
}
val iconRes = if (currentState.type == SendUiStateType.Recipient) {
R.drawable.ic_qrcode_scan_24
} else {
null
}
AppBarWithBackButtonAndIcon(
text = titleRes?.resolveReference(),
subtitle = subtitleRes,
onBackClick = uiState.clickIntents::popBackStack,
onIconClick = uiState.clickIntents::onQrCodeScanClick,
backIconRes = R.drawable.ic_close_24,
iconRes = iconRes,
backgroundColor = TangemTheme.colors.background.tertiary,
modifier = Modifier.height(TangemTheme.dimens.size56),
SendAppBar(
uiState = uiState,
currentState = currentState,
)
SendScreenContent(
uiState = uiState,
@ -88,6 +63,50 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen)
)
}
@Composable
private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen) {
val (titleRes, subtitleRes) = when (currentState.type) {
SendUiStateType.Amount,
SendUiStateType.EditAmount,
-> resourceReference(R.string.send_amount_label) to null
SendUiStateType.Recipient,
SendUiStateType.EditRecipient,
-> resourceReference(R.string.send_recipient_label) to null
SendUiStateType.Fee,
SendUiStateType.EditFee,
-> resourceReference(R.string.common_fee_selector_title) to null
SendUiStateType.Send -> if (uiState.sendState?.isSuccess == false) {
resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) to
uiState.amountState?.walletName
} else {
null to null
}
else -> null to null
}
val iconRes = if (currentState.type == SendUiStateType.Recipient) {
R.drawable.ic_qrcode_scan_24
} else {
null
}
val (backIcon, backClick) = when (currentState.type) {
SendUiStateType.EditAmount,
SendUiStateType.EditFee,
SendUiStateType.EditRecipient,
-> R.drawable.ic_back_24 to uiState.clickIntents::onBackClick
else -> R.drawable.ic_close_24 to uiState.clickIntents::popBackStack
}
AppBarWithBackButtonAndIcon(
text = titleRes?.resolveReference(),
subtitle = subtitleRes,
onBackClick = backClick,
onIconClick = uiState.clickIntents::onQrCodeScanClick,
backIconRes = backIcon,
iconRes = iconRes,
backgroundColor = TangemTheme.colors.background.tertiary,
modifier = Modifier.height(TangemTheme.dimens.size56),
)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentScreen, modifier: Modifier = Modifier) {
@ -135,13 +154,23 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS
isBalanceHiding = uiState.isBalanceHidden,
clickIntents = uiState.clickIntents,
)
SendUiStateType.EditAmount -> SendAmountContent(
amountState = uiState.editAmountState,
isBalanceHiding = uiState.isBalanceHidden,
clickIntents = uiState.clickIntents,
)
SendUiStateType.Recipient -> SendRecipientContent(
uiState = uiState.recipientState,
clickIntents = uiState.clickIntents,
isBalanceHidden = uiState.isBalanceHidden,
)
SendUiStateType.Fee -> SendSpeedAndFeeContent(
state = uiState.feeState,
SendUiStateType.EditRecipient -> SendRecipientContent(
uiState = uiState.editRecipientState,
clickIntents = uiState.clickIntents,
isBalanceHidden = uiState.isBalanceHidden,
)
SendUiStateType.EditFee -> SendSpeedAndFeeContent(
state = uiState.editFeeState,
clickIntents = uiState.clickIntents,
)
SendUiStateType.Send -> SendContent(uiState)

View file

@ -14,7 +14,7 @@ internal interface SendClickIntents {
fun onBackClick()
fun onNextClick()
fun onNextClick(isFromEdit: Boolean = false)
fun onPrevClick()

View file

@ -122,6 +122,7 @@ internal class SendViewModel @Inject constructor(
private val stateFactory = SendStateFactory(
clickIntents = this,
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
@ -132,12 +133,14 @@ internal class SendViewModel @Inject constructor(
)
private val amountStateFactory = AmountStateFactory(
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
)
private val feeStateFactory = FeeStateFactory(
clickIntents = this,
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
@ -146,6 +149,7 @@ internal class SendViewModel @Inject constructor(
private val eventStateFactory = SendEventStateFactory(
clickIntents = this,
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeStateFactory = feeStateFactory,
@ -171,7 +175,10 @@ internal class SendViewModel @Inject constructor(
)
private val sendOnNextScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
SendOnNextScreenAnalyticSender(analyticsEventHandler)
SendOnNextScreenAnalyticSender(
stateRouterProvider = Provider { stateRouter },
analyticsEventHandler = analyticsEventHandler,
)
}
// todo convert to StateFlow
@ -454,7 +461,9 @@ internal class SendViewModel @Inject constructor(
stateRouter.currentState
.onEach {
when (it.type) {
SendUiStateType.Fee -> loadFee()
SendUiStateType.Fee,
SendUiStateType.EditFee,
-> loadFee()
SendUiStateType.Send -> sendIdleTimer = SystemClock.elapsedRealtime()
else -> Unit
}
@ -489,16 +498,17 @@ internal class SendViewModel @Inject constructor(
stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true)
}
override fun onNextClick() {
override fun onNextClick(isFromEdit: Boolean) {
val currentState = stateRouter.currentState.value
uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit)
sendOnNextScreenAnalyticSender.send(currentState.type, uiState)
when (currentState.type) {
SendUiStateType.Fee -> {
if (onFeeNext()) return
}
SendUiStateType.Amount -> {
loadFee()
}
SendUiStateType.Fee,
SendUiStateType.EditFee,
-> if (onFeeNext()) return
SendUiStateType.Amount,
SendUiStateType.EditAmount,
-> loadFee()
else -> Unit
}
@ -523,13 +533,14 @@ internal class SendViewModel @Inject constructor(
innerRouter.openTokenDetails(userWalletId, currency)
private fun onFeeNext(): Boolean {
if (checkIfFeeTooLow(uiState)) {
val feeState = uiState.getFeeState(stateRouter.isEditState)
val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
if (checkIfFeeTooLow(feeSelectorState)) {
uiState = eventStateFactory.getFeeTooLowAlert(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
return true
}
val feeSelectorState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
return checkIfFeeTooHigh(
feeSelectorState = feeSelectorState,
onShow = { diff ->
@ -706,8 +717,9 @@ internal class SendViewModel @Inject constructor(
}
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
val amountState = uiState.amountState ?: return null
val recipientState = uiState.recipientState ?: return null
val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation
val amountState = uiState.getAmountState(isFromConfirmation) ?: return null
val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
return feeCryptoCurrencyStatus?.let { feeCurrencyStatus ->
@ -736,12 +748,14 @@ internal class SendViewModel @Inject constructor(
}
override fun showAmount() {
uiState = stateFactory.syncEditStates(isFromEdit = false)
stateRouter.showAmount(isFromConfirmation = true)
setNeverToShowTapHelp()
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
}
override fun showRecipient() {
uiState = stateFactory.syncEditStates(isFromEdit = false)
stateRouter.showRecipient(isFromConfirmation = true)
uiState = stateFactory.getHiddenTapHelpState()
setNeverToShowTapHelp()
@ -749,6 +763,7 @@ internal class SendViewModel @Inject constructor(
}
override fun showFee() {
uiState = stateFactory.syncEditStates(isFromEdit = false)
stateRouter.showFee(isFromConfirmation = true)
setNeverToShowTapHelp()
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))