Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-15 11:20:04 +05:00
commit 05b27a338b
14 changed files with 305 additions and 44 deletions

View file

@ -22,6 +22,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.derivationIndex
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.api.navigation.EditReturnTracker
import com.tangem.features.send.api.subcomponents.amount.AmountRoute
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
@ -61,6 +62,8 @@ internal class DefaultSendComponent @AssistedInject constructor(
private val model: SendModel = getOrCreateModel(params = params, router = innerRouter)
private val editReturnTracker = EditReturnTracker<CommonSendRoute> { it.isEditMode }
private val childStack = childStack(
key = "sendInnerStack",
source = stackNavigation,
@ -83,6 +86,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,
) { stack ->
val isReturnedFromEdit = editReturnTracker.onRouteActivated(stack.active.configuration)
when (val activeComponent = stack.active.instance) {
is SendConfirmComponent -> {
val fromCurrency = params.currency
@ -99,8 +103,9 @@ internal class DefaultSendComponent @AssistedInject constructor(
type = model.consumeEntryType(),
),
)
if (childStack.value.active.configuration.isEditMode) {
activeComponent.updateState(model.uiState.value)
// A reused Confirm gets no constructor state — re-push the edited fields on edit-return
if (isReturnedFromEdit) {
activeComponent.updateEditedState(model.uiState.value)
}
}
is SendAmountComponent -> {

View file

@ -137,11 +137,10 @@ internal class SendConfirmComponent(
}.launchIn(componentScope)
}
fun updateState(state: SendUM) {
fun updateEditedState(state: SendUM) {
destinationBlockComponent.updateState(state.destinationUM)
amountBlockComponent.updateState(state.amountUM)
feeSelectorBlockComponent.updateState(state.feeSelectorUM)
model.updateState(state)
model.updateEditedState(state)
}
@Composable

View file

@ -166,8 +166,13 @@ internal class SendConfirmModel @Inject constructor(
subscribeOnTapHelpUpdates()
}
fun updateState(state: SendUM) {
_uiState.value = state
/**
* Applies the fields editable outside Confirm (amount, destination) from the parent's [state].
* Confirm-local fields (confirmUM, feeSelectorUM) must be kept the parent's copies of them
* stay stale until a successful send.
*/
fun updateEditedState(state: SendUM) {
_uiState.update { it.copy(amountUM = state.amountUM, destinationUM = state.destinationUM) }
onFeeReload()
updateConfirmNotifications()
}

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.send.api.NFTSendComponent
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.api.navigation.EditReturnTracker
import com.tangem.features.send.api.subcomponents.destination.DestinationRoute
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
@ -53,6 +54,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
private val model: NFTSendModel = getOrCreateModel(params = params, router = innerRouter)
private val editReturnTracker = EditReturnTracker<CommonSendRoute> { it.isEditMode }
private val childStack = childStack(
key = "NFTSendInnerStack",
source = stackNavigation,
@ -75,6 +78,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,
) { stack ->
val isReturnedFromEdit = editReturnTracker.onRouteActivated(stack.active.configuration)
when (val activeComponent = stack.active.instance) {
is NFTSendConfirmComponent -> {
val fromCurrency = model.cryptoCurrency
@ -90,9 +94,9 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
toDerivationIndex = null,
),
)
// Push current state into a reused Confirm on (re)entry. Confirm.isEditMode is `true`
if (stack.active.configuration.isEditMode) {
activeComponent.updateState(model.uiState.value)
// A reused Confirm gets no constructor state — re-push the edited fields on edit-return
if (isReturnedFromEdit) {
activeComponent.updateEditedState(model.uiState.value)
}
}
is SendDestinationComponent -> {

View file

@ -134,9 +134,9 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
}.launchIn(componentScope)
}
fun updateState(state: NFTSendUM) {
fun updateEditedState(state: NFTSendUM) {
destinationBlockComponent.updateState(state.destinationUM)
model.updateState(state)
model.updateEditedState(state)
}
@Composable

View file

@ -127,8 +127,13 @@ internal class NFTSendConfirmModel @Inject constructor(
initialState()
}
fun updateState(nftSendUM: NFTSendUM) {
_uiState.value = nftSendUM
/**
* Applies the field editable outside Confirm (destination) from the parent's [nftSendUM].
* Confirm-local fields (confirmUM, feeSelectorUM) must be kept the parent's copies of them
* stay stale until a successful send.
*/
fun updateEditedState(nftSendUM: NFTSendUM) {
_uiState.update { it.copy(destinationUM = nftSendUM.destinationUM) }
updateConfirmNotifications()
}

View file

@ -6,6 +6,7 @@ import arrow.core.right
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.google.common.truth.Truth.assertThat
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.decompose.model.MutableParamsContainer
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -235,6 +236,59 @@ internal class SendConfirmModelTest : SendModelTestBase() {
}
}
@Nested
inner class UpdateEditedState {
@Test
fun `GIVEN stale parent state WHEN updateEditedState THEN amount and destination applied`() = runTest {
// Arrange
val sut = createSendConfirmModel(this, confirmParams(normalFeeState()))
advanceUntilIdle()
val editedAmount = mockk<AmountState.Data>(relaxed = true)
val editedDestination = mockk<DestinationUM.Content>(relaxed = true)
// Act
sut.updateEditedState(staleParentState(editedAmount, editedDestination))
advanceUntilIdle()
// Assert
assertThat(sut.uiState.value.amountUM).isEqualTo(editedAmount)
assertThat(sut.uiState.value.destinationUM).isEqualTo(editedDestination)
coVerify(exactly = 1) { feeSelectorReloadTrigger.triggerUpdate() }
}
@Test
fun `GIVEN stale parent state WHEN updateEditedState THEN confirm-local state preserved`() = runTest {
// Arrange: the parent's confirmUM/feeSelectorUM stay Empty/Loading until a successful send —
// they must not leak into the confirm model (blocks turn unclickable on ConfirmUM.Empty)
val sut = createSendConfirmModel(this, confirmParams(normalFeeState()))
advanceUntilIdle()
val feeSelectorUMBefore = sut.uiState.value.feeSelectorUM
// Act
sut.updateEditedState(
staleParentState(
amountUM = mockk<AmountState.Data>(relaxed = true),
destinationUM = mockk<DestinationUM.Content>(relaxed = true),
),
)
advanceUntilIdle()
// Assert: confirmUM may be recomputed (notifications), but must stay Content — never the
// parent's Empty, which would disable the confirm blocks; the fee state must survive as is
assertThat(sut.uiState.value.confirmUM).isInstanceOf(ConfirmUM.Content::class.java)
assertThat(sut.uiState.value.feeSelectorUM).isEqualTo(feeSelectorUMBefore)
}
private fun staleParentState(amountUM: AmountState, destinationUM: DestinationUM) = SendUM(
amountUM = amountUM,
destinationUM = destinationUM,
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
confirmData = null,
)
}
// region fixtures
private fun confirmParams(state: SendUM) = MutableParamsContainer(

View file

@ -3,6 +3,7 @@ package com.tangem.features.send.sendnft.confirm.model
import android.os.SystemClock
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
@ -261,6 +262,36 @@ internal class NFTSendConfirmModelTest {
}
}
@Nested
inner class UpdateEditedState {
@Test
fun `GIVEN stale parent state WHEN updateEditedState THEN destination applied and local state kept`() =
runTest {
// Arrange
val sut = buildModel()
advanceUntilIdle()
val feeSelectorUMBefore = sut.uiState.value.feeSelectorUM
val editedDestination = mockk<DestinationUM.Content>(relaxed = true)
// Act: the parent's confirmUM/feeSelectorUM stay Empty/Loading until a successful send —
// they must not leak into the confirm model (blocks turn unclickable on ConfirmUM.Empty)
sut.updateEditedState(
NFTSendUM(
destinationUM = editedDestination,
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
),
)
advanceUntilIdle()
// Assert
assertThat(sut.uiState.value.destinationUM).isEqualTo(editedDestination)
assertThat(sut.uiState.value.confirmUM).isInstanceOf(ConfirmUM.Content::class.java)
assertThat(sut.uiState.value.feeSelectorUM).isEqualTo(feeSelectorUMBefore)
}
}
// region fixtures
private fun TestScope.buildModel(