Updated on 2026-08-14
This commit is contained in:
commit
b603ef3a22
61 changed files with 2076 additions and 1452 deletions
|
|
@ -1,7 +1,8 @@
|
|||
package com.tangem.common.ui.navigationButtons
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
|
||||
interface NavigationModelCallback {
|
||||
fun onNavigationResult(navigationUM: NavigationUM)
|
||||
fun onBackClick()
|
||||
fun onNextClick()
|
||||
fun onBackClick(currentRoute: Route)
|
||||
fun onNextClick(currentRoute: Route)
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.features.send.api.subcomponents.amount
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
|
||||
/**
|
||||
* Common route for amount
|
||||
*/
|
||||
interface AmountRoute {
|
||||
interface AmountRoute : Route {
|
||||
val isEditMode: Boolean
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ package com.tangem.features.send.api.subcomponents.amount
|
|||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.domain.wallets.models.errors.GetUserWalletError
|
||||
|
||||
interface SendAmountComponent : ComposableContentComponent {
|
||||
interface SendAmountComponent : ComposableModularContentComponent {
|
||||
|
||||
fun updateState(amountUM: AmountState)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
sealed class SendAmountComponentParams {
|
||||
|
|
@ -39,7 +38,7 @@ sealed class SendAmountComponentParams {
|
|||
override val accountFlow: StateFlow<Account?>,
|
||||
override val isAccountModeFlow: StateFlow<Boolean>,
|
||||
val callback: SendAmountComponent.ModelCallback,
|
||||
val currentRoute: Flow<AmountRoute>,
|
||||
val route: AmountRoute,
|
||||
) : SendAmountComponentParams()
|
||||
|
||||
data class AmountBlockParams(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.features.send.api.subcomponents.destination
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
|
||||
/**
|
||||
* Common route for destination
|
||||
*/
|
||||
interface DestinationRoute {
|
||||
interface DestinationRoute : Route {
|
||||
val isEditMode: Boolean
|
||||
}
|
||||
|
|
@ -2,10 +2,10 @@ package com.tangem.features.send.api.subcomponents.destination
|
|||
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
|
||||
interface SendDestinationComponent : ComposableContentComponent {
|
||||
interface SendDestinationComponent : ComposableModularContentComponent {
|
||||
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ 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.entity.DestinationUM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
sealed class SendDestinationComponentParams {
|
||||
|
|
@ -26,7 +25,7 @@ sealed class SendDestinationComponentParams {
|
|||
override val userWalletId: UserWalletId,
|
||||
val title: TextReference,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val currentRoute: Flow<DestinationRoute>,
|
||||
val route: DestinationRoute,
|
||||
val callback: SendDestinationComponent.ModelCallback,
|
||||
override val isAllowSelfSend: Boolean = false,
|
||||
) : SendDestinationComponentParams()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal sealed class CommonSendRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data object Confirm : CommonSendRoute() {
|
||||
override val isEditMode: Boolean = true
|
||||
override val isEditMode: Boolean = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -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,7 +83,6 @@ 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
|
||||
|
|
@ -110,11 +99,11 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
type = model.consumeEntryType(),
|
||||
),
|
||||
)
|
||||
if (model.currentRoute.value.isEditMode) {
|
||||
if (childStack.value.active.configuration.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
is DefaultSendAmountComponent -> {
|
||||
is SendAmountComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AmountScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
|
|
@ -134,42 +123,37 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
}
|
||||
model.currentRoute.emit(stack.active.configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
|
|
@ -552,58 +561,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,32 +27,7 @@ internal class SendConfirmSuccessModel @Inject constructor(
|
|||
private val _uiState = params.sendUMFlow
|
||||
val uiState = _uiState
|
||||
|
||||
init {
|
||||
configConfirmSuccessNavigation()
|
||||
}
|
||||
|
||||
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 = {
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
|
|
@ -73,39 +36,14 @@ internal class SendConfirmSuccessModel @Inject constructor(
|
|||
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?,
|
||||
)
|
||||
|
|
@ -4,7 +4,6 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
|
|
@ -16,14 +15,15 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
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.extensions.resourceReference
|
||||
import com.tangem.features.send.api.NFTSendComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
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.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.sendnft.confirm.NFTSendConfirmComponent
|
||||
|
|
@ -32,16 +32,14 @@ import com.tangem.features.send.sendnft.success.NFTSendSuccessComponent
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultNFTSendComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: NFTSendComponent.Params,
|
||||
private val destinationComponentFactory: SendDestinationComponent.Factory,
|
||||
private val nftSendConfirmComponentFactory: NFTSendConfirmComponent.Factory,
|
||||
private val nftSendSuccessComponentFactory: NFTSendSuccessComponent.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendDestinationComponentFactory: SendDestinationComponent.Factory,
|
||||
) : NFTSendComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<CommonSendRoute>()
|
||||
|
|
@ -77,7 +75,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
lifecycle = lifecycle,
|
||||
mode = ObserveLifecycleMode.CREATE_DESTROY,
|
||||
) { stack ->
|
||||
componentScope.launch {
|
||||
when (val activeComponent = stack.active.instance) {
|
||||
is NFTSendConfirmComponent -> {
|
||||
val fromCurrency = model.cryptoCurrency
|
||||
|
|
@ -93,7 +90,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
toDerivationIndex = null,
|
||||
),
|
||||
)
|
||||
if (model.currentRouteFlow.value.isEditMode) {
|
||||
// Push current state into a reused Confirm on (re)entry. Confirm.isEditMode is `true`
|
||||
if (stack.active.configuration.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -107,37 +105,38 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
}
|
||||
model.currentRouteFlow.emit(stack.active.configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val stackState by childStack.subscribeAsState()
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(onBack = model::onBackClick)
|
||||
SendContent(
|
||||
navigationUM = state.navigationUM,
|
||||
confirmUM = state.confirmUM,
|
||||
stackState = stackState,
|
||||
)
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
SendModularContent(stackState = stackState)
|
||||
}
|
||||
|
||||
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext)
|
||||
private fun createChild(
|
||||
route: CommonSendRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent = when (route) {
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(route, factoryContext)
|
||||
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
|
||||
CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext)
|
||||
else -> getStubComponent()
|
||||
// Empty is the bootstrap placeholder until the currency status resolves; NFT has no Amount step.
|
||||
CommonSendRoute.Empty,
|
||||
is CommonSendRoute.Amount,
|
||||
-> ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext): SendDestinationComponent =
|
||||
sendDestinationComponentFactory.create(
|
||||
private fun getDestinationComponent(
|
||||
route: DestinationRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent = destinationComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
title = resourceReference(R.string.nft_send),
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
|
|
@ -148,7 +147,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create(
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
return nftSendConfirmComponentFactory.create(
|
||||
appComponentContext = factoryContext,
|
||||
params = NFTSendConfirmComponent.Params(
|
||||
state = model.uiState.value,
|
||||
|
|
@ -160,7 +160,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
|
||||
appCurrency = model.appCurrency,
|
||||
callback = model,
|
||||
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.Confirm>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
onLoadFee = model::loadFee,
|
||||
analyticsSendSource = analyticsSendSource,
|
||||
|
|
@ -169,13 +168,14 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
val txUrl = (model.uiState.value.confirmUM as? ConfirmUM.Success)?.txUrl
|
||||
|
||||
if (txUrl == null) {
|
||||
model.showAlertError()
|
||||
return getStubComponent()
|
||||
return ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
|
||||
return nftSendSuccessComponentFactory.create(
|
||||
|
|
@ -189,7 +189,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
nftAsset = params.nftAsset,
|
||||
nftCollectionName = params.nftCollectionName,
|
||||
callback = model,
|
||||
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.ConfirmSuccess>(),
|
||||
txUrl = txUrl,
|
||||
account = model.account,
|
||||
isAccountsMode = model.isAccountsMode,
|
||||
|
|
@ -197,8 +196,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getStubComponent() = ComposableContentComponent { }
|
||||
|
||||
private fun onChildBack() {
|
||||
val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty
|
||||
val isEmptyStack = childStack.value.backStack.isEmpty()
|
||||
|
|
|
|||
|
|
@ -1,17 +1,28 @@
|
|||
package com.tangem.features.send.sendnft.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.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
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
|
||||
|
|
@ -19,21 +30,20 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.nft.component.NFTDetailsBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
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.feeSelector.params.FeeSelectorParams.FeeStateConfiguration
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.confirm.model.NFTSendConfirmModel
|
||||
import com.tangem.features.send.sendnft.confirm.ui.NFTSendConfirmContent
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import com.tangem.features.send.subcomponents.notifications.DefaultSendNotificationsComponent
|
||||
import com.tangem.features.send.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -45,7 +55,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
@Assisted params: Params,
|
||||
nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
|
||||
feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: NFTSendConfirmModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -129,6 +139,20 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
model.updateState(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.nft_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()
|
||||
|
|
@ -144,6 +168,29 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@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(),
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val state: NFTSendUM,
|
||||
val analyticsCategoryName: String,
|
||||
|
|
@ -157,7 +204,6 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
val account: Account.CryptoPortfolio?,
|
||||
val isAccountsMode: Boolean,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<CommonSendRoute.Confirm>,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
val onSendTransaction: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import arrow.core.getOrElse
|
|||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.common.routing.AppRouter
|
||||
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
|
||||
|
|
@ -32,28 +31,28 @@ import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase
|
|||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.analytics.NFTSendAnalyticHelper
|
||||
import com.tangem.features.send.sendnft.confirm.NFTSendConfirmComponent
|
||||
import com.tangem.features.send.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer
|
||||
import com.tangem.features.send.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer
|
||||
import com.tangem.features.send.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -122,7 +121,6 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
private var sendIdleTimer: Long = 0L
|
||||
|
||||
init {
|
||||
configConfirmNavigation()
|
||||
subscribeOnNotificationsUpdateTrigger()
|
||||
subscribeOnCheckFeeResultUpdates()
|
||||
subscribeOnTapHelpUpdates()
|
||||
|
|
@ -371,54 +369,18 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun configConfirmNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (state, _) ->
|
||||
val confirmUM = state.confirmUM
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = CommonSendRoute.Confirm.javaClass.simpleName,
|
||||
title = resourceReference(R.string.nft_send),
|
||||
subtitle = null,
|
||||
backIconRes = when (confirmUM) {
|
||||
is ConfirmUM.Success -> R.drawable.ic_close_24
|
||||
else -> R.drawable.ic_back_24
|
||||
},
|
||||
backIconClick = {
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = confirmUM.isPrimaryButtonEnabled,
|
||||
isValid = uiState.value.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,
|
||||
)
|
||||
).takeUnless { (confirmUM as? ConfirmUM.Success)?.txUrl.isNullOrBlank() },
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun primaryButtonUM(): NavigationButton {
|
||||
fun primaryButtonUM(): NavigationButton {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
val isContent = confirmUM is ConfirmUM.Content
|
||||
val isReadyToSend = isContent && !confirmUM.isSending
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import arrow.core.Either
|
|||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
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.datasource.local.nft.converter.NFTSdkAssetConverter
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
|
|
@ -36,9 +36,9 @@ import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
|||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
|
||||
import com.tangem.features.send.api.NFTSendComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
|
||||
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.CommonSendRoute
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
|
|
@ -82,8 +82,6 @@ internal class NFTSendModel @Inject constructor(
|
|||
|
||||
val initialRoute = CommonSendRoute.Empty
|
||||
|
||||
val currentRouteFlow = MutableStateFlow<CommonSendRoute>(initialRoute)
|
||||
|
||||
private val userWalletId = params.userWalletId
|
||||
private val nftAsset = params.nftAsset
|
||||
|
||||
|
|
@ -107,10 +105,6 @@ internal class NFTSendModel @Inject constructor(
|
|||
initAppCurrency()
|
||||
}
|
||||
|
||||
override fun onNavigationResult(navigationUM: NavigationUM) {
|
||||
uiState.update { it.copy(navigationUM = navigationUM) }
|
||||
}
|
||||
|
||||
override fun onResult(nftSendUM: NFTSendUM) {
|
||||
uiState.value = nftSendUM
|
||||
}
|
||||
|
|
@ -119,8 +113,8 @@ internal class NFTSendModel @Inject constructor(
|
|||
uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onBackClick() {
|
||||
if (currentRouteFlow.value == CommonSendRoute.ConfirmSuccess) {
|
||||
override fun onBackClick(currentRoute: Route) {
|
||||
if (currentRoute == CommonSendRoute.ConfirmSuccess) {
|
||||
modelScope.launch {
|
||||
nftSendSuccessTrigger.triggerSuccessNFTSend()
|
||||
}
|
||||
|
|
@ -128,14 +122,14 @@ internal class NFTSendModel @Inject constructor(
|
|||
router.pop()
|
||||
}
|
||||
|
||||
override fun onNextClick() {
|
||||
if (currentRouteFlow.value.isEditMode) {
|
||||
onBackClick()
|
||||
override fun onNextClick(currentRoute: Route) {
|
||||
if ((currentRoute as? CommonSendRoute)?.isEditMode == true) {
|
||||
onBackClick(currentRoute)
|
||||
} else {
|
||||
when (currentRouteFlow.value) {
|
||||
when (currentRoute) {
|
||||
is CommonSendRoute.Destination -> router.push(CommonSendRoute.Confirm)
|
||||
CommonSendRoute.Confirm -> router.replaceAll(CommonSendRoute.ConfirmSuccess)
|
||||
else -> onBackClick()
|
||||
else -> onBackClick(currentRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -241,6 +235,5 @@ internal class NFTSendModel @Inject constructor(
|
|||
destinationUM = DestinationUM.Empty(),
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,15 +1,27 @@
|
|||
package com.tangem.features.send.sendnft.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.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.navigationButtons.DoneButtons
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
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.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -19,24 +31,22 @@ 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.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.success.model.NFTSendSuccessModel
|
||||
import com.tangem.features.send.sendnft.success.ui.NFTSendSuccessContent
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class NFTSendSuccessComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Params,
|
||||
@Assisted private val params: Params,
|
||||
nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
|
||||
sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: NFTSendSuccessModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -69,6 +79,19 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
|
|||
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) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -80,11 +103,40 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@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 nftSendUMFlow: StateFlow<NFTSendUM>,
|
||||
val analyticsCategoryName: String,
|
||||
val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
val currentRoute: Flow<CommonSendRoute>,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val userWallet: UserWallet,
|
||||
val nftAsset: NFTAsset,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,17 @@
|
|||
package com.tangem.features.send.sendnft.success.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
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.ui.state.SendUM
|
||||
import com.tangem.features.send.sendnft.success.NFTSendSuccessComponent
|
||||
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
|
||||
|
|
@ -37,32 +27,7 @@ internal class NFTSendSuccessModel @Inject constructor(
|
|||
|
||||
val uiState = params.nftSendUMFlow
|
||||
|
||||
init {
|
||||
configConfirmSuccessNavigation()
|
||||
}
|
||||
|
||||
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 = {
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
|
|
@ -71,37 +36,14 @@ internal class NFTSendSuccessModel @Inject constructor(
|
|||
isValid = true,
|
||||
),
|
||||
)
|
||||
params.callback.onBackClick()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
iconRes = null,
|
||||
isEnabled = true,
|
||||
isHapticClick = false,
|
||||
onClick = params.callback::onBackClick,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}.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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
|
|
@ -23,8 +22,8 @@ import com.tangem.features.nft.component.NFTDetailsBlockComponent
|
|||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.common.ui.FeeBlockSuccess
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
|
|
@ -52,10 +51,9 @@ internal fun NFTSendSuccessContent(
|
|||
label = "Animate success content",
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
SuccessContent(
|
||||
|
|
@ -69,15 +67,6 @@ internal fun NFTSendSuccessContent(
|
|||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
NavigationButtonsBlockV2(
|
||||
navigationUM = nftSendUM.navigationUM,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
package com.tangem.features.send.sendnft.ui.state
|
||||
|
||||
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
|
||||
|
||||
internal data class NFTSendUM(
|
||||
val destinationUM: DestinationUM,
|
||||
val feeSelectorUM: FeeSelectorUM,
|
||||
val confirmUM: ConfirmUM,
|
||||
val navigationUM: NavigationUM,
|
||||
)
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
package com.tangem.features.send.subcomponents.amount
|
||||
|
||||
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.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.subcomponents.amount.model.SendAmountModel
|
||||
import com.tangem.features.send.subcomponents.amount.ui.SendAmountContent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -24,6 +33,23 @@ internal class DefaultSendAmountComponent @AssistedInject constructor(
|
|||
|
||||
override fun updateState(amountUM: AmountState) = model.updateState(amountUM)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.send_amount_label),
|
||||
onBackClick = {
|
||||
params.callback.onBackClick(params.route)
|
||||
},
|
||||
backIconRes = if (params.route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -37,6 +63,30 @@ internal class DefaultSendAmountComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
PrimaryButton(
|
||||
text = if (params.route.isEditMode) {
|
||||
stringResourceSafe(R.string.common_continue)
|
||||
} else {
|
||||
stringResourceSafe(R.string.common_next)
|
||||
},
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
model.onAmountNext()
|
||||
params.callback.onNextClick(params.route)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendAmountComponent.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import com.tangem.common.ui.amountScreen.models.AmountParameters
|
|||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
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
|
||||
|
|
@ -29,14 +27,12 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
|||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.entity.isFromMainScreenQr
|
||||
import com.tangem.features.send.api.subcomponents.amount.AmountRoute
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents.SelectedCurrencyType
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -84,7 +80,6 @@ internal class SendAmountModel @Inject constructor(
|
|||
private var maxAmountBoundary: EnterAmountBoundary by Delegates.notNull()
|
||||
|
||||
init {
|
||||
configAmountNavigation()
|
||||
initAppCurrency()
|
||||
subscribeOnCryptoCurrencyStatusFlow()
|
||||
subscribeOnAmountReduceByTriggerUpdates()
|
||||
|
|
@ -104,6 +99,7 @@ internal class SendAmountModel @Inject constructor(
|
|||
},
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
setSendWithSwapAvailability()
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
|
|
@ -274,9 +270,7 @@ internal class SendAmountModel @Inject constructor(
|
|||
override fun onConvertToAnotherToken() {
|
||||
val amountParams = params as? SendAmountComponentParams.AmountParams ?: return
|
||||
modelScope.launch {
|
||||
var isEditMode = false
|
||||
amountParams.currentRoute.collect { route -> isEditMode = route.isEditMode }
|
||||
if (isEditMode) {
|
||||
if (amountParams.route.isEditMode) {
|
||||
sendAmountAlertFactory.showResetSendingAlert {
|
||||
params.callback.resetSendNavigation()
|
||||
confirmConvertToToken()
|
||||
|
|
@ -363,44 +357,6 @@ internal class SendAmountModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun configAmountNavigation() {
|
||||
val params = params as? SendAmountComponentParams.AmountParams ?: return
|
||||
combine(
|
||||
flow = uiState,
|
||||
// Filter on the public AmountRoute interface (not the internal CommonSendRoute.Amount) so an
|
||||
// external host (e.g. staking) that supplies its own AmountRoute is not silently dropped here.
|
||||
flow2 = params.currentRoute.filterIsInstance<AmountRoute>(),
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (state, route) ->
|
||||
setSendWithSwapAvailability()
|
||||
params.callback.onNavigationResult(
|
||||
NavigationUM.Content(
|
||||
source = CommonSendRoute.Amount::class.java.simpleName,
|
||||
title = resourceReference(R.string.send_amount_label),
|
||||
subtitle = null,
|
||||
backIconRes = if (route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backIconClick = params.callback::onBackClick,
|
||||
primaryButton = NavigationButton(
|
||||
textReference = if (route.isEditMode) {
|
||||
resourceReference(R.string.common_continue)
|
||||
} else {
|
||||
resourceReference(R.string.common_next)
|
||||
},
|
||||
isEnabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
onAmountNext()
|
||||
params.callback.onNextClick()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun setSendWithSwapAvailability() {
|
||||
// Allowed only in multicurrency wallets
|
||||
val isMultiCurrency = userWallet?.isMultiCurrency == true
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
package com.tangem.features.send.subcomponents.destination
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
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.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
|
|
@ -14,9 +19,15 @@ import com.tangem.core.decompose.model.getOrCreateModel
|
|||
import com.tangem.features.addressbook.AddressBookContactsBlockComponent
|
||||
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||
import com.tangem.features.addressbook.AddressSelectorComponent
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
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.impl.R
|
||||
import com.tangem.features.send.subcomponents.destination.model.SendDestinationModel
|
||||
import com.tangem.features.send.subcomponents.destination.ui.SendDestinationContent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -68,6 +79,22 @@ internal class DefaultSendDestinationComponent @AssistedInject constructor(
|
|||
|
||||
override fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
BackHandler(onBack = model::onBackClick)
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = params.title.resolveReference(),
|
||||
onBackClick = model::onBackClick,
|
||||
backIconRes = if (params.route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -83,6 +110,30 @@ internal class DefaultSendDestinationComponent @AssistedInject constructor(
|
|||
selector.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
PrimaryButton(
|
||||
text = if (params.route.isEditMode) {
|
||||
stringResourceSafe(R.string.common_continue)
|
||||
} else {
|
||||
stringResourceSafe(R.string.common_next)
|
||||
},
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
model.saveResult()
|
||||
params.callback.onNextClick(params.route)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendDestinationComponent.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -8,14 +8,11 @@ import com.arkivanov.decompose.router.slot.activate
|
|||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.entity.AddressBookOpenMode
|
||||
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.decompose.navigation.Router
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAddressUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
|
|
@ -49,8 +46,6 @@ import com.tangem.features.send.api.entity.PredefinedValues
|
|||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.subcomponents.destination.SendDestinationAlertFactory
|
||||
import com.tangem.features.send.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.subcomponents.destination.analytics.SendDestinationAnalyticEvents
|
||||
|
|
@ -142,7 +137,6 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private val backupProblematicWalletCache = AtomicReference<Pair<String, UserWalletId?>?>(null)
|
||||
|
||||
init {
|
||||
configDestinationNavigation()
|
||||
subscribeOnQRScannerResult()
|
||||
initialState()
|
||||
resetContactOnEdit()
|
||||
|
|
@ -153,15 +147,12 @@ internal class SendDestinationModel @Inject constructor(
|
|||
|
||||
private fun resetContactOnEdit() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
params.currentRoute
|
||||
.filter { it.isEditMode }
|
||||
.onEach {
|
||||
val content = uiState.value as? DestinationUM.Content ?: return@onEach
|
||||
if (params.route.isEditMode) {
|
||||
val content = uiState.value as? DestinationUM.Content ?: return
|
||||
if (content.addressTextField.contactName != null) {
|
||||
_uiState.update(SendDestinationContactTransformer(contact = null))
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
fun onContactClick(contact: MatchedContact) {
|
||||
|
|
@ -259,7 +250,23 @@ internal class SendDestinationModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun saveResult() {
|
||||
fun onBackClick() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
if (!params.route.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Address,
|
||||
isFromSummary = false,
|
||||
isValid = uiState.value.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
saveResult()
|
||||
}
|
||||
params.callback.onBackClick(params.route)
|
||||
}
|
||||
|
||||
fun saveResult() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
params.callback.onDestinationResult(uiState.value)
|
||||
}
|
||||
|
|
@ -490,60 +497,10 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private fun autoNextFromRecipient(type: EnterAddressSource, isValidAddress: Boolean, isValidMemo: Boolean) {
|
||||
if (type.isAutoNext && isValidAddress && isValidMemo) {
|
||||
saveResult()
|
||||
(params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick()
|
||||
(params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick(params.route)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun configDestinationNavigation() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (state, route) ->
|
||||
params.callback.onNavigationResult(
|
||||
NavigationUM.Content(
|
||||
source = CommonSendRoute.Destination::class.java.simpleName,
|
||||
title = params.title,
|
||||
subtitle = null,
|
||||
backIconRes = if (route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backIconClick = {
|
||||
if (!route.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Address,
|
||||
isFromSummary = false,
|
||||
isValid = state.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
saveResult()
|
||||
}
|
||||
params.callback.onBackClick()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = if (route.isEditMode) {
|
||||
resourceReference(R.string.common_continue)
|
||||
} else {
|
||||
resourceReference(R.string.common_next)
|
||||
},
|
||||
isEnabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
saveResult()
|
||||
params.callback.onNextClick()
|
||||
},
|
||||
),
|
||||
secondaryPairButtonsUM = null,
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val RECENT_TX_SIZE = 100
|
||||
const val RECENT_LOAD_DELAY = 500L
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
package com.tangem.features.send.send
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
|
|
@ -21,55 +26,45 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.quotes.IsHighNetworkFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.SendFeatureToggles
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.send.analytics.SendAnalyticHelper
|
||||
import com.tangem.features.send.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.send.confirm.model.SendConfirmModel
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import arrow.core.right
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
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.model.SendModel
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
|
@ -89,7 +84,8 @@ internal abstract class SendModelTestBase {
|
|||
protected val router: Router = mockk(relaxed = true)
|
||||
protected val appRouter: AppRouter = mockk(relaxed = true)
|
||||
protected val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true)
|
||||
protected val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase = mockk(relaxed = true)
|
||||
protected val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk(relaxed = true)
|
||||
protected val listenToQrScanningUseCase: ListenToQrScanningUseCase = mockk(relaxed = true)
|
||||
protected val parseQrCodeUseCase: ParseQrCodeUseCase = mockk(relaxed = true)
|
||||
|
|
@ -260,7 +256,6 @@ internal abstract class SendModelTestBase {
|
|||
destinationUM = DestinationUM.Empty(),
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
confirmData = null,
|
||||
),
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus = testCryptoCurrencyStatus,
|
||||
|
|
@ -278,7 +273,6 @@ internal abstract class SendModelTestBase {
|
|||
isAccountModeFlow = kotlinx.coroutines.flow.MutableStateFlow(false),
|
||||
appCurrency = AppCurrency.Default,
|
||||
callback = mockk(relaxed = true),
|
||||
currentRoute = kotlinx.coroutines.flow.flowOf(),
|
||||
isBalanceHidingFlow = kotlinx.coroutines.flow.MutableStateFlow(false),
|
||||
predefinedValues = PredefinedValues.Empty,
|
||||
onLoadFee = { Either.Right(mockk(relaxed = true)) },
|
||||
|
|
|
|||
|
|
@ -7,36 +7,25 @@ import com.tangem.blockchain.common.Amount
|
|||
import com.tangem.blockchain.common.transaction.Fee
|
||||
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.decompose.model.MutableParamsContainer
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
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.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.send.SendModelTestBase
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import io.mockk.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -73,10 +62,30 @@ internal class SendConfirmModelTest : SendModelTestBase() {
|
|||
|
||||
// Assert
|
||||
if (model.expectedSendInitiated) {
|
||||
coVerify(exactly = 1) { createTransferTransactionUseCase(any(), any<Fee>(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 1) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { feeSelectorCheckReloadTrigger.triggerCheckUpdate() }
|
||||
} else {
|
||||
coVerify(exactly = 0) { createTransferTransactionUseCase(any(), any<Fee>(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) { feeSelectorCheckReloadTrigger.triggerCheckUpdate() }
|
||||
}
|
||||
}
|
||||
|
|
@ -108,9 +117,29 @@ internal class SendConfirmModelTest : SendModelTestBase() {
|
|||
|
||||
// Assert
|
||||
if (model.expectedSendInitiated) {
|
||||
coVerify(exactly = 1) { createTransferTransactionUseCase(any(), any<Fee>(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 1) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
coVerify(exactly = 0) { createTransferTransactionUseCase(any(), any<Fee>(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +190,8 @@ internal class SendConfirmModelTest : SendModelTestBase() {
|
|||
fun `GIVEN successful send WHEN verifyAndSend THEN notify onSendTransaction`() = runTest {
|
||||
// Arrange
|
||||
val onSendTransaction = mockk<() -> Unit>(relaxed = true)
|
||||
val callback = mockk<com.tangem.features.send.send.confirm.SendConfirmComponent.ModelCallback>(relaxed = true)
|
||||
val callback =
|
||||
mockk<com.tangem.features.send.send.confirm.SendConfirmComponent.ModelCallback>(relaxed = true)
|
||||
val resultFlow = MutableSharedFlow<Boolean>(extraBufferCapacity = 1)
|
||||
every { feeSelectorCheckReloadListener.checkReloadResultFlow } returns resultFlow
|
||||
coEvery { sendTransactionUseCase(any(), any(), any()) } returns "txHash".right()
|
||||
|
|
@ -256,7 +286,6 @@ internal class SendConfirmModelTest : SendModelTestBase() {
|
|||
destinationUM = destination,
|
||||
feeSelectorUM = feeSelector,
|
||||
confirmUM = mockk<ConfirmUM.Content>(relaxed = true),
|
||||
navigationUM = NavigationUM.Empty,
|
||||
confirmData = null,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
|||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.features.send.send.SendModelTestBase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
|
|
@ -33,7 +32,6 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
fun `GIVEN amount route AND predefined main screen QR WHEN onNextClick THEN push Confirm`() = runTest {
|
||||
// Arrange
|
||||
val model = createSendModel(this)
|
||||
model.currentRoute.value = CommonSendRoute.Amount(isEditMode = false)
|
||||
model.predefinedValues = PredefinedValues.Content.QrCode(
|
||||
amount = "1.0",
|
||||
address = "addr123",
|
||||
|
|
@ -42,7 +40,7 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
)
|
||||
|
||||
// Act
|
||||
model.onNextClick()
|
||||
model.onNextClick(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.Confirm, any()) }
|
||||
|
|
@ -52,11 +50,10 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
fun `GIVEN amount route AND NOT main screen QR WHEN onNextClick THEN push Destination`() = runTest {
|
||||
// Arrange
|
||||
val model = createSendModel(this)
|
||||
model.currentRoute.value = CommonSendRoute.Amount(isEditMode = false)
|
||||
model.predefinedValues = PredefinedValues.Empty
|
||||
|
||||
// Act
|
||||
model.onNextClick()
|
||||
model.onNextClick(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.Destination(isEditMode = false), any()) }
|
||||
|
|
@ -66,10 +63,9 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
fun `GIVEN destination route WHEN onNextClick THEN push Confirm`() = runTest {
|
||||
// Arrange
|
||||
val model = createSendModel(this)
|
||||
model.currentRoute.value = CommonSendRoute.Destination(isEditMode = false)
|
||||
|
||||
// Act
|
||||
model.onNextClick()
|
||||
model.onNextClick(CommonSendRoute.Destination(isEditMode = false))
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.Confirm, any()) }
|
||||
|
|
@ -79,10 +75,9 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
fun `GIVEN route in edit mode WHEN onNextClick THEN pop without push`() = runTest {
|
||||
// Arrange
|
||||
val model = createSendModel(this)
|
||||
model.currentRoute.value = CommonSendRoute.Amount(isEditMode = true)
|
||||
|
||||
// Act
|
||||
model.onNextClick()
|
||||
model.onNextClick(CommonSendRoute.Amount(isEditMode = true))
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { router.pop(any()) }
|
||||
|
|
@ -90,19 +85,18 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirm route WHEN onNextClick THEN pop (Confirm isEditMode is true so push branch is dead)`() =
|
||||
fun `GIVEN confirm route WHEN onNextClick THEN push ConfirmSuccess`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
// CommonSendRoute.Confirm.isEditMode == true, so onNextClick short-circuits to onBackClick().
|
||||
// CommonSendRoute.Confirm.isEditMode == false, so onNextClick pushes ConfirmSuccess.
|
||||
val model = createSendModel(this)
|
||||
model.currentRoute.value = CommonSendRoute.Confirm
|
||||
|
||||
// Act
|
||||
model.onNextClick()
|
||||
model.onNextClick(CommonSendRoute.Confirm)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { router.pop(any()) }
|
||||
verify(exactly = 0) { router.push(CommonSendRoute.ConfirmSuccess, any()) }
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.ConfirmSuccess, any()) }
|
||||
verify(exactly = 0) { router.pop(any()) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,10 +204,9 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
fun `GIVEN amount route non-edit WHEN onBackClick THEN send analytics and pop`() = runTest {
|
||||
// Arrange
|
||||
val model = createSendModel(this)
|
||||
model.currentRoute.value = CommonSendRoute.Amount(isEditMode = false)
|
||||
|
||||
// Act
|
||||
model.onBackClick()
|
||||
model.onBackClick(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { analyticsEventHandler.send(any<CommonSendAnalyticEvents.CloseButtonClicked>()) }
|
||||
|
|
@ -224,10 +217,9 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
fun `GIVEN destination route edit WHEN onBackClick THEN pop without analytics`() = runTest {
|
||||
// Arrange
|
||||
val model = createSendModel(this)
|
||||
model.currentRoute.value = CommonSendRoute.Destination(isEditMode = true)
|
||||
|
||||
// Act
|
||||
model.onBackClick()
|
||||
model.onBackClick(CommonSendRoute.Destination(isEditMode = true))
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any<CommonSendAnalyticEvents.CloseButtonClicked>()) }
|
||||
|
|
@ -251,7 +243,6 @@ internal class SendModelTest : SendModelTestBase() {
|
|||
assertThat(state.feeSelectorUM).isEqualTo(FeeSelectorUMRedesigned.Loading)
|
||||
assertThat(state.confirmUM).isEqualTo(ConfirmUM.Empty)
|
||||
assertThat(state.confirmData).isNull()
|
||||
assertThat(state.navigationUM).isEqualTo(NavigationUM.Empty)
|
||||
verify(exactly = 1) { router.popTo(CommonSendRoute.Amount(isEditMode = false), any()) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import arrow.core.right
|
|||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -32,50 +30,36 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
|||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
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.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
import com.tangem.features.send.sendnft.analytics.NFTSendAnalyticHelper
|
||||
import com.tangem.features.send.sendnft.confirm.NFTSendConfirmComponent
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class NFTSendConfirmModelTest {
|
||||
|
|
@ -166,10 +150,30 @@ internal class NFTSendConfirmModelTest {
|
|||
|
||||
// Assert
|
||||
if (model.expectedSendInitiated) {
|
||||
coVerify(exactly = 1) { createNFTTransferTransactionUseCase(any(), any(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 1) {
|
||||
createNFTTransferTransactionUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { feeSelectorCheckReloadTrigger.triggerCheckUpdate() }
|
||||
} else {
|
||||
coVerify(exactly = 0) { createNFTTransferTransactionUseCase(any(), any(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) {
|
||||
createNFTTransferTransactionUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) { feeSelectorCheckReloadTrigger.triggerCheckUpdate() }
|
||||
}
|
||||
}
|
||||
|
|
@ -303,7 +307,6 @@ internal class NFTSendConfirmModelTest {
|
|||
account = null,
|
||||
isAccountsMode = false,
|
||||
callback = mockk(relaxed = true),
|
||||
currentRoute = flowOf(),
|
||||
isBalanceHidingFlow = kotlinx.coroutines.flow.MutableStateFlow(false),
|
||||
onLoadFee = { mockk<com.tangem.blockchain.common.transaction.TransactionFee>(relaxed = true).right() },
|
||||
onSendTransaction = {},
|
||||
|
|
@ -328,7 +331,6 @@ internal class NFTSendConfirmModelTest {
|
|||
destinationUM = destination,
|
||||
feeSelectorUM = feeSelector,
|
||||
confirmUM = mockk<ConfirmUM.Content>(relaxed = true),
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
package com.tangem.features.send.sendnft.model
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.wallets.models.errors.GetUserWalletError
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
|
||||
import com.tangem.features.send.api.NFTSendComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Guards the NFT-send navigation refactor: [NFTSendModel.onBackClick] / [NFTSendModel.onNextClick] now
|
||||
* receive the active [com.tangem.core.decompose.navigation.Route] as a parameter (instead of reading an
|
||||
* internal `currentRouteFlow`). Routing is synchronous, so those assertions run without advancing; the
|
||||
* one async effect — firing `NFTSendSuccessTrigger.triggerSuccessNFTSend()` when leaving the success
|
||||
* screen — is verified after `advanceUntilIdle()`.
|
||||
*
|
||||
* `init {}` collectors are kept harmless under `advanceUntilIdle()` by stubbing the wallet lookup to the
|
||||
* not-found branch (so no further currency/account fetching is reached) and the app-currency lookup.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class NFTSendModelNavigationTest {
|
||||
|
||||
private val router: Router = mockk(relaxed = true)
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true)
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk(relaxed = true)
|
||||
private val nftSendSuccessTrigger: NFTSendSuccessTrigger = mockk(relaxed = true)
|
||||
|
||||
private var model: NFTSendModel? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
model?.onDestroy()
|
||||
model = null
|
||||
}
|
||||
|
||||
@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) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN edit-mode destination WHEN onNextClick THEN pops instead of advancing`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(CommonSendRoute.Destination(isEditMode = true))
|
||||
|
||||
verify(exactly = 1) { router.pop() }
|
||||
verify(exactly = 0) { router.push(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN destination step WHEN onBackClick THEN pops without firing success trigger`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onBackClick(CommonSendRoute.Destination(isEditMode = false))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 1) { router.pop() }
|
||||
coVerify(exactly = 0) { nftSendSuccessTrigger.triggerSuccessNFTSend() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirm-success route WHEN onBackClick THEN fires success trigger and pops`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onBackClick(CommonSendRoute.ConfirmSuccess)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { nftSendSuccessTrigger.triggerSuccessNFTSend() }
|
||||
verify(exactly = 1) { router.pop() }
|
||||
}
|
||||
|
||||
private fun createModel(testScope: TestScope): NFTSendModel {
|
||||
every { getUserWalletUseCase(any()) } returns GetUserWalletError.UserWalletNotFound.left()
|
||||
coEvery { getSelectedAppCurrencyUseCase.invokeSync() } returns AppCurrency.Default.right()
|
||||
|
||||
val params = NFTSendComponent.Params(
|
||||
userWalletId = UserWalletId(stringValue = "0123456789"),
|
||||
nftAsset = mockk<NFTAsset>(relaxed = true),
|
||||
nftCollectionName = "Test Collection",
|
||||
)
|
||||
return NFTSendModel(
|
||||
paramsContainer = MutableParamsContainer(value = params),
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
router = router,
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
multiWalletCryptoCurrenciesSupplier = mockk(relaxed = true),
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase = mockk(relaxed = true),
|
||||
createNFTTransferTransactionUseCase = mockk(relaxed = true),
|
||||
getFeeUseCase = mockk(relaxed = true),
|
||||
saveBlockchainErrorUseCase = mockk(relaxed = true),
|
||||
getWalletMetaInfoUseCase = mockk(relaxed = true),
|
||||
sendFeedbackEmailUseCase = mockk(relaxed = true),
|
||||
alertFactory = mockk(relaxed = true),
|
||||
nftSendSuccessTrigger = nftSendSuccessTrigger,
|
||||
isAccountsModeEnabledUseCase = mockk(relaxed = true),
|
||||
getAccountCurrencyStatusUseCase = mockk(relaxed = true),
|
||||
analyticsEventHandler = mockk(relaxed = true),
|
||||
).also { model = it }
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -102,31 +102,41 @@ internal class NFTSendModelTest {
|
|||
// Arrange
|
||||
val sut = buildModel()
|
||||
advanceUntilIdle()
|
||||
sut.currentRouteFlow.value = model.route
|
||||
|
||||
// Act
|
||||
sut.onNextClick()
|
||||
sut.onNextClick(model.route)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
if (model.expectPushConfirm) {
|
||||
when (model.expectedAction) {
|
||||
NextClickAction.PushConfirm -> {
|
||||
verify(exactly = 1) { router.push(CommonSendRoute.Confirm, any()) }
|
||||
verify(exactly = 0) { router.pop(any()) }
|
||||
} else {
|
||||
verify(exactly = 0) { router.replaceAll(*anyVararg(), onComplete = any()) }
|
||||
}
|
||||
NextClickAction.ReplaceAllConfirmSuccess -> {
|
||||
// Confirm route → replaceAll(ConfirmSuccess)
|
||||
verify(exactly = 1) { router.replaceAll(CommonSendRoute.ConfirmSuccess, onComplete = any()) }
|
||||
verify(exactly = 0) { router.pop(any()) }
|
||||
verify(exactly = 0) { router.push(any(), any()) }
|
||||
}
|
||||
NextClickAction.Pop -> {
|
||||
verify(exactly = 1) { router.pop(any()) }
|
||||
verify(exactly = 0) { router.push(any(), any()) }
|
||||
// Confirm.isEditMode == true, so the `Confirm -> replaceAll(ConfirmSuccess)` branch is unreachable
|
||||
verify(exactly = 0) { router.replaceAll(CommonSendRoute.ConfirmSuccess, onComplete = any()) }
|
||||
verify(exactly = 0) { router.replaceAll(*anyVararg(), onComplete = any()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
NextClickModel(route = CommonSendRoute.Destination(isEditMode = false), expectPushConfirm = true),
|
||||
NextClickModel(route = CommonSendRoute.Destination(isEditMode = true), expectPushConfirm = false),
|
||||
NextClickModel(route = CommonSendRoute.Confirm, expectPushConfirm = false),
|
||||
NextClickModel(route = CommonSendRoute.Destination(isEditMode = false), expectedAction = NextClickAction.PushConfirm),
|
||||
NextClickModel(route = CommonSendRoute.Destination(isEditMode = true), expectedAction = NextClickAction.Pop),
|
||||
NextClickModel(route = CommonSendRoute.Confirm, expectedAction = NextClickAction.ReplaceAllConfirmSuccess),
|
||||
)
|
||||
}
|
||||
|
||||
enum class NextClickAction { PushConfirm, ReplaceAllConfirmSuccess, Pop }
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OnBackClick {
|
||||
|
|
@ -138,10 +148,9 @@ internal class NFTSendModelTest {
|
|||
// Arrange
|
||||
val sut = buildModel()
|
||||
advanceUntilIdle()
|
||||
sut.currentRouteFlow.value = model.route
|
||||
|
||||
// Act
|
||||
sut.onBackClick()
|
||||
sut.onBackClick(model.route)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
|
|
@ -221,7 +230,7 @@ internal class NFTSendModelTest {
|
|||
)
|
||||
}
|
||||
|
||||
data class NextClickModel(val route: CommonSendRoute, val expectPushConfirm: Boolean)
|
||||
data class NextClickModel(val route: CommonSendRoute, val expectedAction: NextClickAction)
|
||||
|
||||
data class BackClickModel(val route: CommonSendRoute, val expectedTriggerCalls: Int)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.features.send.subcomponents.amount.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
|
|
@ -13,43 +16,28 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
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.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -79,7 +67,14 @@ internal class SendAmountModelTest {
|
|||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
// PER_CLASS parameterized nested classes reuse one instance — reset verified mocks between rows.
|
||||
clearMocks(callback, sendAmountAlertFactory, analyticsEventHandler, answers = false, recordedCalls = true, childMocks = false)
|
||||
clearMocks(
|
||||
callback,
|
||||
sendAmountAlertFactory,
|
||||
analyticsEventHandler,
|
||||
answers = false,
|
||||
recordedCalls = true,
|
||||
childMocks = false
|
||||
)
|
||||
every { getUserWalletUseCase.invokeFlow(testUserWalletId) } returns flowOf(coldWallet().right())
|
||||
coEvery { getMinimumTransactionAmountSyncUseCase(any(), any()) } returns BigDecimal.ONE.right()
|
||||
coEvery { getSelectedAppCurrencyUseCase.invokeSync() } returns AppCurrency.Default.right()
|
||||
|
|
@ -107,12 +102,7 @@ internal class SendAmountModelTest {
|
|||
PredefinedValues.Empty
|
||||
}
|
||||
// Start off an Amount route so the navigation combine stays idle until the wallet is loaded.
|
||||
val currentRoute = MutableStateFlow<CommonSendRoute>(CommonSendRoute.Confirm)
|
||||
val sut = buildModel(predefinedValues = predefined, currentRoute = currentRoute)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act — flip to Amount so setSendWithSwapAvailability() re-runs with the loaded wallet
|
||||
currentRoute.value = CommonSendRoute.Amount(isEditMode = false)
|
||||
val sut = buildModel(predefinedValues = predefined, route = CommonSendRoute.Amount(false))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
|
|
@ -138,7 +128,7 @@ internal class SendAmountModelTest {
|
|||
fun `WHEN onConvertToAnotherToken THEN reset-alert in edit mode else convert directly`(model: ConvertModel) =
|
||||
runTest {
|
||||
// Arrange
|
||||
val sut = buildModel(currentRoute = MutableStateFlow(CommonSendRoute.Amount(isEditMode = model.isEditMode)))
|
||||
val sut = buildModel(route = CommonSendRoute.Amount(isEditMode = model.isEditMode))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
|
|
@ -248,7 +238,10 @@ internal class SendAmountModelTest {
|
|||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
AmountNextModel(isFiat = true, expectedType = CommonSendAmountAnalyticEvents.SelectedCurrencyType.AppCurrency),
|
||||
AmountNextModel(
|
||||
isFiat = true,
|
||||
expectedType = CommonSendAmountAnalyticEvents.SelectedCurrencyType.AppCurrency
|
||||
),
|
||||
AmountNextModel(isFiat = false, expectedType = CommonSendAmountAnalyticEvents.SelectedCurrencyType.Token),
|
||||
)
|
||||
}
|
||||
|
|
@ -257,7 +250,7 @@ internal class SendAmountModelTest {
|
|||
|
||||
private fun TestScope.buildModel(
|
||||
predefinedValues: PredefinedValues = PredefinedValues.Empty,
|
||||
currentRoute: MutableStateFlow<CommonSendRoute> = MutableStateFlow(CommonSendRoute.Amount(isEditMode = false)),
|
||||
route: CommonSendRoute.Amount = CommonSendRoute.Amount(isEditMode = false),
|
||||
cryptoCurrencyStatusFlow: MutableStateFlow<CryptoCurrencyStatus> =
|
||||
MutableStateFlow(loadedStatus(cryptoCurrency, balance = BigDecimal.TEN)),
|
||||
state: AmountState = AmountState.Empty,
|
||||
|
|
@ -275,7 +268,7 @@ internal class SendAmountModelTest {
|
|||
accountFlow = MutableStateFlow<Account?>(null),
|
||||
isAccountModeFlow = MutableStateFlow(false),
|
||||
callback = callback,
|
||||
currentRoute = currentRoute.filterIsInstance<AmountRoute>(),
|
||||
route = route,
|
||||
)
|
||||
return SendAmountModel(
|
||||
paramsContainer = MutableParamsContainer(params),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
package com.tangem.features.send.subcomponents.amount.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
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.model.MutableParamsContainer
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
|
|
@ -25,13 +22,11 @@ import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentPara
|
|||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -46,12 +41,14 @@ import org.junit.jupiter.api.Test
|
|||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Guards the route-decoupling fix: `SendAmountModel.configAmountNavigation()` filters its route flow on
|
||||
* the public `AmountRoute` interface, not the `internal CommonSendRoute.Amount`. The test feeds a
|
||||
* foreign `AmountRoute` (which is NOT a `CommonSendRoute.Amount`) and asserts the navigation result is
|
||||
* still produced — before the fix the `combine`'s `filterIsInstance<CommonSendRoute.Amount>()` dropped
|
||||
* it and `onNavigationResult` never fired, leaving an external host (e.g. staking) with a dead Next
|
||||
* button. Also checks the `isEditMode` → back-icon / primary-button mapping is unaffected.
|
||||
* Guards the route-decoupling fix at the model level. The amount step is now hostable by foreign flows
|
||||
* (e.g. staking / send-with-swap) that supply their own [AmountRoute] implementation rather than the
|
||||
* `internal CommonSendRoute.Amount`. The navigation chrome (back icon / Next vs Continue) moved out of
|
||||
* the model into `DefaultSendAmountComponent`, but the model still makes a route-driven decision in
|
||||
* [SendAmountModel.onConvertToAnotherToken]: it reads `params.route.isEditMode` *directly* (previously
|
||||
* it collected the route flow). This test feeds a foreign [AmountRoute] and verifies that edit-mode
|
||||
* branches to the "reset sending" alert while a fresh (non-edit) route converts immediately — both via
|
||||
* the public interface, not `CommonSendRoute`.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SendAmountNavigationTest {
|
||||
|
|
@ -79,10 +76,11 @@ internal class SendAmountNavigationTest {
|
|||
sendAmountUpdateListener,
|
||||
getSelectedAppCurrencyUseCase,
|
||||
getUserWalletUseCase,
|
||||
sendAmountAlertFactory,
|
||||
callback,
|
||||
)
|
||||
// No wallet → the model stays on AmountState.Empty (the heavy AmountStateConverter path is skipped),
|
||||
// which is all the navigation block needs to emit.
|
||||
// which is all these route-driven assertions need.
|
||||
every { getUserWalletUseCase.invokeFlow(any()) } returns emptyFlow()
|
||||
every { sendAmountReduceListener.reduceToTriggerFlow } returns emptyFlow()
|
||||
every { sendAmountReduceListener.reduceByTriggerFlow } returns emptyFlow()
|
||||
|
|
@ -94,43 +92,41 @@ internal class SendAmountNavigationTest {
|
|||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
// Cancel modelScope so the long-lived navigation/status collectors stop between tests.
|
||||
// Cancel modelScope so the long-lived reduce/status collectors stop between tests.
|
||||
model?.onDestroy()
|
||||
model = null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a foreign AmountRoute WHEN model created THEN navigation produced with close icon and next button`() =
|
||||
runTest {
|
||||
// Arrange — a route that is NOT CommonSendRoute.Amount (the impl type the model used to filter on).
|
||||
val navSlot = slot<NavigationUM>()
|
||||
|
||||
// Act
|
||||
createModel(testScope = this, route = TestAmountRoute(isEditMode = false))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert — before the fix this never fired for a non-CommonSendRoute.Amount route.
|
||||
verify(atLeast = 1) { callback.onNavigationResult(capture(navSlot)) }
|
||||
val content = navSlot.captured as NavigationUM.Content
|
||||
assertThat(content.backIconRes).isEqualTo(R.drawable.ic_close_24)
|
||||
assertThat(content.primaryButton.textReference).isEqualTo(resourceReference(R.string.common_next))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a foreign AmountRoute in edit mode WHEN model created THEN navigation has back icon and continue button`() =
|
||||
fun `GIVEN foreign AmountRoute in edit mode WHEN onConvertToAnotherToken THEN reset sending alert shown`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val navSlot = slot<NavigationUM>()
|
||||
|
||||
// Act
|
||||
createModel(testScope = this, route = TestAmountRoute(isEditMode = true))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(atLeast = 1) { callback.onNavigationResult(capture(navSlot)) }
|
||||
val content = navSlot.captured as NavigationUM.Content
|
||||
assertThat(content.backIconRes).isEqualTo(R.drawable.ic_back_24)
|
||||
assertThat(content.primaryButton.textReference).isEqualTo(resourceReference(R.string.common_continue))
|
||||
// Act
|
||||
model?.onConvertToAnotherToken()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert — edit mode must guard the conversion behind the reset-sending confirmation.
|
||||
verify(exactly = 1) { sendAmountAlertFactory.showResetSendingAlert(any()) }
|
||||
verify(exactly = 0) { callback.onConvertToAnotherToken(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN foreign AmountRoute not in edit mode WHEN onConvertToAnotherToken THEN converts without alert`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
createModel(testScope = this, route = TestAmountRoute(isEditMode = false))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
model?.onConvertToAnotherToken()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert — a fresh route converts immediately, with no reset-sending alert.
|
||||
verify(exactly = 0) { sendAmountAlertFactory.showResetSendingAlert(any()) }
|
||||
verify(exactly = 1) { callback.onConvertToAnotherToken(any(), any()) }
|
||||
}
|
||||
|
||||
private fun createModel(testScope: TestScope, route: AmountRoute): SendAmountModel {
|
||||
|
|
@ -147,7 +143,7 @@ internal class SendAmountNavigationTest {
|
|||
accountFlow = MutableStateFlow<Account?>(null),
|
||||
isAccountModeFlow = MutableStateFlow(false),
|
||||
callback = callback,
|
||||
currentRoute = MutableStateFlow(route),
|
||||
route = route,
|
||||
)
|
||||
return SendAmountModel(
|
||||
paramsContainer = MutableParamsContainer(value = params),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.features.send.subcomponents.destination.model
|
|||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -9,6 +11,8 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAddressUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.addressbook.model.*
|
||||
import com.tangem.domain.addressbook.usecase.GetContactsUseCase
|
||||
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.CryptoCurrencyAddress
|
||||
|
|
@ -21,25 +25,16 @@ import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
|||
import com.tangem.domain.transaction.error.AddressValidation
|
||||
import com.tangem.domain.transaction.error.AddressValidationResult
|
||||
import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.domain.addressbook.model.AddressEntry
|
||||
import com.tangem.domain.addressbook.model.AddressEntryId
|
||||
import com.tangem.domain.addressbook.model.Contact
|
||||
import com.tangem.domain.addressbook.model.ContactId
|
||||
import com.tangem.domain.addressbook.model.ContactName
|
||||
import com.tangem.domain.addressbook.usecase.GetContactsUseCase
|
||||
import com.tangem.features.addressbook.MatchedContact
|
||||
import com.tangem.features.addressbook.SelectedContact
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import com.tangem.domain.transaction.usecase.IsSelfSendAvailableUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.addressbook.ContactSelectionListener
|
||||
import com.tangem.features.addressbook.MatchedContact
|
||||
import com.tangem.features.addressbook.SelectedContact
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
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
|
||||
|
|
@ -49,13 +44,14 @@ import com.tangem.features.send.subcomponents.destination.SendDestinationAlertFa
|
|||
import com.tangem.features.send.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.subcomponents.destination.analytics.SendDestinationAnalyticEvents
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -64,7 +60,6 @@ import kotlinx.coroutines.flow.flowOf
|
|||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
@ -127,7 +122,15 @@ internal class SendDestinationModelTest {
|
|||
fun `GIVEN valid non-problematic address WHEN address entered THEN send valid analytics without backup alert`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
coEvery { validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any()) } returns
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any<List<CryptoCurrencyAddress>>(),
|
||||
any()
|
||||
)
|
||||
} returns
|
||||
AddressValidation.Success.Valid.right()
|
||||
val sut = buildModel()
|
||||
advanceUntilIdle()
|
||||
|
|
@ -144,14 +147,22 @@ internal class SendDestinationModelTest {
|
|||
}
|
||||
verify(exactly = 0) { sendDestinationAlertFactory.showRecipientBackupErrorAlert(any()) }
|
||||
// InputField is not an auto-next source → no auto-advance even for a valid address
|
||||
verify(exactly = 0) { callback.onNextClick() }
|
||||
verify(exactly = 0) { callback.onNextClick(CommonSendRoute.Destination(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN valid backup-problematic address WHEN address entered THEN show recipient backup error alert`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
coEvery { validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any()) } returns
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any<List<CryptoCurrencyAddress>>(),
|
||||
any()
|
||||
)
|
||||
} returns
|
||||
AddressValidation.Success.Valid.right()
|
||||
coEvery { getBackupProblematicWalletForAddressUseCase(any()) } returns testUserWalletId
|
||||
val sut = buildModel()
|
||||
|
|
@ -172,7 +183,15 @@ internal class SendDestinationModelTest {
|
|||
@Test
|
||||
fun `GIVEN invalid address WHEN address entered THEN send invalid analytics`() = runTest {
|
||||
// Arrange
|
||||
coEvery { validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any()) } returns
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any<List<CryptoCurrencyAddress>>(),
|
||||
any()
|
||||
)
|
||||
} returns
|
||||
AddressValidation.Error.InvalidAddress.left()
|
||||
val sut = buildModel()
|
||||
advanceUntilIdle()
|
||||
|
|
@ -193,7 +212,15 @@ internal class SendDestinationModelTest {
|
|||
fun `GIVEN memo change with null type WHEN handled THEN no address-entered analytics and no auto-next`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
coEvery { validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any()) } returns
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any<List<CryptoCurrencyAddress>>(),
|
||||
any()
|
||||
)
|
||||
} returns
|
||||
AddressValidation.Success.Valid.right()
|
||||
val sut = buildModel()
|
||||
advanceUntilIdle()
|
||||
|
|
@ -206,7 +233,7 @@ internal class SendDestinationModelTest {
|
|||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(any<SendDestinationAnalyticEvents.AddressEntered>())
|
||||
}
|
||||
verify(exactly = 0) { callback.onNextClick() }
|
||||
verify(exactly = 0) { callback.onNextClick(CommonSendRoute.Destination(false)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +258,7 @@ internal class SendDestinationModelTest {
|
|||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = model.expectedNextClicks) { callback.onNextClick() }
|
||||
verify(exactly = model.expectedNextClicks) { callback.onNextClick(CommonSendRoute.Destination(false)) }
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
|
|
@ -256,7 +283,15 @@ internal class SendDestinationModelTest {
|
|||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any()) }
|
||||
coVerify(exactly = 0) {
|
||||
validateWalletAddressUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any<List<CryptoCurrencyAddress>>(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,20 +321,33 @@ internal class SendDestinationModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a contact is set WHEN route switches to edit mode THEN the contact is reset`() = runTest {
|
||||
// Arrange
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any())
|
||||
} returns AddressValidation.Success.Valid.right()
|
||||
val currentRoute = MutableStateFlow<DestinationRoute>(CommonSendRoute.Destination(isEditMode = false))
|
||||
val sut = buildModel(currentRoute = currentRoute)
|
||||
advanceUntilIdle()
|
||||
sut.applySelectedContact(selectedContact(name = "Dave", address = "0xDave"))
|
||||
advanceUntilIdle()
|
||||
assertThat(content(sut).addressTextField.contactName).isEqualTo("Dave")
|
||||
fun `GIVEN a contact is pre-set in state AND route is edit mode WHEN model initializes THEN the contact is reset`() =
|
||||
runTest {
|
||||
// Arrange — build initial state with a contact name already set and isInitialized = true
|
||||
// so the InitialStateTransformer does NOT overwrite it, leaving resetContactOnEdit() to clear it.
|
||||
val stateWithContact = DestinationUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
isInitialized = true,
|
||||
addressTextField = DestinationTextFieldUM.RecipientAddress(
|
||||
value = "0xDave",
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Text),
|
||||
placeholder = com.tangem.core.ui.extensions.stringReference(""),
|
||||
label = com.tangem.core.ui.extensions.stringReference(""),
|
||||
isValuePasted = false,
|
||||
contactName = "Dave",
|
||||
),
|
||||
memoTextField = null,
|
||||
recent = persistentListOf(),
|
||||
wallets = persistentListOf(),
|
||||
networkName = "Ethereum",
|
||||
isRecentHidden = false,
|
||||
)
|
||||
|
||||
// Act — entering edit mode must clear the bound contact
|
||||
currentRoute.value = CommonSendRoute.Destination(isEditMode = true)
|
||||
// Act — init with isEditMode = true triggers resetContactOnEdit()
|
||||
val sut = buildModel(
|
||||
currentRoute = CommonSendRoute.Destination(isEditMode = true),
|
||||
initialState = stateWithContact,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
|
|
@ -335,9 +383,19 @@ internal class SendDestinationModelTest {
|
|||
|
||||
private fun provideTestModels() = listOf(
|
||||
// saved "0xAddr", entered "0xaddr" → case-insensitive match
|
||||
ContactRecognitionModel(savedName = "Alice", savedAddress = "0xAddr", enteredAddress = "0xaddr", expectedContactName = "Alice"),
|
||||
ContactRecognitionModel(
|
||||
savedName = "Alice",
|
||||
savedAddress = "0xAddr",
|
||||
enteredAddress = "0xaddr",
|
||||
expectedContactName = "Alice"
|
||||
),
|
||||
// entered address not among saved contacts → no recognition
|
||||
ContactRecognitionModel(savedName = "Alice", savedAddress = "0xOther", enteredAddress = "0xAddr", expectedContactName = null),
|
||||
ContactRecognitionModel(
|
||||
savedName = "Alice",
|
||||
savedAddress = "0xOther",
|
||||
enteredAddress = "0xAddr",
|
||||
expectedContactName = null
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -411,29 +469,44 @@ internal class SendDestinationModelTest {
|
|||
|
||||
private fun provideTestModels() = listOf(
|
||||
// not available -> never shown, even for a fresh valid address
|
||||
AddContactModel(isAddContactAvailable = false, savedAddresses = emptyList(), enteredAddress = "0xFresh", expectedShown = false),
|
||||
AddContactModel(
|
||||
isAddContactAvailable = false,
|
||||
savedAddresses = emptyList(),
|
||||
enteredAddress = "0xFresh",
|
||||
expectedShown = false
|
||||
),
|
||||
// available + address not in the book -> shown
|
||||
AddContactModel(isAddContactAvailable = true, savedAddresses = emptyList(), enteredAddress = "0xFresh", expectedShown = true),
|
||||
AddContactModel(
|
||||
isAddContactAvailable = true,
|
||||
savedAddresses = emptyList(),
|
||||
enteredAddress = "0xFresh",
|
||||
expectedShown = true
|
||||
),
|
||||
// available but address already saved -> hidden
|
||||
AddContactModel(isAddContactAvailable = true, savedAddresses = listOf("0xSaved"), enteredAddress = "0xSaved", expectedShown = false),
|
||||
AddContactModel(
|
||||
isAddContactAvailable = true,
|
||||
savedAddresses = listOf("0xSaved"),
|
||||
enteredAddress = "0xSaved",
|
||||
expectedShown = false
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region fixtures
|
||||
|
||||
private fun TestScope.buildModel(
|
||||
currentRoute: MutableStateFlow<DestinationRoute> =
|
||||
MutableStateFlow(CommonSendRoute.Destination(isEditMode = false)),
|
||||
currentRoute: DestinationRoute = CommonSendRoute.Destination(isEditMode = false),
|
||||
initialState: DestinationUM = DestinationUM.Empty(),
|
||||
): SendDestinationModel {
|
||||
val params = SendDestinationComponentParams.DestinationParams(
|
||||
state = DestinationUM.Empty(),
|
||||
state = initialState,
|
||||
analyticsCategoryName = "test_send",
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Send,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = testUserWalletId,
|
||||
title = stringReference("Send to"),
|
||||
isBalanceHidingFlow = MutableStateFlow(false),
|
||||
currentRoute = currentRoute,
|
||||
route = currentRoute,
|
||||
callback = callback,
|
||||
isAllowSelfSend = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -98,4 +98,6 @@ dependencies {
|
|||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.features.swap.v2.impl.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
|
|
@ -14,10 +18,14 @@ import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel
|
||||
import com.tangem.features.swap.v2.impl.amount.ui.SwapAmountContent
|
||||
|
|
@ -28,7 +36,7 @@ import dagger.assisted.AssistedInject
|
|||
internal class SwapAmountComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SwapAmountComponentParams.AmountParams,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SwapAmountModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -49,6 +57,23 @@ internal class SwapAmountComponent @AssistedInject constructor(
|
|||
|
||||
fun updateState(amountUM: SwapAmountUM) = model.updateState(amountUM)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.send_amount_label),
|
||||
onBackClick = {
|
||||
params.callback.onBackClick(params.route)
|
||||
},
|
||||
backIconRes = if (params.route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val amountUM by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -63,6 +88,30 @@ internal class SwapAmountComponent @AssistedInject constructor(
|
|||
rateInfo.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
PrimaryButton(
|
||||
text = if (params.route.isEditMode) {
|
||||
stringResourceSafe(R.string.common_continue)
|
||||
} else {
|
||||
stringResourceSafe(R.string.common_next)
|
||||
},
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
model.onAmountNext()
|
||||
params.callback.onNextClick(params.route)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun rateInfoChild(
|
||||
config: ExpressRateType,
|
||||
componentContext: ComponentContext,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.domain.swap.models.SwapDirection
|
|||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal sealed class SwapAmountComponentParams {
|
||||
|
|
@ -41,7 +40,7 @@ internal sealed class SwapAmountComponentParams {
|
|||
override val isAccountModeFlow: StateFlow<Boolean>,
|
||||
val title: TextReference,
|
||||
val callback: SwapAmountComponent.ModelCallback,
|
||||
val currentRoute: Flow<SendWithSwapRoute>,
|
||||
val route: SendWithSwapRoute,
|
||||
) : SwapAmountComponentParams()
|
||||
|
||||
data class AmountBlockParams(
|
||||
|
|
|
|||
|
|
@ -8,14 +8,11 @@ import com.tangem.common.routing.AppRouter
|
|||
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.common.ui.notifications.NotificationId
|
||||
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.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.datasource.local.swap.SwapBestRateAnimationStore
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
|
|
@ -40,7 +37,6 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
|||
import com.tangem.features.send.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent.SwapChooseProviderConfig
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceListener
|
||||
|
|
@ -54,7 +50,6 @@ import com.tangem.features.swap.v2.impl.amount.model.transformers.*
|
|||
import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent
|
||||
import com.tangem.features.swap.v2.impl.common.SwapAlertFactory
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -125,7 +120,6 @@ internal class SwapAmountModel @Inject constructor(
|
|||
private val amountAnalyticsSender = SwapAmountAnalyticsSender(analyticsEventHandler)
|
||||
|
||||
private var autoUpdateSubscriberJob: Job? = null
|
||||
private var navigationJob: Job? = null
|
||||
|
||||
val uiState: StateFlow<SwapAmountUM>
|
||||
field = MutableStateFlow(params.amountUM)
|
||||
|
|
@ -156,7 +150,6 @@ internal class SwapAmountModel @Inject constructor(
|
|||
} else {
|
||||
QUOTES_UPDATE_DELAY
|
||||
}
|
||||
configAmountNavigation()
|
||||
quoteTaskScheduler.scheduleTask(
|
||||
scope = modelScope,
|
||||
task = loadQuotesTask(initialDelay = initialDelay),
|
||||
|
|
@ -167,7 +160,6 @@ internal class SwapAmountModel @Inject constructor(
|
|||
fun onStop() {
|
||||
quoteTaskScheduler.cancelTask()
|
||||
autoUpdateSubscriberJob?.cancel()
|
||||
navigationJob?.cancel()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -325,7 +317,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
val isShowSendViaSwapNotification = shouldShowNotificationUseCase(
|
||||
NotificationId.SendViaSwapTokenSelectorNotification.key,
|
||||
)
|
||||
val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true
|
||||
val isEditMode = amountParams.route.isEditMode
|
||||
val selectedCurrency = (uiState.value as? SwapAmountUM.Content)?.secondaryCryptoCurrencyStatus?.currency
|
||||
appRouter.push(
|
||||
AppRoute.ChooseManagedTokens(
|
||||
|
|
@ -354,7 +346,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return
|
||||
|
||||
modelScope.launch {
|
||||
if (amountParams.currentRoute.firstOrNull()?.isEditMode == true) {
|
||||
if (amountParams.route.isEditMode) {
|
||||
swapAmountAlertFactory.showCloseSendWithSwapAlert {
|
||||
params.callback.resetSendWithSwapNavigation(resetNavigation = true)
|
||||
confirmSendWithSwapClose()
|
||||
|
|
@ -934,42 +926,6 @@ internal class SwapAmountModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun configAmountNavigation() {
|
||||
val params = params as? SwapAmountComponentParams.AmountParams ?: return
|
||||
navigationJob?.cancel()
|
||||
navigationJob = combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter { (_, route) -> route is SendWithSwapRoute.Amount }.onEach { (state, route) ->
|
||||
params.callback.onNavigationResult(
|
||||
NavigationUM.Content(
|
||||
source = SendWithSwapRoute.Amount::class.java.simpleName,
|
||||
title = resourceReference(R.string.common_amount),
|
||||
subtitle = null,
|
||||
backIconRes = if (route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backIconClick = params.callback::onBackClick,
|
||||
primaryButton = NavigationButton(
|
||||
textReference = if (route.isEditMode) {
|
||||
resourceReference(R.string.common_continue)
|
||||
} else {
|
||||
resourceReference(R.string.common_next)
|
||||
},
|
||||
isEnabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
onAmountNext()
|
||||
params.callback.onNextClick()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DEBOUNCE_AMOUNT_DELAY = 500L
|
||||
const val QUOTES_UPDATE_DELAY = 10000L
|
||||
|
|
|
|||
|
|
@ -4,21 +4,18 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.value.ObserveLifecycleMode
|
||||
import com.arkivanov.decompose.value.subscribe
|
||||
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.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.swap.models.R
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
|
|
@ -41,7 +38,6 @@ import com.tangem.features.swap.v2.impl.sendviaswap.ui.SendWithSwapContent
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
||||
|
|
@ -50,7 +46,6 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
private val sendDestinationComponentFactory: SendDestinationComponent.Factory,
|
||||
private val confirmComponentFactory: SendWithSwapConfirmComponent.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : SendWithSwapComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<SendWithSwapRoute>()
|
||||
|
|
@ -99,7 +94,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
is SendWithSwapConfirmComponent -> {
|
||||
if (model.currentRoute.value.isEditMode) {
|
||||
if (stack.active.configuration.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
// Re-sync destination from parent on Confirm entry, bypassing the edit-mode gate ([REDACTED_TASK_KEY]).
|
||||
|
|
@ -120,7 +115,6 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
model.currentRoute.emit(stack.active.configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -128,35 +122,33 @@ internal class DefaultSendWithSwapComponent @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()
|
||||
},
|
||||
onBack = ::onChildBack,
|
||||
)
|
||||
|
||||
SendWithSwapContent(
|
||||
navigationUM = state.navigationUM,
|
||||
confirmUM = state.confirmUM,
|
||||
stackState = stackState,
|
||||
onLinkClick = urlOpener::openUrl,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createChild(route: SendWithSwapRoute, childContext: AppComponentContext) = when (route) {
|
||||
is SendWithSwapRoute.Amount -> getAmountComponent(factoryContext = childContext)
|
||||
is SendWithSwapRoute.Destination -> getDestinationComponent(factoryContext = childContext)
|
||||
is SendWithSwapRoute.Amount -> getAmountComponent(route, factoryContext = childContext)
|
||||
is SendWithSwapRoute.Destination -> getDestinationComponent(route = route, factoryContext = childContext)
|
||||
is SendWithSwapRoute.Confirm -> getConfirmComponent(factoryContext = childContext)
|
||||
is SendWithSwapRoute.Success -> getSuccessComponent(factoryContext = childContext)
|
||||
}
|
||||
|
||||
private fun getAmountComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getAmountComponent(
|
||||
route: SendWithSwapRoute.Amount,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent {
|
||||
return SwapAmountComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SwapAmountComponentParams.AmountParams(
|
||||
amountUM = model.uiState.value.amountUM,
|
||||
title = resourceReference(R.string.common_send),
|
||||
currentRoute = model.currentRoute,
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow,
|
||||
|
|
@ -172,17 +164,20 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getDestinationComponent(
|
||||
route: DestinationRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent {
|
||||
val amountContentUM = model.uiState.value.amountUM as? SwapAmountUM.Content
|
||||
?: return ComposableContentComponent.EMPTY
|
||||
?: return ComposableModularContentComponent.EMPTY
|
||||
val secondaryCryptoCurrency = amountContentUM.secondaryCryptoCurrencyStatus?.currency
|
||||
?: return ComposableContentComponent.EMPTY
|
||||
?: return ComposableModularContentComponent.EMPTY
|
||||
|
||||
return sendDestinationComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = model.currentRoute.filterIsInstance<DestinationRoute>(),
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
analyticsSendSource = model.analyticsSendSource,
|
||||
|
|
@ -195,12 +190,11 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
return confirmComponentFactory.create(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendWithSwapConfirmComponent.Params(
|
||||
sendWithSwapUM = model.uiState.value,
|
||||
currentRoute = model.currentRoute.filterIsInstance<SendWithSwapRoute.Confirm>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
appCurrency = model.appCurrency,
|
||||
userWallet = model.userWallet,
|
||||
|
|
@ -216,12 +210,11 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
return SendWithSwapSuccessComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendWithSwapSuccessComponent.Params(
|
||||
sendWithSwapUMFlow = model.uiState,
|
||||
currentRoute = model.currentRoute.filterIsInstance<SendWithSwapRoute.Success>(),
|
||||
callback = model,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,37 +1,52 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.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.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.*
|
||||
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
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams.*
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
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.api.subcomponents.feeSelector.params.FeeSelectorParams.*
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
|
||||
import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.SendWithSwapConfirmModel
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.ui.SendWithSwapConfirmContent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -44,7 +59,8 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory,
|
||||
feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
sendNotificationsComponentFactory: SendNotificationsComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
private val urlOpener: UrlOpener,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendWithSwapConfirmModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -177,6 +193,17 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.send_with_swap_confirm_title),
|
||||
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 sendWithSwapUM by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -196,13 +223,120 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
Column {
|
||||
val confirmUMContent = state.confirmUM as? ConfirmUM.Content
|
||||
AnimatedVisibility(
|
||||
visible = confirmUMContent != null,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
val sendFooter = confirmUMContent?.sendingFooter ?: TextReference.EMPTY
|
||||
val legalFooter = getAnnotatedStringForLegals(
|
||||
tosUM = confirmUMContent?.tosUM,
|
||||
sendFooter = sendFooter,
|
||||
onClick = urlOpener::openUrl,
|
||||
)
|
||||
val footerText = remember(sendFooter, legalFooter) {
|
||||
if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) {
|
||||
combinedReference(sendFooter, legalFooter)
|
||||
} else {
|
||||
TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
SendingText(footerText = footerText)
|
||||
}
|
||||
NavigationPrimaryButton(
|
||||
primaryButton = model.primaryButtonUM(state.confirmUM),
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAnnotatedStringForLegals(
|
||||
tosUM: ConfirmUM.Content.TosUM?,
|
||||
sendFooter: TextReference,
|
||||
onClick: (String) -> Unit,
|
||||
): TextReference {
|
||||
if (tosUM == null) return TextReference.EMPTY
|
||||
val tos = tosUM.tosLink
|
||||
val policy = tosUM.policyLink
|
||||
return if (tos != null && policy != null) {
|
||||
val tosTitle = tos.title.resolveReference()
|
||||
val policyTitle = policy.title.resolveReference()
|
||||
val fullString = stringResourceSafe(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle)
|
||||
val tosIndex = fullString.indexOf(tosTitle)
|
||||
val policyIndex = fullString.indexOf(policyTitle)
|
||||
|
||||
annotatedReference {
|
||||
if (!sendFooter.resolveReference().endsWith(StringsSigns.POINT_SIGN)) {
|
||||
append(StringsSigns.POINT_SIGN)
|
||||
}
|
||||
appendSpace()
|
||||
append(fullString.substring(0, tosIndex))
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "TOS_TAG",
|
||||
linkInteractionListener = { onClick(tos.link) },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = fullString.substring(tosIndex, tosIndex + tosTitle.length),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
append(fullString.substring(tosIndex + tosTitle.length, policyIndex))
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "POLICY_TAG",
|
||||
linkInteractionListener = { onClick(policy.link) },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = fullString.substring(policyIndex, policyIndex + policyTitle.length),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val legal = requireNotNull(tos ?: policy) { "tos or policy must not be null" }
|
||||
val legalTitle = legal.title.resolveReference()
|
||||
val fullString = stringResourceSafe(id = R.string.express_legal_one_placeholder, legalTitle)
|
||||
val legalIndex = fullString.indexOf(legalTitle)
|
||||
|
||||
annotatedReference {
|
||||
append(fullString.substring(0, legalIndex))
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "LEGAL_TAG",
|
||||
linkInteractionListener = { onClick(legal.link) },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = fullString.substring(legalIndex, legalIndex + legalTitle.length),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sendWithSwapUM: SendWithSwapUM,
|
||||
val analyticsCategoryName: String,
|
||||
val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
val userWallet: UserWallet,
|
||||
val appCurrency: AppCurrency,
|
||||
val currentRoute: Flow<SendWithSwapRoute>,
|
||||
val swapDirection: SwapDirection,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
|
||||
|
|
@ -218,6 +352,6 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
interface ModelCallback {
|
||||
fun onResult(route: SendWithSwapRoute, sendWithSwapUM: SendWithSwapUM)
|
||||
fun onResult(sendWithSwapUM: SendWithSwapUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
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
|
||||
|
|
@ -38,14 +37,14 @@ import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
|
|||
import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForGaslessTxUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForTokenUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
|
||||
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.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.swap.v2.api.SwapFeatureToggles
|
||||
|
|
@ -176,7 +175,6 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
|
||||
init {
|
||||
updateAmountSubtractAvailability()
|
||||
configConfirmNavigation()
|
||||
initialState()
|
||||
subscribeOnNotificationUpdates()
|
||||
subscribeOnTapHelpUpdates()
|
||||
|
|
@ -383,6 +381,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
swapDataModel = data,
|
||||
),
|
||||
)
|
||||
params.callback.onResult(uiState.value)
|
||||
router.replaceAll(SendWithSwapRoute.Success)
|
||||
},
|
||||
expressOperationType = ExpressOperationType.SEND_WITH_SWAP,
|
||||
|
|
@ -582,28 +581,12 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun configConfirmNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter {
|
||||
it.second is SendWithSwapRoute.Confirm
|
||||
}.onEach { (state, _) ->
|
||||
val confirmUM = state.confirmUM
|
||||
fun primaryButtonUM(confirmUM: ConfirmUM): NavigationButton {
|
||||
val isContent = confirmUM is ConfirmUM.Content
|
||||
val isReadyToSend = isContent && !confirmUM.isTransactionInProcess
|
||||
val isHoldToConfirm = params.userWallet.isHotWallet && isContent
|
||||
params.callback.onResult(
|
||||
route = SendWithSwapRoute.Confirm,
|
||||
sendWithSwapUM = state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = SendWithSwapRoute.Confirm.javaClass.simpleName,
|
||||
title = resourceReference(id = R.string.send_with_swap_confirm_title),
|
||||
subtitle = null,
|
||||
backIconRes = R.drawable.ic_back_24,
|
||||
backIconClick = router::pop,
|
||||
primaryButton = NavigationButton(
|
||||
|
||||
return NavigationButton(
|
||||
textReference = getPrimaryButtonText(confirmUM, isHoldToConfirm),
|
||||
iconRes = walletInterationIcon(params.userWallet),
|
||||
isIconVisible = isReadyToSend && !isHoldToConfirm,
|
||||
|
|
@ -620,11 +603,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
else -> return@NavigationButton
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun getPrimaryButtonText(confirmUM: ConfirmUM, isHoldToConfirm: Boolean): TextReference {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.entity
|
||||
|
||||
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.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
|
||||
|
|
@ -11,5 +10,4 @@ internal data class SendWithSwapUM(
|
|||
val destinationUM: DestinationUM,
|
||||
val feeSelectorUM: FeeSelectorUM,
|
||||
val confirmUM: ConfirmUM,
|
||||
val navigationUM: NavigationUM,
|
||||
)
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
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.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
|
|
@ -20,9 +20,9 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponent
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
|
|
@ -61,7 +61,6 @@ internal class SendWithSwapModel @Inject constructor(
|
|||
val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY
|
||||
val analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.SendWithSwap
|
||||
val initialRoute = SendWithSwapRoute.Amount(false)
|
||||
val currentRoute = MutableStateFlow<SendWithSwapRoute>(initialRoute)
|
||||
|
||||
var userWallet: UserWallet by Delegates.notNull()
|
||||
var appCurrency: AppCurrency = AppCurrency.Default
|
||||
|
|
@ -108,15 +107,9 @@ internal class SendWithSwapModel @Inject constructor(
|
|||
uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onResult(route: SendWithSwapRoute, sendWithSwapUM: SendWithSwapUM) {
|
||||
if (currentRoute.value == route) {
|
||||
override fun onResult(sendWithSwapUM: SendWithSwapUM) {
|
||||
uiState.value = sendWithSwapUM
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationResult(navigationUM: NavigationUM) {
|
||||
uiState.update { it.copy(navigationUM = navigationUM) }
|
||||
}
|
||||
|
||||
override fun onSeparatorClick(lastAmount: String, isEnterInFiatSelected: Boolean) {
|
||||
params.callback?.onCloseSwap(lastAmount, isEnterInFiatSelected)
|
||||
|
|
@ -129,7 +122,6 @@ internal class SendWithSwapModel @Inject constructor(
|
|||
destinationUM = DestinationUM.Empty(),
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
if (resetNavigation) {
|
||||
|
|
@ -137,17 +129,17 @@ internal class SendWithSwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onBackClick() = router.pop()
|
||||
override fun onBackClick(currentRoute: Route) = router.pop()
|
||||
|
||||
override fun onNextClick() {
|
||||
if (currentRoute.value.isEditMode) {
|
||||
onBackClick()
|
||||
override fun onNextClick(currentRoute: Route) {
|
||||
if ((currentRoute as? SendWithSwapRoute)?.isEditMode == true) {
|
||||
onBackClick(currentRoute)
|
||||
} else {
|
||||
when (currentRoute.value) {
|
||||
when (currentRoute) {
|
||||
is SendWithSwapRoute.Amount -> router.push(SendWithSwapRoute.Destination(isEditMode = false))
|
||||
is SendWithSwapRoute.Destination -> router.push(SendWithSwapRoute.Confirm)
|
||||
SendWithSwapRoute.Confirm -> router.push(SendWithSwapRoute.Success)
|
||||
SendWithSwapRoute.Success -> onBackClick()
|
||||
SendWithSwapRoute.Success -> onBackClick(currentRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -171,7 +163,7 @@ internal class SendWithSwapModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
},
|
||||
popBack = ::onBackClick,
|
||||
popBack = router::pop,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -189,7 +181,6 @@ internal class SendWithSwapModel @Inject constructor(
|
|||
destinationUM = DestinationUM.Empty(),
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,91 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.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.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.navigationButtons.DoneButtons
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
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.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.success.model.SendWithSwapSuccessModel
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.success.ui.SendWithSwapSuccessContent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class SendWithSwapSuccessComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
private val params: Params,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendWithSwapSuccessModel = getOrCreateModel(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
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) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
SendWithSwapSuccessContent(sendWithSwapUM = state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
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,
|
||||
)).takeUnless { (state.confirmUM as? ConfirmUM.Success)?.txUrl.isNullOrBlank() },
|
||||
)
|
||||
PrimaryButton(
|
||||
text = stringResourceSafe(R.string.common_close),
|
||||
onClick = router::pop,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sendWithSwapUMFlow: StateFlow<SendWithSwapUM>,
|
||||
val analyticsCategoryName: String,
|
||||
val currentRoute: Flow<SendWithSwapRoute.Success>,
|
||||
val callback: NavigationModelCallback,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,18 +1,11 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.success.model
|
||||
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
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.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -24,7 +17,6 @@ internal class SendWithSwapSuccessModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
private val appRouter: AppRouter,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -34,47 +26,14 @@ internal class SendWithSwapSuccessModel @Inject constructor(
|
|||
|
||||
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success
|
||||
|
||||
init {
|
||||
configConfirmSuccessNavigation()
|
||||
}
|
||||
|
||||
private fun configConfirmSuccessNavigation() {
|
||||
params.callback.onNavigationResult(
|
||||
NavigationUM.Content(
|
||||
source = SendWithSwapRoute.Success.javaClass.simpleName,
|
||||
title = TextReference.EMPTY,
|
||||
subtitle = null,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backIconClick = 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,
|
||||
)).takeUnless { confirmUM?.txUrl.isNullOrBlank() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onExploreClick() {
|
||||
fun onExploreClick() {
|
||||
if (confirmUM == null) return
|
||||
|
||||
// analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName))
|
||||
urlOpener.openUrl(confirmUM.txUrl)
|
||||
}
|
||||
|
||||
private fun onShareClick() {
|
||||
fun onShareClick() {
|
||||
if (confirmUM == null) return
|
||||
|
||||
// analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName))
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ import com.tangem.common.ui.account.AccountTitle
|
|||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
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.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
|
|
@ -45,27 +42,24 @@ import com.tangem.domain.express.models.ExpressProviderType
|
|||
import com.tangem.domain.swap.models.SwapDataModel
|
||||
import com.tangem.domain.swap.models.SwapDataTransactionModel
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) {
|
||||
if (sendWithSwapUM.navigationUM !is NavigationUM.Content) return
|
||||
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -82,14 +76,6 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) {
|
|||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
NavigationButtonsBlockV2(
|
||||
navigationUM = sendWithSwapUM.navigationUM,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,31 +444,6 @@ private fun SendWithSwapSuccessContent_Preview() {
|
|||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
),
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = SendWithSwapRoute.Success.javaClass.simpleName,
|
||||
title = TextReference.EMPTY,
|
||||
subtitle = null,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backIconClick = {},
|
||||
additionalIconRes = null,
|
||||
additionalIconClick = null,
|
||||
primaryButton = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
secondaryPairButtonsUM = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,25 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.withLink
|
||||
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.plus
|
||||
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.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
||||
@Composable
|
||||
internal fun SendWithSwapContent(
|
||||
navigationUM: NavigationUM,
|
||||
confirmUM: ConfirmUM,
|
||||
stackState: ChildStack<SendWithSwapRoute, ComposableContentComponent>,
|
||||
onLinkClick: (String) -> Unit,
|
||||
) {
|
||||
val navigationUMContent = navigationUM as? NavigationUM.Content ?: return
|
||||
|
||||
internal fun SendWithSwapContent(stackState: ChildStack<SendWithSwapRoute, ComposableModularContentComponent>) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
|
|
@ -48,12 +28,7 @@ internal fun SendWithSwapContent(
|
|||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
text = navigationUMContent.title.resolveReference(),
|
||||
onBackClick = navigationUMContent.backIconClick,
|
||||
iconRes = navigationUMContent.additionalIconRes,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
stackState.active.instance.Title()
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation { child ->
|
||||
|
|
@ -76,125 +51,6 @@ internal fun SendWithSwapContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (stackState.active.configuration != SendWithSwapRoute.Success) {
|
||||
SendWithSwapFooter(
|
||||
confirmUM = confirmUM,
|
||||
stackState = stackState,
|
||||
primaryButton = navigationUMContent.primaryButton,
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendWithSwapFooter(
|
||||
confirmUM: ConfirmUM,
|
||||
stackState: ChildStack<SendWithSwapRoute, ComposableContentComponent>,
|
||||
primaryButton: NavigationButton,
|
||||
onLinkClick: (String) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = stackState.active.configuration == SendWithSwapRoute.Confirm,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
val confirmContentUM = confirmUM as? ConfirmUM.Content
|
||||
val sendFooter = confirmContentUM?.sendingFooter ?: TextReference.EMPTY
|
||||
val legalFooter = getAnnotatedStringForLegals(
|
||||
tosUM = confirmContentUM?.tosUM,
|
||||
sendFooter = sendFooter,
|
||||
onClick = onLinkClick,
|
||||
)
|
||||
val footerText = remember(sendFooter, legalFooter) {
|
||||
if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) {
|
||||
combinedReference(sendFooter, legalFooter)
|
||||
} else {
|
||||
TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
SendingText(footerText = footerText)
|
||||
}
|
||||
NavigationPrimaryButton(
|
||||
primaryButton = primaryButton,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAnnotatedStringForLegals(
|
||||
tosUM: ConfirmUM.Content.TosUM?,
|
||||
sendFooter: TextReference,
|
||||
onClick: (String) -> Unit,
|
||||
): TextReference {
|
||||
if (tosUM == null) return TextReference.EMPTY
|
||||
val tos = tosUM.tosLink
|
||||
val policy = tosUM.policyLink
|
||||
return if (tos != null && policy != null) {
|
||||
val tosTitle = tos.title.resolveReference()
|
||||
val policyTitle = policy.title.resolveReference()
|
||||
val fullString = stringResourceSafe(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle)
|
||||
val tosIndex = fullString.indexOf(tosTitle)
|
||||
val policyIndex = fullString.indexOf(policyTitle)
|
||||
|
||||
annotatedReference {
|
||||
if (!sendFooter.resolveReference().endsWith(StringsSigns.POINT_SIGN)) {
|
||||
append(StringsSigns.POINT_SIGN)
|
||||
}
|
||||
appendSpace()
|
||||
append(fullString.substring(0, tosIndex))
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "TOS_TAG",
|
||||
linkInteractionListener = { onClick(tos.link) },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = fullString.substring(tosIndex, tosIndex + tosTitle.length),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
append(fullString.substring(tosIndex + tosTitle.length, policyIndex))
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "POLICY_TAG",
|
||||
linkInteractionListener = { onClick(policy.link) },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = fullString.substring(policyIndex, policyIndex + policyTitle.length),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val legal = requireNotNull(tos ?: policy) { "tos or policy must not be null" }
|
||||
val legalTitle = legal.title.resolveReference()
|
||||
val fullString = stringResourceSafe(id = R.string.express_legal_one_placeholder, legalTitle)
|
||||
val legalIndex = fullString.indexOf(legalTitle)
|
||||
|
||||
annotatedReference {
|
||||
append(fullString.substring(0, legalIndex))
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "LEGAL_TAG",
|
||||
linkInteractionListener = { onClick(legal.link) },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = fullString.substring(legalIndex, legalIndex + legalTitle.length),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
stackState.active.instance.Footer()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.model
|
||||
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
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.wallets.models.errors.GetUserWalletError
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.entry.SendEntryRoute
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
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.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Guards the send-with-swap navigation refactor: [SendWithSwapModel.onBackClick] /
|
||||
* [SendWithSwapModel.onNextClick] now receive the active [com.tangem.core.decompose.navigation.Route]
|
||||
* as a parameter instead of reading an internal `currentRoute` StateFlow. Routing is a pure,
|
||||
* synchronous decision over [SendWithSwapRoute], so each method is asserted before any advance; the
|
||||
* model is then destroyed inside the test body so its `init {}` collectors (`initAppCurrency`,
|
||||
* `subscribeOnBalanceHidden`) are cancelled — never run — before `runTest`'s terminal advance. The
|
||||
* synchronous `initUserWallet()` is steered to the not-found branch so it never touches the router
|
||||
* during construction.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SendWithSwapModelNavigationTest {
|
||||
|
||||
private val router: Router = mockk(relaxed = true)
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true)
|
||||
private val cryptoCurrency = MockCryptoCurrencyFactory().createCoin(Blockchain.Ethereum)
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount step WHEN onNextClick THEN pushes destination`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(SendWithSwapRoute.Amount(isEditMode = false))
|
||||
|
||||
verify(exactly = 1) { router.push(SendWithSwapRoute.Destination(isEditMode = false)) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN destination step WHEN onNextClick THEN pushes confirm`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(SendWithSwapRoute.Destination(isEditMode = false))
|
||||
|
||||
verify(exactly = 1) { router.push(SendWithSwapRoute.Confirm) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirm step WHEN onNextClick THEN pushes success`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(SendWithSwapRoute.Confirm)
|
||||
|
||||
verify(exactly = 1) { router.push(SendWithSwapRoute.Success) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN success step WHEN onNextClick THEN pops`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(SendWithSwapRoute.Success)
|
||||
|
||||
verify(exactly = 1) { router.pop() }
|
||||
verify(exactly = 0) { router.push(any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN edit-mode amount WHEN onNextClick THEN pops instead of advancing`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onNextClick(SendWithSwapRoute.Amount(isEditMode = true))
|
||||
|
||||
verify(exactly = 1) { router.pop() }
|
||||
verify(exactly = 0) { router.push(any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN any route WHEN onBackClick THEN pops`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onBackClick(SendWithSwapRoute.Destination(isEditMode = false))
|
||||
|
||||
verify(exactly = 1) { router.pop() }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
private fun createModel(testScope: TestScope): SendWithSwapModel {
|
||||
// Synchronous initUserWallet() runs at construction → keep it on the not-found branch (no router calls).
|
||||
every { getUserWalletUseCase(any()) } returns GetUserWalletError.UserWalletNotFound.left()
|
||||
|
||||
val params = SendWithSwapComponent.Params(
|
||||
userWalletId = UserWalletId(stringValue = "0123456789"),
|
||||
currency = cryptoCurrency,
|
||||
callback = null,
|
||||
currentRoute = MutableStateFlow(SendEntryRoute.SendWithSwap),
|
||||
)
|
||||
return SendWithSwapModel(
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
router = router,
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase = mockk(relaxed = true),
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
getSelectedAppCurrencyUseCase = mockk(relaxed = true),
|
||||
getBalanceHidingSettingsUseCase = mockk(relaxed = true),
|
||||
getAccountCurrencyStatusUseCase = mockk(relaxed = true),
|
||||
isAccountsModeEnabledUseCase = mockk(relaxed = true),
|
||||
swapAlertFactory = mockk(relaxed = true),
|
||||
paramsContainer = MutableParamsContainer(value = params),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -406,6 +406,24 @@ internal data class TangemMessageBannerStory(
|
|||
}
|
||||
}
|
||||
|
||||
internal data class TextStyleStory(
|
||||
val style: Style,
|
||||
val textScale: Float,
|
||||
val onStyleChange: (Style) -> Unit,
|
||||
val onTextScaleChange: (Float) -> Unit,
|
||||
) : DsStoryBookPage {
|
||||
|
||||
/** DS3 typography3 text styles available in the preview. Labels match the Figma naming. */
|
||||
enum class Style(val label: String) {
|
||||
Display("Display"),
|
||||
HeadM("Head.M"),
|
||||
HeadS("Head.S"),
|
||||
Body("Body"),
|
||||
SubH("Sub.H"),
|
||||
Caption("Caption"),
|
||||
}
|
||||
}
|
||||
|
||||
internal data class TangemBadgeV2Story(
|
||||
val variant: TangemBadge.Variant,
|
||||
val status: TangemBadge.Status,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner.ta
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.tangemSearchStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.textstyle.textStyleStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.tangemTopNavigationStoryFactory
|
||||
|
||||
private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
|
||||
|
|
@ -43,6 +44,7 @@ private fun buildDsStories() = listOf(
|
|||
DsStoryItem(title = "🧭 TangemTopNavigation", factory = tangemTopNavigationStoryFactory),
|
||||
DsStoryItem(title = "💫 TangemGlowRing", factory = tangemGlowRingStoryFactory),
|
||||
DsStoryItem(title = "📢 TangemMessageBanner", factory = tangemMessageBannerStoryFactory),
|
||||
DsStoryItem(title = "🔤 Text styles", factory = textStyleStoryFactory),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.feature.tester.presentation.storybook.page.ds.textstyle
|
||||
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TextStyleStory
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||
|
||||
internal fun StateUpdater<TextStyleStory>.build(): TextStyleStory {
|
||||
return TextStyleStory(
|
||||
style = TextStyleStory.Style.HeadM,
|
||||
textScale = 1f,
|
||||
onStyleChange = { style ->
|
||||
updateStory { it.copy(style = style) }
|
||||
},
|
||||
onTextScaleChange = { scale ->
|
||||
updateStory { it.copy(textScale = scale) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal val textStyleStoryFactory
|
||||
get() = storyPageFactory(StateUpdater<TextStyleStory>::build)
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.feature.tester.presentation.storybook.page.ds.textstyle
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TextStyleStory
|
||||
|
||||
@Composable
|
||||
internal fun TextStyleStory(state: TextStyleStory, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors2.surface.level1),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
ComponentPreview(style = state.style, textScale = state.textScale)
|
||||
StyleSelector(selected = state.style, onSelect = state.onStyleChange)
|
||||
TextScaleSlider(value = state.textScale, onChange = state.onTextScaleChange)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentPreview(style: TextStyleStory.Style, textScale: Float) {
|
||||
val textStyle = style.toTextStyle()
|
||||
val baseDensity = LocalDensity.current
|
||||
val scaledDensity = remember(baseDensity, textScale) {
|
||||
Density(density = baseDensity.density, fontScale = textScale)
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(320.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.padding(24.dp),
|
||||
) {
|
||||
CompositionLocalProvider(LocalDensity provides scaledDensity) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
SampleText(text = "multiline\nstring", style = textStyle)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
SampleText(text = "one string", style = textStyle)
|
||||
SampleText(text = "separate string", style = textStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SampleText(text: String, style: TextStyle) {
|
||||
Text(
|
||||
text = text,
|
||||
style = style,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StyleSelector(selected: TextStyleStory.Style, onSelect: (TextStyleStory.Style) -> Unit) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Text style",
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
val shape = RoundedCornerShape(50)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.secondary,
|
||||
shape = shape,
|
||||
)
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
TextStyleStory.Style.entries.forEach { style ->
|
||||
StyleChip(
|
||||
label = style.label,
|
||||
selected = style == selected,
|
||||
onClick = { onSelect(style) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextScaleSlider(value: Float, onChange: (Float) -> Unit) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Text scale: ${"%.2f".format(value)}x",
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Slider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = value,
|
||||
onValueChange = onChange,
|
||||
valueRange = 0.5f..2f,
|
||||
steps = 14,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = TangemTheme.colors.text.accent,
|
||||
activeTrackColor = TangemTheme.colors.text.accent,
|
||||
activeTickColor = TangemTheme.colors2.surface.level3,
|
||||
inactiveTrackColor = TangemTheme.colors2.surface.level3,
|
||||
inactiveTickColor = TangemTheme.colors.text.accent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StyleChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val chipShape = RoundedCornerShape(50)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.clip(chipShape)
|
||||
.background(
|
||||
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun TextStyleStory.Style.toTextStyle(): TextStyle = when (this) {
|
||||
TextStyleStory.Style.Display -> TangemTheme.typography3.display.medium
|
||||
TextStyleStory.Style.HeadM -> TangemTheme.typography3.heading.medium
|
||||
TextStyleStory.Style.HeadS -> TangemTheme.typography3.heading.small
|
||||
TextStyleStory.Style.Body -> TangemTheme.typography3.body.medium
|
||||
TextStyleStory.Style.SubH -> TangemTheme.typography3.subheading.medium
|
||||
TextStyleStory.Style.Caption -> TangemTheme.typography3.caption.medium
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner.Ta
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.TangemSearchStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.textstyle.TextStyleStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.TangemTopNavigationStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.message.TangemMessageStory
|
||||
|
|
@ -105,6 +106,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
|
|||
is TangemFadeStory -> TangemFadeStory(state = storyState)
|
||||
is TangemTopNavigationStory -> TangemTopNavigationStory(state = storyState)
|
||||
is TangemMessageBannerStory -> TangemMessageBannerStory(state = storyState)
|
||||
is TextStyleStory -> TextStyleStory(state = storyState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +67,6 @@ internal class WalletTokensListUMConverterTest {
|
|||
isAccountsModeEnabled = false,
|
||||
expandedAccounts = emptySet(),
|
||||
stakingAvailabilityMap = emptyMap(),
|
||||
isAddAndManageTokensEnabled = true,
|
||||
shouldShowMainPromo = false,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue