Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-22 16:20:14 +03:00
parent f7f2ab7f88
commit 0b037f2b2d
26 changed files with 385 additions and 80 deletions

View file

@ -0,0 +1,47 @@
package com.tangem.features.send.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.send.impl.R
@Immutable
internal sealed class SendAlertState {
abstract val title: TextReference?
abstract val message: TextReference
open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
open val onConfirmClick: (() -> Unit)? = null
data class GenericError(
override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title),
override val onConfirmClick: (() -> Unit),
) : SendAlertState() {
override val message: TextReference = resourceReference(R.string.common_unknown_error)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.send_alert_button_request_support)
}
data class TransactionError(
val code: String,
val cause: String?,
val causeTextReference: TextReference? = null,
override val onConfirmClick: (() -> Unit),
) : SendAlertState() {
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
override val message: TextReference = resourceReference(
id = R.string.send_alert_transaction_failed_text,
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.send_alert_button_request_support)
}
data class DemoMode(
override val onConfirmClick: () -> Unit,
) : SendAlertState() {
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.send.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class SendEvent {
data class ShowSnackBar(val text: TextReference) : SendEvent()
data class ShowAlert(val alert: SendAlertState) : SendEvent()
}

View file

@ -0,0 +1,50 @@
package com.tangem.features.send.impl.presentation.state
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
/**
* Factory to produce event state for [SendUiState]
*
* @param currentStateProvider [Provider] of [SendUiState]
* @param clickIntents [SendClickIntents]
*/
internal class SendEventStateFactory(
val currentStateProvider: Provider<SendUiState>,
val clickIntents: SendClickIntents,
) {
private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) }
fun onConsumeEventState(): SendUiState {
return currentStateProvider().copy(event = consumedEvent())
}
fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState {
val state = currentStateProvider()
val event = error?.let {
sendTransactionErrorConverter.convert(error)?.let {
triggeredEvent<SendEvent>(SendEvent.ShowAlert(it), onConsume)
}
}
return state.copy(
event = event ?: consumedEvent(),
)
}
fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState {
val state = currentStateProvider()
return state.copy(
event = triggeredEvent(
data = SendEvent.ShowAlert(
SendAlertState.GenericError(
onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) },
),
),
onConsume = onConsume,
),
)
}
}

View file

