Updated on 2026-08-14
This commit is contained in:
parent
f5e14a9ab4
commit
b9a5220984
12 changed files with 526 additions and 425 deletions
|
|
@ -1,99 +0,0 @@
|
|||
package com.tangem.features.send.common.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
|
||||
@Composable
|
||||
internal fun SendContent(
|
||||
navigationUM: NavigationUM,
|
||||
confirmUM: ConfirmUM,
|
||||
stackState: ChildStack<CommonSendRoute, ComposableContentComponent>,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SendAppBar(navigationUM = navigationUM)
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation { child ->
|
||||
when (child.configuration) {
|
||||
is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f)
|
||||
is CommonSendRoute.Confirm -> fade()
|
||||
else -> slide()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxHeight()) {
|
||||
it.instance.Content(Modifier.fillMaxSize(1f))
|
||||
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
Fade(
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = stackState.active.configuration == CommonSendRoute.Confirm,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
SendingText(footerText = (confirmUM as? ConfirmUM.Content)?.sendingFooter ?: TextReference.EMPTY)
|
||||
}
|
||||
NavigationButtonsBlockV2(
|
||||
navigationUM = navigationUM,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendAppBar(navigationUM: NavigationUM) {
|
||||
val navigationUMContent = navigationUM as? NavigationUM.Content ?: return
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = navigationUMContent.title.resolveReference(),
|
||||
subtitle = navigationUMContent.subtitle?.resolveReference(),
|
||||
onBackClick = navigationUMContent.backIconClick,
|
||||
onIconClick = navigationUMContent.additionalIconClick,
|
||||
backIconRes = navigationUMContent.backIconRes,
|
||||
iconRes = navigationUMContent.additionalIconRes,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.send.common.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
|
||||
/**
|
||||
* Shared pull-based host for the regular Send and NFT Send flows (both over [CommonSendRoute]). Renders the
|
||||
* ACTIVE child's [ComposableModularContentComponent.Title] / [ComposableModularContentComponent.Footer] slots
|
||||
* in place (matching the previous in-place app-bar/footer behavior), while keeping the per-route slide/fade
|
||||
* Decompose [Children] animation for the Content region (and the bottom `Fade` gradient, hidden on
|
||||
* `ConfirmSuccess`, exactly as the previous `SendContent`).
|
||||
*
|
||||
* Each step's `Footer()` owns its own bottom block (Confirm reveals `SendingText`; Success/Empty render
|
||||
* nothing), so the host no longer special-cases routes for the footer.
|
||||
*/
|
||||
@Composable
|
||||
internal fun SendModularContent(stackState: ChildStack<CommonSendRoute, ComposableModularContentComponent>) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
stackState.active.instance.Title()
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation { child ->
|
||||
when (child.configuration) {
|
||||
is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f)
|
||||
is CommonSendRoute.Confirm -> fade()
|
||||
else -> slide()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxHeight()) {
|
||||
it.instance.Content(Modifier.fillMaxSize(1f))
|
||||
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
Fade(
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
stackState.active.instance.Footer()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package com.tangem.features.send.common.utils
|
||||
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
|
||||
/**
|
||||
* Workaround to try fix duplicate route crash
|
||||
*/
|
||||
internal fun Router.safeNextClick(
|
||||
currentRoute: CommonSendRoute,
|
||||
nextRoute: CommonSendRoute,
|
||||
childStack: Value<ChildStack<CommonSendRoute, ComposableContentComponent>>,
|
||||
popBack: () -> Unit,
|
||||
) {
|
||||
if (currentRoute.isEditMode) {
|
||||
popBack()
|
||||
} else {
|
||||
val isAlreadyInStack = childStack.value.items.any { it.configuration == nextRoute }
|
||||
if (isAlreadyInStack) {
|
||||
popTo(nextRoute)
|
||||
} else {
|
||||
push(nextRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,9 @@
|
|||
package com.tangem.features.send.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
|
|
@ -17,49 +11,45 @@ import com.arkivanov.decompose.router.stack.pop
|
|||
import com.arkivanov.decompose.value.ObserveLifecycleMode
|
||||
import com.arkivanov.decompose.value.subscribe
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
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.subcomponents.amount.AmountRoute
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
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
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.ui.SendContent
|
||||
import com.tangem.features.send.common.ui.SendModularContent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.send.model.SendModel
|
||||
import com.tangem.features.send.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.subcomponents.amount.DefaultSendAmountComponent
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultSendComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendComponent.Params,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val amountComponentFactory: SendAmountComponent.Factory,
|
||||
private val destinationComponentFactory: SendDestinationComponent.Factory,
|
||||
private val sendConfirmSuccessComponent: SendConfirmSuccessComponent.Factory,
|
||||
private val feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
private val sendDestinationComponentFactory: SendDestinationComponent.Factory,
|
||||
) : SendComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<CommonSendRoute>()
|
||||
|
|
@ -93,48 +83,45 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
lifecycle = lifecycle,
|
||||
mode = ObserveLifecycleMode.CREATE_DESTROY,
|
||||
) { stack ->
|
||||
componentScope.launch {
|
||||
when (val activeComponent = stack.active.instance) {
|
||||
is SendConfirmComponent -> {
|
||||
val fromCurrency = params.currency
|
||||
val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value
|
||||
.takeIf { model.isAccountModeFlow.value }
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ConfirmationScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
sendBlockchain = fromCurrency.network.name,
|
||||
sendToken = fromCurrency.symbol,
|
||||
fromDerivationIndex = fromDerivationIndex,
|
||||
toDerivationIndex = null,
|
||||
type = model.consumeEntryType(),
|
||||
),
|
||||
)
|
||||
if (model.currentRoute.value.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
is DefaultSendAmountComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AmountScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
type = model.consumeEntryType(),
|
||||
),
|
||||
)
|
||||
activeComponent.updateState(model.uiState.value.amountUM)
|
||||
}
|
||||
is SendDestinationComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AddressScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
),
|
||||
)
|
||||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
when (val activeComponent = stack.active.instance) {
|
||||
is SendConfirmComponent -> {
|
||||
val fromCurrency = params.currency
|
||||
val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value
|
||||
.takeIf { model.isAccountModeFlow.value }
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ConfirmationScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
sendBlockchain = fromCurrency.network.name,
|
||||
sendToken = fromCurrency.symbol,
|
||||
fromDerivationIndex = fromDerivationIndex,
|
||||
toDerivationIndex = null,
|
||||
type = model.consumeEntryType(),
|
||||
),
|
||||
)
|
||||
if (childStack.value.active.configuration.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
model.currentRoute.emit(stack.active.configuration)
|
||||
is SendAmountComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AmountScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
type = model.consumeEntryType(),
|
||||
),
|
||||
)
|
||||
activeComponent.updateState(model.uiState.value.amountUM)
|
||||
}
|
||||
is SendDestinationComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AddressScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
source = model.analyticsSendSource,
|
||||
),
|
||||
)
|
||||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,34 +129,31 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val stackState by childStack.subscribeAsState()
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(
|
||||
onBack = {
|
||||
(state.navigationUM as? NavigationUM.Content)?.backIconClick() ?: onChildBack()
|
||||
},
|
||||
)
|
||||
SendContent(
|
||||
navigationUM = state.navigationUM,
|
||||
confirmUM = state.confirmUM,
|
||||
stackState = stackState,
|
||||
)
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
SendModularContent(stackState = stackState)
|
||||
}
|
||||
|
||||
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
|
||||
CommonSendRoute.Empty -> getStubComponent()
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext)
|
||||
is CommonSendRoute.Amount -> getAmountComponent(factoryContext)
|
||||
private fun createChild(
|
||||
route: CommonSendRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent = when (route) {
|
||||
CommonSendRoute.Empty -> ComposableModularContentComponent.EMPTY
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(route, factoryContext)
|
||||
is CommonSendRoute.Amount -> getAmountComponent(route, factoryContext)
|
||||
is CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
|
||||
is CommonSendRoute.ConfirmSuccess -> getConfirmSuccessComponent(factoryContext)
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext): SendDestinationComponent =
|
||||
sendDestinationComponentFactory.create(
|
||||
private fun getDestinationComponent(
|
||||
route: DestinationRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent {
|
||||
return destinationComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = model.currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
analyticsSendSource = model.analyticsSendSource,
|
||||
|
|
@ -179,13 +163,17 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
callback = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAmountComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getAmountComponent(
|
||||
route: AmountRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent {
|
||||
return amountComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendAmountComponentParams.AmountParams(
|
||||
state = model.uiState.value.amountUM,
|
||||
currentRoute = model.currentRoute.filterIsInstance<CommonSendRoute.Amount>(),
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
appCurrency = model.appCurrency,
|
||||
|
|
@ -201,7 +189,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
return if (model.isAvailableForSend) {
|
||||
val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value
|
||||
val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value
|
||||
|
|
@ -210,7 +198,6 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
params = SendConfirmComponent.Params(
|
||||
state = model.uiState.value,
|
||||
userWallet = model.userWallet,
|
||||
currentRoute = model.currentRoute,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
|
|
@ -233,79 +220,43 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
)
|
||||
} else {
|
||||
model.showAlertError()
|
||||
getStubComponent()
|
||||
ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfirmSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getConfirmSuccessComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
val state = model.uiState.value
|
||||
val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value
|
||||
val txUrl = (state.confirmUM as? ConfirmUM.Success)?.txUrl
|
||||
val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value
|
||||
|
||||
if (sendAmount == null ||
|
||||
destinationAddress == null ||
|
||||
txUrl == null
|
||||
) {
|
||||
model.showAlertError()
|
||||
return getStubComponent()
|
||||
return ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
|
||||
val destinationBlockComponent =
|
||||
DefaultSendDestinationBlockComponent(
|
||||
appComponentContext = child("sendConfirmDestinationBlock"),
|
||||
params = SendDestinationComponentParams.DestinationBlockParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
analyticsSendSource = model.analyticsSendSource,
|
||||
userWalletId = model.userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
blockClickEnableFlow = MutableStateFlow(true),
|
||||
predefinedValues = model.predefinedValues,
|
||||
isAddContactAvailable = true,
|
||||
),
|
||||
onResult = { },
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
return SendConfirmSuccessComponent(
|
||||
return sendConfirmSuccessComponent.create(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendConfirmSuccessComponent.Params(
|
||||
sendUMFlow = model.uiState,
|
||||
destinationBlockComponent = destinationBlockComponent,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.currency,
|
||||
predefinedValues = model.predefinedValues,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
currentRoute = model.currentRoute,
|
||||
txUrl = txUrl,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStubComponent() = StubComponent()
|
||||
|
||||
class StubComponent : ComposableContentComponent {
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty
|
||||
val isEmptyStack = childStack.value.backStack.isEmpty()
|
||||
val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success
|
||||
val isStubComponent = childStack.value.active.instance is StubComponent
|
||||
val isStubComponent = childStack.value.active.instance == EmptyComposableBottomSheetComponent
|
||||
val isSendingInProgress = (model.uiState.value.confirmUM as? ConfirmUM.Content)?.isSending == true
|
||||
|
||||
val isPopSend = isEmptyRoute || isEmptyStack || isSuccess || isStubComponent
|
||||
|
|
|
|||
|
|
@ -1,15 +1,26 @@
|
|||
package com.tangem.features.send.send.confirm
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -18,17 +29,17 @@ import com.tangem.domain.transaction.error.GetFeeError
|
|||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.confirm.model.SendConfirmModel
|
||||
import com.tangem.features.send.send.confirm.ui.SendConfirmContent
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.subcomponents.amount.DefaultSendAmountBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import com.tangem.features.send.subcomponents.notifications.DefaultSendNotificationsComponent
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -38,7 +49,7 @@ internal class SendConfirmComponent(
|
|||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendConfirmModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -133,6 +144,20 @@ internal class SendConfirmComponent(
|
|||
model.updateState(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.common_send),
|
||||
onBackClick = {
|
||||
model.onBackClick()
|
||||
router.pop()
|
||||
},
|
||||
backIconRes = R.drawable.ic_back_24,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -148,6 +173,29 @@ internal class SendConfirmComponent(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
Column {
|
||||
val sendingFooter = (state.confirmUM as? ConfirmUM.Content)?.sendingFooter
|
||||
AnimatedVisibility(
|
||||
visible = sendingFooter != null,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
SendingText(footerText = sendingFooter ?: TextReference.EMPTY)
|
||||
}
|
||||
NavigationPrimaryButton(
|
||||
primaryButton = model.primaryButtonUM(state.confirmUM),
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val state: SendUM,
|
||||
val analyticsCategoryName: String,
|
||||
|
|
@ -161,7 +209,6 @@ internal class SendConfirmComponent(
|
|||
val isAccountModeFlow: StateFlow<Boolean>,
|
||||
val appCurrency: AppCurrency,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<CommonSendRoute>,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.common.routing.AppRouter
|
|||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -160,7 +159,6 @@ internal class SendConfirmModel @Inject constructor(
|
|||
|
||||
init {
|
||||
updateAmountSubtractAvailability()
|
||||
configConfirmNavigation()
|
||||
subscribeOnNotificationsUpdateTrigger()
|
||||
subscribeOnCheckFeeResultUpdates()
|
||||
initialState()
|
||||
|
|
@ -303,6 +301,17 @@ internal class SendConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = uiState.value.confirmUM.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
|
||||
|
|
@ -543,58 +552,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
return isHighNetworkFeeUseCase(feeCurrency, feeAmount)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun configConfirmNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter {
|
||||
it.second is CommonSendRoute.Confirm
|
||||
}.onEach { (state, _) ->
|
||||
val confirmUM = state.confirmUM
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = CommonSendRoute.Confirm.javaClass.simpleName,
|
||||
title = resourceReference(id = R.string.common_send),
|
||||
subtitle = null,
|
||||
backIconRes = when (confirmUM) {
|
||||
is ConfirmUM.Success -> R.drawable.ic_close_24
|
||||
else -> R.drawable.ic_back_24
|
||||
},
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = confirmUM.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
router.pop()
|
||||
},
|
||||
primaryButton = primaryButtonUM(),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = (
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = ::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = ::onShareClick,
|
||||
)
|
||||
).takeIf { confirmUM is ConfirmUM.Success },
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun primaryButtonUM(): NavigationButton {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
fun primaryButtonUM(confirmUM: ConfirmUM): NavigationButton {
|
||||
val isContent = confirmUM is ConfirmUM.Content
|
||||
val isReadyToSend = isContent && !confirmUM.isSending
|
||||
val isHoldToConfirm = userWallet.isHotWallet && isContent
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import arrow.core.left
|
|||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||
|
|
@ -123,8 +123,6 @@ internal class SendModel @Inject constructor(
|
|||
CommonSendRoute.Empty
|
||||
}
|
||||
|
||||
val currentRoute = MutableStateFlow(initialRoute)
|
||||
|
||||
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
|
||||
field = MutableStateFlow(
|
||||
CryptoCurrencyStatus(
|
||||
|
|
@ -149,7 +147,7 @@ internal class SendModel @Inject constructor(
|
|||
return cryptoCurrencyStatus.isAvailableForSend() && feeCryptoCurrencyStatus.isAvailableForSend()
|
||||
}
|
||||
|
||||
val isUnavailableForSend: Boolean
|
||||
private val isUnavailableForSend: Boolean
|
||||
get() {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value
|
||||
val feeCryptoCurrencyStatus = feeCryptoCurrencyStatusFlow.value
|
||||
|
|
@ -179,10 +177,6 @@ internal class SendModel @Inject constructor(
|
|||
initAppCurrency()
|
||||
}
|
||||
|
||||
override fun onNavigationResult(navigationUM: NavigationUM) {
|
||||
uiState.update { it.copy(navigationUM = navigationUM) }
|
||||
}
|
||||
|
||||
override fun onDestinationResult(destinationUM: DestinationUM) {
|
||||
uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
|
@ -196,9 +190,9 @@ internal class SendModel @Inject constructor(
|
|||
uiState.update { sendUM }
|
||||
}
|
||||
|
||||
override fun onBackClick() {
|
||||
when (val route = currentRoute.value) {
|
||||
is CommonSendRoute.Amount -> if (!route.isEditMode) {
|
||||
override fun onBackClick(currentRoute: Route) {
|
||||
when (currentRoute) {
|
||||
is CommonSendRoute.Amount -> if (!currentRoute.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticCategoryName,
|
||||
|
|
@ -208,7 +202,7 @@ internal class SendModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
is CommonSendRoute.Destination -> if (!route.isEditMode) {
|
||||
is CommonSendRoute.Destination -> if (!currentRoute.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticCategoryName,
|
||||
|
|
@ -224,11 +218,11 @@ internal class SendModel @Inject constructor(
|
|||
router.pop()
|
||||
}
|
||||
|
||||
override fun onNextClick() {
|
||||
if (currentRoute.value.isEditMode) {
|
||||
onBackClick()
|
||||
override fun onNextClick(currentRoute: Route) {
|
||||
if ((currentRoute as? CommonSendRoute)?.isEditMode == true) {
|
||||
onBackClick(currentRoute)
|
||||
} else {
|
||||
when (currentRoute.value) {
|
||||
when (currentRoute) {
|
||||
is CommonSendRoute.Amount -> {
|
||||
val nextRoute = if (predefinedValues.isFromMainScreenQr) {
|
||||
CommonSendRoute.Confirm
|
||||
|
|
@ -239,7 +233,7 @@ internal class SendModel @Inject constructor(
|
|||
}
|
||||
is CommonSendRoute.Destination -> router.push(CommonSendRoute.Confirm)
|
||||
CommonSendRoute.Confirm -> router.push(CommonSendRoute.ConfirmSuccess)
|
||||
else -> onBackClick()
|
||||
else -> router.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -263,7 +257,6 @@ internal class SendModel @Inject constructor(
|
|||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
confirmData = null,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
router.popTo(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
|
@ -442,7 +435,7 @@ internal class SendModel @Inject constructor(
|
|||
cryptoCurrencyStatusFlow,
|
||||
feeCryptoCurrencyStatusFlow,
|
||||
) { cryptoCurrencyStatus, _ ->
|
||||
if (!isAvailableForSend || currentRoute.value != initialRoute) {
|
||||
if (!isAvailableForSend) {
|
||||
if (isUnavailableForSend) showAlertError()
|
||||
return@combine
|
||||
}
|
||||
|
|
@ -548,7 +541,6 @@ internal class SendModel @Inject constructor(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
).transform(DestinationUM.Empty()),
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
confirmData = null,
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,77 @@
|
|||
package com.tangem.features.send.send.success
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.navigationButtons.DoneButtons
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.success.model.SendConfirmSuccessModel
|
||||
import com.tangem.features.send.send.success.ui.SendConfirmSuccessContent
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class SendConfirmSuccessComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
internal class SendConfirmSuccessComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
destinationBlockComponentFactory: SendDestinationBlockComponent.Factory,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendConfirmSuccessModel = getOrCreateModel(params = params)
|
||||
private val destinationBlockComponent: SendDestinationBlockComponent = params.destinationBlockComponent
|
||||
private val destinationBlockComponent: SendDestinationBlockComponent = destinationBlockComponentFactory.create(
|
||||
context = child("sendConfirmDestinationBlock"),
|
||||
params = SendDestinationComponentParams.DestinationBlockParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Send,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
blockClickEnableFlow = MutableStateFlow(true),
|
||||
predefinedValues = params.predefinedValues,
|
||||
isAddContactAvailable = true,
|
||||
),
|
||||
onResult = {},
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
onBackClick = {
|
||||
model.onBackClick()
|
||||
router.pop()
|
||||
},
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
|
|
@ -32,11 +82,43 @@ internal class SendConfirmSuccessComponent(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
DoneButtons(
|
||||
(NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = model::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = model::onShareClick,
|
||||
)).takeIf { params.txUrl.isNotEmpty() },
|
||||
)
|
||||
PrimaryButton(
|
||||
text = stringResourceSafe(R.string.common_close),
|
||||
onClick = router::pop,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sendUMFlow: StateFlow<SendUM>,
|
||||
val destinationBlockComponent: SendDestinationBlockComponent,
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val analyticsCategoryName: String,
|
||||
val currentRoute: Flow<CommonSendRoute>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
val txUrl: String,
|
||||
val callback: ModelCallback,
|
||||
)
|
||||
|
|
@ -44,4 +126,9 @@ internal class SendConfirmSuccessComponent(
|
|||
interface ModelCallback {
|
||||
fun onResult(sendUM: SendUM)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(appComponentContext: AppComponentContext, params: Params): SendConfirmSuccessComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,17 @@
|
|||
package com.tangem.features.send.send.success.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
|
|
@ -31,7 +20,6 @@ internal class SendConfirmSuccessModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
) : Model() {
|
||||
|
|
@ -39,73 +27,23 @@ internal class SendConfirmSuccessModel @Inject constructor(
|
|||
private val _uiState = params.sendUMFlow
|
||||
val uiState = _uiState
|
||||
|
||||
init {
|
||||
configConfirmSuccessNavigation()
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun configConfirmSuccessNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter { (state, route) ->
|
||||
// Emit the success navigation exactly once. Building NavigationUM.Content here creates fresh
|
||||
// lambdas every time, so the SendUM written back via callback.onResult is never equal to the
|
||||
// previous one — without this guard the combine re-triggers itself endlessly and the success
|
||||
// screen recomposes forever (never reaching Compose idle). See [REDACTED_TASK_KEY].
|
||||
route is CommonSendRoute.ConfirmSuccess &&
|
||||
(state.navigationUM as? NavigationUM.Content)?.source !=
|
||||
CommonSendRoute.ConfirmSuccess.javaClass.simpleName
|
||||
}.onEach { (state, _) ->
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = CommonSendRoute.ConfirmSuccess.javaClass.simpleName,
|
||||
title = stringReference(""),
|
||||
subtitle = null,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = true,
|
||||
),
|
||||
)
|
||||
appRouter.pop()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
iconRes = null,
|
||||
isEnabled = true,
|
||||
isHapticClick = false,
|
||||
onClick = {
|
||||
appRouter.pop()
|
||||
},
|
||||
),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = (NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = ::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = ::onShareClick,
|
||||
)).takeIf { params.txUrl.isNotEmpty() },
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onExploreClick() {
|
||||
fun onExploreClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName))
|
||||
urlOpener.openUrl(params.txUrl)
|
||||
}
|
||||
|
||||
private fun onShareClick() {
|
||||
fun onShareClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName))
|
||||
shareManager.shareText(params.txUrl)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.amountScreen.ui.AmountBlock
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
|
|
@ -63,14 +62,6 @@ internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent
|
|||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
NavigationButtonsBlockV2(
|
||||
navigationUM = sendUM.navigationUM,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package com.tangem.features.send.send.ui.state
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.send.confirm.model.ConfirmData
|
||||
|
||||
|
|
@ -12,6 +11,5 @@ internal data class SendUM(
|
|||
val destinationUM: DestinationUM,
|
||||
val feeSelectorUM: FeeSelectorUM,
|
||||
val confirmUM: ConfirmUM,
|
||||
val navigationUM: NavigationUM,
|
||||
val confirmData: ConfirmData?,
|
||||
)
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.features.send.send.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.CloseButtonClicked as CloseButtonClickedEvent
|
||||
|
||||
/**
|
||||
* Guards the navigation refactor that moved the footer/app-bar actions out of the model into
|
||||
* `DefaultSendComponent`. The model's [SendModel.onBackClick] / [SendModel.onNextClick] no longer read
|
||||
* an internal `currentRoute` StateFlow — they receive the active [com.tangem.core.decompose.navigation.Route]
|
||||
* as a parameter and decide routing/analytics from it. These are pure, synchronous decisions, so each
|
||||
* method is asserted *before* advancing the scheduler; the model is then destroyed inside the test body
|
||||
* so its `init {}` collectors (all `modelScope.launch`/`launchIn`, including an infinite status collector)
|
||||
* are cancelled — never run — before `runTest`'s terminal advance.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SendModelNavigationTest {
|
||||
|
||||
private val router: Router = mockk(relaxed = true)
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase = mockk(relaxed = true)
|
||||
private val cryptoCurrency = MockCryptoCurrencyFactory().createCoin(Blockchain.Ethereum)
|
||||
|
||||
// region onNextClick
|
||||
|
||||
@Test
|
||||
fun `GIVEN manual entry on amount WHEN onNextClick THEN pushes destination`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.Destination(isEditMode = false)) }
|
||||
verify(exactly = 0) { router.push(CommonSendRoute.Confirm) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN main-screen QR predefined values WHEN onNextClick on amount THEN skips destination and pushes confirm`() =
|
||||
runTest {
|
||||
// Arrange — address-only predefined values resolve to a MAIN_SCREEN QrCode (isFromMainScreenQr = true).
|
||||
val model = createModel(this, params = qrParams())
|
||||
|
||||
// Act
|
||||
model.onNextClick(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.Confirm) }
|
||||
verify(exactly = 0) { router.push(CommonSendRoute.Destination(isEditMode = false)) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN destination step WHEN onNextClick THEN pushes confirm`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(CommonSendRoute.Destination(isEditMode = false))
|
||||
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.Confirm) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN edit-mode route WHEN onNextClick THEN pops instead of advancing`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(CommonSendRoute.Amount(isEditMode = true))
|
||||
|
||||
verify(exactly = 1) { router.pop() }
|
||||
verify(exactly = 0) { router.push(any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirm-success route WHEN onNextClick THEN pops`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(CommonSendRoute.ConfirmSuccess)
|
||||
|
||||
verify(exactly = 1) { router.pop() }
|
||||
verify(exactly = 0) { router.push(any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region onBackClick
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount step not in edit mode WHEN onBackClick THEN sends amount close analytics and pops`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onBackClick(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
||||
verify(exactly = 1) {
|
||||
analyticsEventHandler.send(match { it is CloseButtonClickedEvent && it.source == SendScreenSource.Amount })
|
||||
}
|
||||
verify(exactly = 1) { router.pop() }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN destination step not in edit mode WHEN onBackClick THEN sends address close analytics and pops`() =
|
||||
runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onBackClick(CommonSendRoute.Destination(isEditMode = false))
|
||||
|
||||
verify(exactly = 1) {
|
||||
analyticsEventHandler.send(
|
||||
match { it is CloseButtonClickedEvent && it.source == SendScreenSource.Address },
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) { router.pop() }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN edit-mode route WHEN onBackClick THEN pops without close analytics`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onBackClick(CommonSendRoute.Amount(isEditMode = true))
|
||||
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any<CloseButtonClickedEvent>()) }
|
||||
verify(exactly = 1) { router.pop() }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun manualParams() = SendComponent.Params(
|
||||
userWalletId = UserWalletId(stringValue = "0123456789"),
|
||||
currency = cryptoCurrency,
|
||||
)
|
||||
|
||||
private fun qrParams() = SendComponent.Params(
|
||||
userWalletId = UserWalletId(stringValue = "0123456789"),
|
||||
currency = cryptoCurrency,
|
||||
destinationAddress = "0xRECIPIENT",
|
||||
)
|
||||
|
||||
private fun createModel(testScope: TestScope, params: SendComponent.Params = manualParams()): SendModel {
|
||||
// Runs synchronously in init {}; a relaxed Either<Exception, …> would break getOrElse, so stub a Right.
|
||||
every { listenToQrScanningUseCase(any()) } returns emptyFlow<String>().right()
|
||||
return SendModel(
|
||||
paramsContainer = MutableParamsContainer(value = params),
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
router = router,
|
||||
getUserWalletUseCase = mockk(relaxed = true),
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase = mockk(relaxed = true),
|
||||
getSelectedAppCurrencyUseCase = mockk(relaxed = true),
|
||||
listenToQrScanningUseCase = listenToQrScanningUseCase,
|
||||
parseQrCodeUseCase = mockk(relaxed = true),
|
||||
sendConfirmAlertFactory = mockk(relaxed = true),
|
||||
saveBlockchainErrorUseCase = mockk(relaxed = true),
|
||||
getWalletMetaInfoUseCase = mockk(relaxed = true),
|
||||
sendFeedbackEmailUseCase = mockk(relaxed = true),
|
||||
getBalanceHidingSettingsUseCase = mockk(relaxed = true),
|
||||
createTransferTransactionUseCase = mockk(relaxed = true),
|
||||
getFeeUseCase = mockk(relaxed = true),
|
||||
getFeeForGaslessUseCase = mockk(relaxed = true),
|
||||
getFeeForTokenUseCase = mockk(relaxed = true),
|
||||
getAccountCurrencyStatusUseCase = mockk(relaxed = true),
|
||||
isAccountsModeEnabledUseCase = mockk(relaxed = true),
|
||||
sendAmountUpdateTrigger = mockk(relaxed = true),
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue