diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 4366b157be..2c45500ae0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -11,10 +11,13 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor @@ -47,6 +50,7 @@ fun SearchBar( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } val interactionSource = remember { MutableInteractionSource() } BasicTextField( @@ -60,6 +64,7 @@ fun SearchBar( state.onActiveChange(false) } } + .focusRequester(focusRequester) .testTag(SelectCountryBottomSheetTestTags.SEARCH_BAR), enabled = enabled, value = state.query, @@ -92,6 +97,10 @@ fun SearchBar( ) }, ) + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } } @Suppress("LongParameterList") diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt index d17cd2b862..c5d28df18c 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt @@ -13,11 +13,17 @@ interface ChooseManagedTokensComponent : ComposableContentComponent { val selectedCurrency: CryptoCurrency?, val source: Source, val showSendViaSwapNotification: Boolean, + val callback: ModelCallback? = null, ) enum class Source { SendViaSwap, } + interface ModelCallback { + fun onResult() + fun onBack() + } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt index 8e71b560bf..99206f1591 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -70,7 +70,7 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( cryptoCurrency = cryptoCurrency, shouldResetNavigation = params.selectedCurrency != null, ) - router.pop() + params.callback?.onResult() ?: router.pop() } }, ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index fe98829fd5..53b4245820 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -89,13 +89,13 @@ internal class ChooseManagedTokensModel @Inject constructor( return ChooseManagedTokenUM( notificationUM = getNotification(), readContent = ManageTokensUM.ReadContent( - popBack = router::pop, + popBack = { params.callback?.onBack() ?: router.pop() }, isInitialBatchLoading = true, isNextBatchLoading = false, items = getLoadingItems(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_choose_token), - onBackButtonClick = router::pop, + onBackButtonClick = { params.callback?.onBack() ?: router.pop() }, ), search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index ef4d10fe2d..9d3830f696 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.features.txhistory.api) implementation(projects.features.nft.api) implementation(projects.features.swapV2.api) + implementation(projects.features.manageTokens.api) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt index 4d66bc37aa..0985a678fb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt @@ -1,18 +1,29 @@ package com.tangem.features.send.v2.entrypoint -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +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.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.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.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent -import com.tangem.features.send.v2.entrypoint.model.SendEntryPoint import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel import com.tangem.features.swap.v2.api.SendWithSwapComponent import dagger.assisted.Assisted @@ -24,9 +35,17 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( @Assisted private val params: SendEntryPointComponent.Params, sendWithSwapComponentFactory: SendWithSwapComponent.Factory, sendComponentFactory: SendComponent.Factory, + private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, ) : SendEntryPointComponent, AppComponentContext by appComponentContext { - private val model: SendEntryPointModel = getOrCreateModel(params = params) + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val model: SendEntryPointModel = getOrCreateModel(params = params, router = innerRouter) private val sendWithSwapComponent = sendWithSwapComponentFactory.create( context = child("sendEntrySendWithSwap"), @@ -46,18 +65,76 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( ), ) + private val childStack = childStack( + key = "sendEntryStack", + source = stackNavigation, + serializer = null, + initialConfiguration = SendEntryRoute.Send, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + getChildComponent( + configuration = configuration, + factoryContext = childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + @Composable override fun Content(modifier: Modifier) { - val sendEntryState by model.sendEntryPointState.collectAsStateWithLifecycle() + val childStackValue by childStack.subscribeAsState() - sendComponent.Content(modifier) + Children( + stack = childStackValue, + animation = stackAnimation { child -> + when (child.configuration) { + SendEntryRoute.Send, + SendEntryRoute.SendWithSwap, + -> fade() + is SendEntryRoute.ChooseToken -> slide(orientation = Orientation.Vertical) + fade() + } + }, + ) { child -> + child.instance.Content(modifier.fillMaxSize()) + } + } - AnimatedVisibility( - visible = sendEntryState == SendEntryPoint.SendWithSwap, - enter = fadeIn(), - exit = fadeOut(), - ) { - sendWithSwapComponent.Content(modifier) + private fun getChildComponent( + configuration: SendEntryRoute, + factoryContext: AppComponentContext, + ): ComposableContentComponent = when (configuration) { + is SendEntryRoute.ChooseToken -> getManagedTokensComponent( + componentContext = factoryContext, + showSendViaSwapNotification = configuration.showSendViaSwapNotification, + ) + SendEntryRoute.Send -> sendComponent + SendEntryRoute.SendWithSwap -> sendWithSwapComponent + } + + private fun getManagedTokensComponent( + componentContext: ComponentContext, + showSendViaSwapNotification: Boolean, + ): ChooseManagedTokensComponent { + return chooseManagedTokensComponentFactory.create( + context = childByContext(componentContext), + params = ChooseManagedTokensComponent.Params( + userWalletId = params.userWalletId, + initialCurrency = params.cryptoCurrency, + source = ChooseManagedTokensComponent.Source.SendViaSwap, + selectedCurrency = null, + showSendViaSwapNotification = showSendViaSwapNotification, + callback = model, + ), + ) + } + + private fun onChildBack() { + if (childStack.value.backStack.isEmpty()) { + router.pop() + } else { + stackNavigation.pop() } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt new file mode 100644 index 0000000000..470cfb6215 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2.entrypoint + +import com.tangem.core.decompose.navigation.Route + +internal sealed class SendEntryRoute : Route { + data object Send : SendEntryRoute() + data object SendWithSwap : SendEntryRoute() + data class ChooseToken( + val showSendViaSwapNotification: Boolean, + ) : SendEntryRoute() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 3bdbfcaf8d..984330ba7d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -1,90 +1,74 @@ package com.tangem.features.send.v2.entrypoint.model -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter import com.tangem.common.ui.notifications.NotificationId 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.domain.notifications.ShouldShowNotificationUseCase +import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent -import com.tangem.features.send.v2.api.SendEntryPointComponent +import com.tangem.features.send.v2.entrypoint.SendEntryRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger import com.tangem.features.swap.v2.api.SendWithSwapComponent -import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn import jakarta.inject.Inject -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Suppress("LongParameterList") @ModelScoped internal class SendEntryPointModel @Inject constructor( - paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - val appRouter: AppRouter, - private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, + val router: Router, private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, -) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback { +) : Model(), + SendComponent.ModelCallback, + SendWithSwapComponent.ModelCallback, + ChooseManagedTokensComponent.ModelCallback { - private val params: SendEntryPointComponent.Params = paramsContainer.require() - - val sendEntryPointState: StateFlow - field = MutableStateFlow(SendEntryPoint.SendVanilla) - - private var swapChooseTokenListenerJobHolder = JobHolder() + private var lastSavedAmount = "" override fun onConvertToAnotherToken(lastAmount: String) { + lastSavedAmount = lastAmount modelScope.launch { val showSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) - appRouter.push( - AppRoute.ChooseManagedTokens( - userWalletId = params.userWalletId, - initialCurrency = params.cryptoCurrency, - selectedCurrency = null, - source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + router.push( + SendEntryRoute.ChooseToken( showSendViaSwapNotification = showSendViaSwapNotification, ), ) - observeChooseSelectToken(lastAmount) } } override fun onCloseSwap(lastAmount: String) { + lastSavedAmount = lastAmount modelScope.launch { if (lastAmount.isNotBlank()) { sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount) } - triggerScreenUpdate(SendEntryPoint.SendVanilla) + router.replaceAll(SendEntryRoute.Send) } } - private fun observeChooseSelectToken(lastAmount: String) { - swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow - .onEach { currency -> - if (lastAmount.isNotBlank()) { - swapAmountUpdateTrigger.triggerUpdateAmount(lastAmount) - } - triggerScreenUpdate(SendEntryPoint.SendWithSwap) + @Suppress("MagicNumber") + override fun onResult() { + modelScope.launch { + if (lastSavedAmount.isNotBlank()) { + swapAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount) } - .launchIn(modelScope) - .saveIn(swapChooseTokenListenerJobHolder) + // Workaround in order to execute correct exit animation on SendEntryRoute.ChooseToken + router.pop() + delay(10L) + router.replaceAll(SendEntryRoute.SendWithSwap) + } } - private fun triggerScreenUpdate(entry: SendEntryPoint) { - swapChooseTokenListenerJobHolder.cancel() - sendEntryPointState.update { entry } + override fun onBack() { + router.pop() } -} - -enum class SendEntryPoint { - SendVanilla, - SendWithSwap, } \ No newline at end of file