@ -5,6 +5,7 @@ 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.event.consumedEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
@ -44,7 +45,6 @@ internal class SendStateFactory(
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) }
private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) }
private val customFeeFieldConverter by lazy {
@ -92,6 +92,7 @@ internal class SendStateFactory(
fun getInitialState(): SendUiState = SendUiState(
clickIntents = clickIntents,
currentState = MutableStateFlow(SendUiStateType.Amount),
event = consumedEvent(),
)
fun getReadyState(): SendUiState {

View file

@ -0,0 +1,44 @@
package com.tangem.features.send.impl.presentation.state
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.converter.Converter
internal class SendTransactionAlertConverter(
private val clickIntents: SendClickIntents,
) : Converter<SendTransactionError, SendAlertState?> {
override fun convert(value: SendTransactionError): SendAlertState? {
return when (value) {
SendTransactionError.DemoCardError -> SendAlertState.DemoMode(
onConfirmClick = { clickIntents.popBackStack() },
)
is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError(
code = value.code.toString(),
cause = null,
causeTextReference = value.messageReference,
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) },
)
is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError(
code = value.code.toString(),
cause = value.message,
onConfirmClick = { clickIntents.onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
)
is SendTransactionError.DataError -> SendAlertState.TransactionError(
code = "",
cause = value.message,
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.NetworkError -> SendAlertState.TransactionError(
code = "",
cause = value.message,
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.UnknownError -> SendAlertState.TransactionError(
code = "",
cause = value.ex?.localizedMessage,
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
)
else -> null
}
}
}

View file

@ -5,6 +5,7 @@ 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.core.ui.event.StateEvent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
@ -31,6 +32,7 @@ internal data class SendUiState(
val sendState: SendStates.SendState = SendStates.SendState(),
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
val currentState: MutableStateFlow<SendUiStateType>,
val event: StateEvent<SendEvent>,
)
@Stable

View file

@ -0,0 +1,73 @@
package com.tangem.features.send.impl.presentation.ui
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendAlertState
import com.tangem.features.send.impl.presentation.state.SendEvent
@Composable
internal fun SendEventEffect(event: StateEvent<SendEvent>, snackbarHostState: SnackbarHostState) {
val resources = LocalContext.current.resources
var alertConfig by remember { mutableStateOf<SendAlertState?>(value = null) }
alertConfig?.let {
SendAlert(state = it, onDismiss = { alertConfig = null })
}
EventEffect(
event = event,
onTrigger = { value ->
when (value) {
is SendEvent.ShowSnackBar -> {
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
}
is SendEvent.ShowAlert -> {
alertConfig = value.alert
}
}
},
)
}
@Composable
internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) {
val confirmButton: DialogButton
val dismissButton: DialogButton?
val onActionClick = state.onConfirmClick
if (onActionClick != null) {
confirmButton = DialogButton(
title = state.confirmButtonText.resolveReference(),
onClick = {
onActionClick()
onDismiss()
},
)
dismissButton = DialogButton(
title = stringResource(id = R.string.common_cancel),
onClick = onDismiss,
)
} else {
confirmButton = DialogButton(
title = state.confirmButtonText.resolveReference(),
onClick = onDismiss,
)
dismissButton = null
}
BasicDialog(
message = state.message.resolveReference(),
confirmButton = confirmButton,
onDismissDialog = onDismiss,
title = state.title?.resolveReference(),
dismissButton = dismissButton,
)
}

View file

@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -28,6 +30,7 @@ import com.tangem.features.send.impl.presentation.ui.send.SendContent
internal fun SendScreen(uiState: SendUiState) {
val currentState = uiState.currentState.collectAsStateWithLifecycle()
val isSuccess = uiState.sendState.isSuccess
val snackbarHostState = remember { SnackbarHostState() }
BackHandler { uiState.clickIntents.onBackClick() }
Column(
modifier = Modifier
@ -65,6 +68,11 @@ internal fun SendScreen(uiState: SendUiState) {
)
SendNavigationButtons(uiState)
}
SendEventEffect(
event = uiState.event,
snackbarHostState = snackbarHostState,
)
}
@Composable

View file

@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.send.impl.presentation.state.fee.FeeType
@Suppress("TooManyFunctions")
interface SendClickIntents {
fun popBackStack()
@ -16,6 +17,8 @@ interface SendClickIntents {
fun onQrCodeScanClick()
fun onFailedTxEmailClick(errorMessage: String)
fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency)
// region Amount

View file

@ -8,8 +8,11 @@ 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.TransactionData
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
@ -31,10 +34,6 @@ 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.*
import com.tangem.features.send.impl.presentation.state.SendNotificationFactory
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.StateRouter
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
@ -70,6 +69,7 @@ internal class SendViewModel @Inject constructor(
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val reduxStateHolder: ReduxStateHolder,
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
savedStateHandle: SavedStateHandle,
@ -98,6 +98,11 @@ internal class SendViewModel @Inject constructor(
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
)
private val eventStateFactory = SendEventStateFactory(
clickIntents = this,
currentStateProvider = Provider { uiState },
)
private val sendNotificationFactory = SendNotificationFactory(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
@ -106,6 +111,7 @@ internal class SendViewModel @Inject constructor(
walletManagersFacade = walletManagersFacade,
)
// todo convert to StateFlow
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
private set
@ -139,7 +145,9 @@ internal class SendViewModel @Inject constructor(
getCurrenciesStatusUpdates(owner, wallet)
},
ifLeft = {
// todo add error handling [[REDACTED_JIRA]]
uiState = eventStateFactory.getGenericErrorState(
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
return@launch
},
)
@ -331,6 +339,10 @@ internal class SendViewModel @Inject constructor(
override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
override fun onFailedTxEmailClick(errorMessage: String) {
reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage))
}
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
innerRouter.openTokenDetails(userWalletId, currency)
// endregion
@ -433,8 +445,8 @@ internal class SendViewModel @Inject constructor(
val sendState = uiState.sendState
if (sendState.isSuccess) popBackStack()
uiState = stateFactory.getSendingStateUpdate(true)
viewModelScope.launch(dispatchers.io) { verifyAndSendTransaction() }
uiState = stateFactory.getSendingStateUpdate(isSending = true)
verifyAndSendTransaction()
}
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
@ -445,7 +457,7 @@ internal class SendViewModel @Inject constructor(
override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl)
private suspend fun verifyAndSendTransaction() {
private fun verifyAndSendTransaction() {
val recipient = uiState.recipientState?.addressTextField?.value ?: return
val feeState = uiState.feeState ?: return
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
@ -454,41 +466,46 @@ internal class SendViewModel @Inject constructor(
val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency)
// todo add error handling [[REDACTED_JIRA]]
// val transactionErrors = walletManagersFacade.validateTransaction(
// amount = amountToSend,
// fee = fee.amount,
// userWalletId = userWalletId,
// network = cryptoCurrency.network,
// )
viewModelScope.launch(dispatchers.main) {
createTransactionUseCase(
amount = amountToSend,
fee = fee,
memo = memo,
destination = recipient,
userWalletId = userWalletId,
network = cryptoCurrency.network,
).fold(
ifLeft = {
Timber.e(it)
uiState = eventStateFactory.getGenericErrorState(
error = it,
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
},
ifRight = { txData ->
sendTransaction(txData)
},
)
}
}
createTransactionUseCase(
amount = amountToSend,
fee = fee,
memo = memo,
destination = recipient,
userWalletId = userWalletId,
private suspend fun sendTransaction(txData: TransactionData) {
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = cryptoCurrency.network,
).fold(
ifLeft = {
Timber.e(it)
// todo add error handling [[REDACTED_JIRA]]
},
ifRight = { txData ->
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = cryptoCurrency.network,
).fold(
ifLeft = {
uiState = stateFactory.getSendingStateUpdate(false)
// todo add error handling [[REDACTED_JIRA]]
},
ifRight = {
uiState = stateFactory.getTransactionSendState(txData)
},
ifLeft = { error ->
uiState = stateFactory.getSendingStateUpdate(isSending = false)
uiState = eventStateFactory.getSendTransactionErrorState(
error = error,
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
)
},
ifRight = {
uiState = stateFactory.getSendingStateUpdate(isSending = false)
uiState = stateFactory.getTransactionSendState(txData)
},
)
}
// endregion