Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-20 19:24:05 +05:00
parent 6e642be3ca
commit 35898a31cd
8 changed files with 82 additions and 53 deletions

View file

@ -27,7 +27,7 @@ internal class SendNotificationFactory(
) {
fun create(): Flow<ImmutableList<SendNotification>> = currentStateProvider().currentState
.filter { it == SendUiStateType.Send }
.filter { it.type == SendUiStateType.Send }
.map {
val state = currentStateProvider()
val feeState = state.feeState ?: return@map persistentListOf()

View file

@ -78,7 +78,7 @@ internal class SendStateFactory(
// region UI states
fun getInitialState(): SendUiState = SendUiState(
clickIntents = clickIntents,
currentState = MutableStateFlow(SendUiStateType.None),
currentState = MutableStateFlow(SendUiCurrentScreen(type = SendUiStateType.None, isFromConfirmation = false)),
event = consumedEvent(),
isEditingDisabled = false,
isBalanceHidden = false,

View file

@ -33,7 +33,7 @@ internal data class SendUiState(
val feeState: SendStates.FeeState? = null,
val sendState: SendStates.SendState = SendStates.SendState(),
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
val currentState: StateFlow<SendUiStateType>,
val currentState: StateFlow<SendUiCurrentScreen>,
val isBalanceHidden: Boolean,
val event: StateEvent<SendEvent>,
)
@ -97,10 +97,16 @@ internal sealed class SendStates {
val transactionDate: Long = 0L,
val txUrl: String = "",
val ignoreAmountReduce: Boolean = false,
val isFromConfirmation: Boolean = true,
val notifications: ImmutableList<SendNotification> = persistentListOf(),
) : SendStates()
}
data class SendUiCurrentScreen(
val type: SendUiStateType,
val isFromConfirmation: Boolean,
)
enum class SendUiStateType {
None,
Amount,

View file

@ -14,58 +14,65 @@ internal class StateRouter(
private val analyticsEventsHandler: AnalyticsEventHandler,
private val isEditingDisabled: Boolean,
) {
private var mutableCurrentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(
private var mutableCurrentState: MutableStateFlow<SendUiCurrentScreen> = MutableStateFlow(
if (isEditingDisabled) {
SendUiStateType.None
SendUiCurrentScreen(
type = SendUiStateType.None,
isFromConfirmation = false,
)
} else {
SendUiStateType.Recipient
SendUiCurrentScreen(
type = SendUiStateType.Recipient,
isFromConfirmation = false,
)
},
)
val currentState: StateFlow<SendUiStateType> = mutableCurrentState
val currentState: StateFlow<SendUiCurrentScreen> = mutableCurrentState
fun popBackStack() {
fragmentManager.get()?.popBackStack()
}
fun onBackClick(isSuccess: Boolean = false) {
val type = currentState.value.type
when {
isSuccess -> popBackStack()
isEditingDisabled -> when (currentState.value) {
isEditingDisabled -> when (type) {
SendUiStateType.Send -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
showFee()
}
else -> popBackStack()
}
else -> when (currentState.value) {
else -> when (type) {
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Address))
showRecipient()
continueToSend(::showRecipient)
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
showAmount()
continueToSend(::showAmount)
}
SendUiStateType.Send -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
showFee()
continueToSend(::showFee)
}
else -> popBackStack()
else -> continueToSend(::popBackStack)
}
}
}
fun onNextClick(): SendUiStateType {
val prevState = currentState.value
when (currentState.value) {
val prevState = currentState.value.type
when (currentState.value.type) {
SendUiStateType.Recipient -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Amount))
showAmount()
continueToSend(::showAmount)
}
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
showFee()
continueToSend(::showFee)
}
SendUiStateType.Fee -> {
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
@ -83,7 +90,7 @@ internal class StateRouter(
if (isEditingDisabled) {
popBackStack()
} else {
when (currentState.value) {
when (currentState.value.type) {
SendUiStateType.Amount -> {
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
showRecipient()
@ -97,23 +104,27 @@ internal class StateRouter(
}
}
fun showAmount() {
fun showAmount(isFromConfirmation: Boolean = false) {
analyticsEventsHandler.send(SendAnalyticEvents.AmountScreenOpened)
mutableCurrentState.update { SendUiStateType.Amount }
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Amount, isFromConfirmation) }
}
fun showRecipient() {
fun showRecipient(isFromConfirmation: Boolean = false) {
analyticsEventsHandler.send(SendAnalyticEvents.AddressScreenOpened)
mutableCurrentState.update { SendUiStateType.Recipient }
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Recipient, isFromConfirmation) }
}
fun showFee() {
fun showFee(isFromConfirmation: Boolean = false) {
analyticsEventsHandler.send(SendAnalyticEvents.FeeScreenOpened)
mutableCurrentState.update { SendUiStateType.Fee }
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Fee, isFromConfirmation) }
}
private fun continueToSend(show: () -> Unit) {
if (currentState.value.isFromConfirmation) showSend() else show()
}
private fun showSend() {
analyticsEventsHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
mutableCurrentState.update { SendUiStateType.Send }
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Send, isFromConfirmation = false) }
}
}

View file

@ -32,7 +32,7 @@ internal class FeeNotificationFactory(
) {
fun create() = currentStateProvider().currentState
.filter { it == SendUiStateType.Fee }
.filter { it.type == SendUiStateType.Fee }
.map {
val state = currentStateProvider()
val feeState = state.feeState ?: return@map persistentListOf()

View file

@ -15,7 +15,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -24,25 +23,29 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
@Composable
internal fun SendNavigationButtons(uiState: SendUiState) {
internal fun SendNavigationButtons(uiState: SendUiState, currentState: State<SendUiCurrentScreen>) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing12),
) {
SendSecondaryNavigationButton(uiState)
SendSecondaryNavigationButton(
uiState = uiState,
currentState = currentState,
)
SendPrimaryNavigationButton(
uiState = uiState,
currentState = currentState,
modifier = Modifier
.weight(1f)
.padding(horizontal = TangemTheme.dimens.spacing16),
@ -51,12 +54,13 @@ internal fun SendNavigationButtons(uiState: SendUiState) {
}
@Composable
private fun SendSecondaryNavigationButton(uiState: SendUiState) {
val currentState = uiState.currentState.collectAsState()
private fun SendSecondaryNavigationButton(uiState: SendUiState, currentState: State<SendUiCurrentScreen>) {
val isEditingDisabled = uiState.isEditingDisabled
val isCorrectScreen = currentState.value == SendUiStateType.Amount || currentState.value == SendUiStateType.Fee
val isFromConfirmation = currentState.value.isFromConfirmation
val isCorrectScreen =
currentState.value.type == SendUiStateType.Amount || currentState.value.type == SendUiStateType.Fee
AnimatedVisibility(
visible = !isEditingDisabled && isCorrectScreen,
visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation,
enter = expandHorizontally(expandFrom = Alignment.End),
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
) {
@ -77,8 +81,11 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) {
}
@Composable
private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
val currentState = uiState.currentState.collectAsStateWithLifecycle()
private fun SendPrimaryNavigationButton(
uiState: SendUiState,
currentState: State<SendUiCurrentScreen>,
modifier: Modifier = Modifier,
) {
val isSuccess = uiState.sendState.isSuccess
val isSending = uiState.sendState.isSending
val txUrl = uiState.sendState.txUrl
@ -100,7 +107,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
modifier = modifier,
) { textId ->
when {
currentState.value == SendUiStateType.Send && !isSuccess -> {
currentState.value.type == SendUiStateType.Send && !isSuccess -> {
val hapticFeedback = rememberHapticFeedback(state = currentState, onAction = buttonClick)
PrimaryButtonIconEnd(
text = stringResource(textId),
@ -110,7 +117,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
showProgress = isSending,
)
}
currentState.value == SendUiStateType.Send && isSuccess -> {
currentState.value.type == SendUiStateType.Send && isSuccess -> {
PrimaryButtonsDone(
textRes = textId,
txUrl = txUrl,
@ -177,15 +184,19 @@ private fun PrimaryButtonsDone(
private fun getButtonData(
uiState: SendUiState,
currentState: State<SendUiStateType>,
currentState: State<SendUiCurrentScreen>,
isSuccess: Boolean,
): Pair<Int, () -> Unit> {
return when (currentState.value) {
return when (currentState.value.type) {
SendUiStateType.None,
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,
-> R.string.common_next to uiState.clickIntents::onNextClick
-> if (currentState.value.isFromConfirmation) {
R.string.common_continue to uiState.clickIntents::onNextClick
} else {
R.string.common_next to uiState.clickIntents::onNextClick
}
SendUiStateType.Send -> if (isSuccess) {
R.string.common_close
} else {
@ -194,8 +205,8 @@ private fun getButtonData(
}
}
private fun isButtonEnabled(currentState: State<SendUiStateType>, uiState: SendUiState): Boolean {
return when (currentState.value) {
private fun isButtonEnabled(currentState: State<SendUiCurrentScreen>, uiState: SendUiState): Boolean {
return when (currentState.value.type) {
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false

View file

@ -19,6 +19,7 @@ import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
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.ui.amount.SendAmountContent
@ -40,14 +41,14 @@ internal fun SendScreen(uiState: SendUiState) {
.background(color = TangemTheme.colors.background.tertiary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val titleRes = when (currentState.value) {
val titleRes = when (currentState.value.type) {
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) R.string.send_confirm_label else null
else -> null
}
val iconRes = if (currentState.value == SendUiStateType.Recipient) {
val iconRes = if (currentState.value.type == SendUiStateType.Recipient) {
R.drawable.ic_qrcode_scan_24
} else {
null
@ -67,7 +68,7 @@ internal fun SendScreen(uiState: SendUiState) {
modifier = Modifier
.weight(1f),
)
SendNavigationButtons(uiState)
SendNavigationButtons(uiState, currentState)
}
SendEventEffect(
@ -79,7 +80,7 @@ internal fun SendScreen(uiState: SendUiState) {
@Composable
private fun SendScreenContent(
uiState: SendUiState,
currentState: State<SendUiStateType>,
currentState: State<SendUiCurrentScreen>,
modifier: Modifier = Modifier,
) {
val recipientList = uiState.recipientList.collectAsLazyPagingItems()
@ -88,7 +89,7 @@ private fun SendScreenContent(
label = "Send Scree Navigation",
modifier = modifier,
) { state ->
when (state) {
when (state.type) {
SendUiStateType.Amount -> SendAmountContent(
amountState = uiState.amountState,
isBalanceHiding = uiState.isBalanceHidden,

View file

@ -377,8 +377,8 @@ internal class SendViewModel @Inject constructor(
private fun onStateActive() {
uiState.currentState
.onEach {
when (it) {
SendUiStateType.Fee -> loadFee()
when (it.type) {
SendUiStateType.Fee -> if (!it.isFromConfirmation) loadFee()
SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis()
else -> Unit
}
@ -576,17 +576,17 @@ internal class SendViewModel @Inject constructor(
}
override fun showAmount() {
stateRouter.showAmount()
stateRouter.showAmount(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
}
override fun showRecipient() {
stateRouter.showRecipient()
stateRouter.showRecipient(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
}
override fun showFee() {
stateRouter.showFee()
stateRouter.showFee(isFromConfirmation = true)
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
}