Updated on 2026-08-14
This commit is contained in:
commit
e638377a5e
55 changed files with 1841 additions and 1451 deletions
|
|
@ -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,49 +581,29 @@ 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
|
||||
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(
|
||||
textReference = getPrimaryButtonText(confirmUM, isHoldToConfirm),
|
||||
iconRes = walletInterationIcon(params.userWallet),
|
||||
isIconVisible = isReadyToSend && !isHoldToConfirm,
|
||||
isHapticClick = isReadyToSend,
|
||||
isHoldToConfirm = isHoldToConfirm,
|
||||
isEnabled = confirmUM.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
when (confirmUM) {
|
||||
is ConfirmUM.Content -> if (confirmUM.isTransactionInProcess) {
|
||||
return@NavigationButton
|
||||
} else {
|
||||
onSendClick()
|
||||
}
|
||||
else -> return@NavigationButton
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
fun primaryButtonUM(confirmUM: ConfirmUM): NavigationButton {
|
||||
val isContent = confirmUM is ConfirmUM.Content
|
||||
val isReadyToSend = isContent && !confirmUM.isTransactionInProcess
|
||||
val isHoldToConfirm = params.userWallet.isHotWallet && isContent
|
||||
|
||||
return NavigationButton(
|
||||
textReference = getPrimaryButtonText(confirmUM, isHoldToConfirm),
|
||||
iconRes = walletInterationIcon(params.userWallet),
|
||||
isIconVisible = isReadyToSend && !isHoldToConfirm,
|
||||
isHapticClick = isReadyToSend,
|
||||
isHoldToConfirm = isHoldToConfirm,
|
||||
isEnabled = confirmUM.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
when (confirmUM) {
|
||||
is ConfirmUM.Content -> if (confirmUM.isTransactionInProcess) {
|
||||
return@NavigationButton
|
||||
} else {
|
||||
onSendClick()
|
||||
}
|
||||
else -> return@NavigationButton
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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,14 +107,8 @@ internal class SendWithSwapModel @Inject constructor(
|
|||
uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onResult(route: SendWithSwapRoute, sendWithSwapUM: SendWithSwapUM) {
|
||||
if (currentRoute.value == route) {
|
||||
uiState.value = sendWithSwapUM
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationResult(navigationUM: NavigationUM) {
|
||||
uiState.update { it.copy(navigationUM = navigationUM) }
|
||||
override fun onResult(sendWithSwapUM: SendWithSwapUM) {
|
||||
uiState.value = sendWithSwapUM
|
||||
}
|
||||
|
||||
override fun onSeparatorClick(lastAmount: String, isEnterInFiatSelected: Boolean) {
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue