Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-05 17:34:34 +03:00
commit 18d7ee1ec6
14 changed files with 192 additions and 54 deletions

View file

@ -46,6 +46,11 @@ object TradeCryptoMiddleware {
}
}
private val isSendRedesignedEnabled: Boolean
get() = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::sendFeatureToggles,
).isRedesignedSendEnabled
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
@ -57,8 +62,20 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.Swap -> openSwap(
currency = action.cryptoCurrency,
)
is TradeCryptoAction.SendToken -> handleSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleSendCoin(action = action)
is TradeCryptoAction.SendToken -> {
if (isSendRedesignedEnabled) {
handleNewSendToken(action = action)
} else {
handleSendToken(action = action)
}
}
is TradeCryptoAction.SendCoin -> {
if (isSendRedesignedEnabled) {
handleNewSendCoin(action = action)
} else {
handleSendCoin(action = action)
}
}
}
}
@ -280,4 +297,35 @@ object TradeCryptoMiddleware {
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
txInfo = action.transactionInfo,
currency = action.tokenCurrency,
)
}
private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
txInfo = action.transactionInfo,
currency = action.coinStatus.currency,
)
}
private fun handleNewSend(
userWalletId: String,
txInfo: TradeCryptoAction.TransactionInfo?,
currency: CryptoCurrency,
) {
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to userWalletId,
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
SendRouter.AMOUNT_KEY to txInfo?.amount,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}

View file

@ -9,5 +9,8 @@ interface SendRouter {
companion object {
const val CRYPTO_CURRENCY_KEY = "send_crypto_currency"
const val USER_WALLET_ID_KEY = "send_user_wallet_id"
const val TRANSACTION_ID_KEY = "send_transaction_id"
const val AMOUNT_KEY = "send_amount"
const val DESTINATION_ADDRESS_KEY = "send_destination_address"
}
}

View file

@ -50,10 +50,13 @@ internal class SendFragment : ComposeFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
val isEditingDisabled = arguments?.getString(SendRouter.TRANSACTION_ID_KEY) != null
viewModel.setRouter(
innerSendRouter,
StateRouter(
fragmentManager = WeakReference(parentFragmentManager),
isEditingDisabled = isEditingDisabled,
),
)
listenToQrCode()

View file

@ -92,18 +92,29 @@ internal class SendStateFactory(
// region UI states
fun getInitialState(): SendUiState = SendUiState(
clickIntents = clickIntents,
currentState = MutableStateFlow(SendUiStateType.Amount),
currentState = MutableStateFlow(SendUiStateType.None),
event = consumedEvent(),
isEditingDisabled = false,
)
fun getReadyState(): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(Unit),
recipientState = state.recipientState ?: recipientStateConverter.convert(Unit),
amountState = state.amountState ?: amountStateConverter.convert(""),
recipientState = state.recipientState ?: recipientStateConverter.convert(""),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
)
}
fun getReadyState(amount: String, destinationAddress: String): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(amount),
recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
isEditingDisabled = true,
)
}
//endregion
//region amount state clicks

View file

@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.math.BigDecimal
/**
@ -26,12 +27,13 @@ import java.math.BigDecimal
@Immutable
internal data class SendUiState(
val clickIntents: SendClickIntents,
val isEditingDisabled: Boolean,
val amountState: SendStates.AmountState? = null,
val recipientState: SendStates.RecipientState? = null,
val feeState: SendStates.FeeState? = null,
val sendState: SendStates.SendState = SendStates.SendState(),
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
val currentState: MutableStateFlow<SendUiStateType>,
val currentState: StateFlow<SendUiStateType>,
val event: StateEvent<SendEvent>,
)
@ -99,6 +101,7 @@ internal sealed class SendStates {
}
enum class SendUiStateType {
None,
Amount,
Recipient,
Fee,

View file

@ -2,28 +2,40 @@ package com.tangem.features.send.impl.presentation.state
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.lang.ref.WeakReference
internal class StateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
private val isEditingDisabled: Boolean,
) {
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Recipient)
private set
private var mutableCurrentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(
if (isEditingDisabled) {
SendUiStateType.None
} else {
SendUiStateType.Recipient
},
)
val currentState: StateFlow<SendUiStateType> = mutableCurrentState
fun popBackStack() {
fragmentManager.get()?.popBackStack()
}
fun onBackClick(isSuccess: Boolean = false) {
if (isSuccess) {
popBackStack()
} else {
when (currentState.value) {
SendUiStateType.Recipient -> popBackStack()
when {
isSuccess -> popBackStack()
isEditingDisabled -> when (currentState.value) {
SendUiStateType.Send -> showFee()
else -> popBackStack()
}
else -> when (currentState.value) {
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
SendUiStateType.Send -> showFee()
else -> popBackStack()
}
}
}
@ -34,31 +46,35 @@ internal class StateRouter(
SendUiStateType.Amount -> showFee()
SendUiStateType.Fee -> showSend()
SendUiStateType.Send -> onBackClick()
else -> popBackStack()
}
}
fun onPrevClick() {
when (currentState.value) {
SendUiStateType.Recipient -> popBackStack()
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
SendUiStateType.Send -> popBackStack()
if (isEditingDisabled) {
popBackStack()
} else {
when (currentState.value) {
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
else -> popBackStack()
}
}
}
fun showAmount() {
currentState.update { SendUiStateType.Amount }
mutableCurrentState.update { SendUiStateType.Amount }
}
fun showRecipient() {
currentState.update { SendUiStateType.Recipient }
mutableCurrentState.update { SendUiStateType.Recipient }
}
fun showFee() {
currentState.update { SendUiStateType.Fee }
mutableCurrentState.update { SendUiStateType.Fee }
}
fun showSend() {
currentState.update { SendUiStateType.Send }
private fun showSend() {
mutableCurrentState.update { SendUiStateType.Send }
}
}

View file

@ -22,9 +22,9 @@ internal class SendAmountStateConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
private val sendAmountFieldConverter: SendAmountFieldConverter,
) : Converter<Unit, SendStates.AmountState> {
) : Converter<String, SendStates.AmountState> {
override fun convert(value: Unit): SendStates.AmountState {
override fun convert(value: String): SendStates.AmountState {
val userWallet = userWalletProvider()
val appCurrency = appCurrencyProvider()
val status = cryptoCurrencyStatusProvider()
@ -35,7 +35,7 @@ internal class SendAmountStateConverter(
walletName = userWallet.name,
walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)),
tokenIconState = iconStateConverter.convert(status),
amountTextField = sendAmountFieldConverter.convert(Unit),
amountTextField = sendAmountFieldConverter.convert(value),
isPrimaryButtonEnabled = false,
segmentedButtonConfig = persistentListOf(
SendAmountSegmentedButtonsConfig(

View file

@ -3,7 +3,9 @@ package com.tangem.features.send.impl.presentation.state.fields
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
@ -21,20 +23,28 @@ internal class SendAmountFieldConverter(
private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Unit, SendTextField.AmountField> {
) : Converter<String, SendTextField.AmountField> {
override fun convert(value: Unit): SendTextField.AmountField {
override fun convert(value: String): SendTextField.AmountField {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val cryptoDecimal = value.toBigDecimalOrDefault()
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatValue = if (value.isEmpty()) {
""
} else {
val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
fiatDecimal.parseBigDecimal(FIAT_DECIMALS)
}
return SendTextField.AmountField(
value = "",
fiatValue = "",
value = value,
fiatValue = fiatValue,
onValueChange = clickIntents::onAmountValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Number,
),
isFiatValue = false,
cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrencyStatus.currency),
cryptoAmount = cryptoAmount,
fiatAmount = getAppCurrencyAmount(appCurrencyProvider()),
isError = false,
error = TextReference.Res(R.string.swapping_insufficient_funds),

View file

@ -11,11 +11,11 @@ import com.tangem.utils.converter.Converter
internal class SendRecipientAddressFieldConverter(
private val clickIntents: SendClickIntents,
) : Converter<Unit, SendTextField.RecipientAddress> {
) : Converter<String, SendTextField.RecipientAddress> {
override fun convert(value: Unit): SendTextField.RecipientAddress {
override fun convert(value: String): SendTextField.RecipientAddress {
return SendTextField.RecipientAddress(
value = "",
value = value,
onValueChange = clickIntents::onRecipientAddressValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,

View file

@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
internal class SendRecipientStateConverter(
private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Unit, SendStates.RecipientState> {
) : Converter<String, SendStates.RecipientState> {
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
private val memoFieldConverter by lazy {
@ -19,9 +19,9 @@ internal class SendRecipientStateConverter(
)
}
override fun convert(value: Unit): SendStates.RecipientState {
override fun convert(value: String): SendStates.RecipientState {
return SendStates.RecipientState(
addressTextField = addressFieldConverter.convert(Unit),
addressTextField = addressFieldConverter.convert(value),
memoTextField = memoFieldConverter.convertOrNull(),
network = cryptoCurrencyStatusProvider().currency.network.name,
isPrimaryButtonEnabled = false,

View file

@ -49,9 +49,10 @@ internal fun SendNavigationButtons(uiState: SendUiState) {
@Composable
private fun SendSecondaryNavigationButton(uiState: SendUiState) {
val currentState = uiState.currentState.collectAsState()
val isEditingDisabled = uiState.isEditingDisabled
val isCorrectScreen = currentState.value == SendUiStateType.Amount || currentState.value == SendUiStateType.Fee
AnimatedVisibility(
visible = currentState.value == SendUiStateType.Amount ||
currentState.value == SendUiStateType.Fee,
visible = !isEditingDisabled && isCorrectScreen,
) {
Icon(
modifier = Modifier
@ -170,6 +171,7 @@ private fun getButtonData(
isSuccess: Boolean,
): Pair<Int, () -> Unit> {
return when (currentState.value) {
SendUiStateType.None,
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,

View file

@ -45,6 +45,7 @@ internal fun SendScreen(uiState: SendUiState) {
SendUiStateType.Recipient -> R.string.send_recipient_label
SendUiStateType.Fee -> R.string.common_fee_selector_title
SendUiStateType.Send -> if (!isSuccess) R.string.send_confirm_label else null
else -> null
}
val iconRes = if (currentState.value == SendUiStateType.Recipient) {
R.drawable.ic_qrcode_scan_24
@ -102,6 +103,7 @@ private fun SendScreenContent(
uiState.clickIntents,
)
SendUiStateType.Send -> SendContent(uiState)
else -> Unit
}
}
}

View file

@ -70,11 +70,13 @@ internal fun SendContent(uiState: SendUiState) {
RecipientBlock(
recipientState = recipientState,
isSuccess = isSuccess,
isEditingDisabled = uiState.isEditingDisabled,
onClick = uiState.clickIntents::showRecipient,
)
AmountBlock(
amountState = amountState,
isSuccess = isSuccess,
isEditingDisabled = uiState.isEditingDisabled,
onClick = uiState.clickIntents::showAmount,
)
FeeBlock(
@ -121,7 +123,12 @@ private fun FromWallet(walletName: String, walletBalance: String) {
}
@Composable
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean, onClick: () -> Unit) {
private fun AmountBlock(
amountState: SendStates.AmountState,
isSuccess: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
) {
val amount = amountState.amountTextField
val cryptoAmount = formatCryptoAmount(
@ -134,6 +141,11 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean,
fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code,
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
)
val backgroundColor = if (isEditingDisabled) {
TangemTheme.colors.button.disabled
} else {
TangemTheme.colors.background.action
}
InputRowImage(
title = TextReference.Res(R.string.send_amount_label),
subtitle = TextReference.Str(cryptoAmount),
@ -142,21 +154,31 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean,
showNetworkIcon = true,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess) { onClick() },
.background(backgroundColor)
.clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() },
)
}
@Composable
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: Boolean, onClick: () -> Unit) {
private fun RecipientBlock(
recipientState: SendStates.RecipientState,
isSuccess: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
) {
val address = recipientState.addressTextField
val memo = recipientState.memoTextField
val backgroundColor = if (isEditingDisabled) {
TangemTheme.colors.button.disabled
} else {
TangemTheme.colors.background.action
}
Column(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess) { onClick() },
.background(backgroundColor)
.clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() },
) {
val showMemo = memo != null && memo.value.isNotBlank()
InputRowRecipientDefault(

View file

@ -88,6 +88,10 @@ internal class SendViewModel @Inject constructor(
private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
?: error("This screen can't open without `CryptoCurrency`")
private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY]
private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY]
private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY]
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var innerRouter: InnerSendRouter by Delegates.notNull()
@ -192,11 +196,10 @@ internal class SendViewModel @Inject constructor(
.flowWithLifecycle(owner.lifecycle)
.onEach { currencyStatus ->
currencyStatus.onRight {
cryptoCurrencyStatus = it
coinCryptoCurrencyStatus = it
getWalletsAndRecent()
uiState = stateFactory.getReadyState()
updateNotifications()
onDataLoaded(
currencyStatus = it,
coinCurrencyStatus = it,
)
}
}
.flowOn(dispatchers.main)
@ -208,10 +211,10 @@ internal class SendViewModel @Inject constructor(
flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
) { coinStatus, currencyStatus ->
if (coinStatus.isRight() && currencyStatus.isRight()) {
coinStatus.onRight { coinCryptoCurrencyStatus = it }
currencyStatus.onRight { cryptoCurrencyStatus = it }
getWalletsAndRecent()
uiState = stateFactory.getReadyState()
onDataLoaded(
currencyStatus = currencyStatus.getOrElse { error("Currency status is unreachable") },
coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") },
)
}
}.flowWithLifecycle(owner.lifecycle)
.flowOn(dispatchers.main)
@ -245,6 +248,21 @@ internal class SendViewModel @Inject constructor(
)
}
private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus) {
cryptoCurrencyStatus = currencyStatus
coinCryptoCurrencyStatus = coinCurrencyStatus
if (transactionId != null && amount != null && destinationAddress != null) {
uiState = stateFactory.getReadyState(amount, destinationAddress)
showFee()
} else {
getWalletsAndRecent()
uiState = stateFactory.getReadyState()
showRecipient()
}
updateNotifications()
}
private fun getWalletsAndRecent() {
combine(
flow = getUserWallets().conflate(),