From c6ddab601beeccd94e67b4ef1b644fa94ee9bb55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Feb 2024 09:56:02 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/common/extensions/Navigation.kt | 13 +- core/ui/build.gradle.kts | 3 + .../com/tangem/core/ui/components/Spacers.kt | 5 + .../tangem/core/ui/components/atoms/Hand.kt | 17 +- .../ui/extensions/ComposeNavigationExt.kt | 49 ++++++ features/manage-tokens/api/build.gradle.kts | 3 + .../navigation/ManageTokensRouter.kt | 8 +- features/manage-tokens/impl/build.gradle.kts | 1 + .../managetokens/ManageTokensFragment.kt | 37 ----- .../managetokens/ui/ManageTokensScreen.kt | 61 ++++--- .../managetokens/ui/components/TokensList.kt | 8 +- .../router/DefaultManageTokensRouter.kt | 110 +++++++------ .../router/InnerManageTokensRouter.kt | 9 - features/wallet/impl/build.gradle.kts | 3 +- .../wallet/presentation/WalletFragment.kt | 9 +- .../router/DefaultWalletRouter.kt | 29 +++- .../presentation/router/InnerWalletRouter.kt | 3 +- .../wallet/state/WalletStateController.kt | 7 +- .../wallet/state/model/WalletScreenState.kt | 1 + .../presentation/wallet/ui/WalletScreen.kt | 154 +++++++++++++++++- 20 files changed, 370 insertions(+), 160 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 6dc414e945..fa0d174bf9 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -151,18 +151,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.ResetToFactory -> ResetCardFragment() AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() AppScreen.Disclaimer -> DisclaimerFragment() - AppScreen.ManageTokens -> { - val featureToggles = store.state.daggerGraphState.get( - getDependency = DaggerGraphState::manageTokensFeatureToggles, - ) - if (featureToggles.isRedesignedScreenEnabled) { - store.state.daggerGraphState - .get(getDependency = DaggerGraphState::manageTokensRouter) - .getEntryFragment() - } else { - TokensListFragment() - } - } + AppScreen.ManageTokens -> TokensListFragment() AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> { store.state.daggerGraphState diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index ab9b42cd1f..62f9551fa1 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -30,6 +30,8 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.utils) implementation(deps.compose.coil) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) @@ -38,4 +40,5 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.zxing.qrCore) implementation(deps.jodatime) + implementation(deps.timber) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt index ad4fc53b0a..99230c1853 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -39,6 +39,11 @@ fun SpacerH16(modifier: Modifier = Modifier) { SpacerH(16.dp, modifier) } +@Composable +fun SpacerH18(modifier: Modifier = Modifier) { + SpacerH(18.dp, modifier) +} + @Composable fun SpacerH24(modifier: Modifier = Modifier) { SpacerH(24.dp, modifier) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt index cbb97ce8e1..f3d155f5a2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt @@ -1,17 +1,13 @@ package com.tangem.core.ui.components.atoms import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -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.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.TangemTheme /** @@ -21,7 +17,7 @@ import com.tangem.core.ui.res.TangemTheme fun Hand(modifier: Modifier = Modifier) { Box( modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing8) + .height(handComposableComponentHeight) .fillMaxWidth(), contentAlignment = Alignment.Center, ) { @@ -37,6 +33,11 @@ fun Hand(modifier: Modifier = Modifier) { } } +val handComposableComponentHeight: Dp + @Composable + @ReadOnlyComposable + get() = TangemTheme.dimens.size4 + TangemTheme.dimens.spacing16 + // region Preview @Composable private fun HandSample(modifier: Modifier = Modifier) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt new file mode 100644 index 0000000000..2dc5bcd17e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt @@ -0,0 +1,49 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import timber.log.Timber + +/** + * The ViewModel is scoped to the parent route Navigation graph + * and is provided using the Hilt-generated ViewModel factory + * + * ``` + * val navController = rememberNavController() + * + * navigation( + * route = "parent", + * startDestination = "parent/1" + * ) { + * composable("route/1") { entry -> + * val viewModel = entry.parentHiltViewModel(navController) + * } + * composable("route/2") { entry -> + * val viewModel = entry.parentHiltViewModel(navController) + * } + * composable("route/3") { entry -> + * val viewModel = entry.parentHiltViewModel(navController) + * } + * } + * ``` + * + * @param navController NavController within the common NavGraph + * @throws Exception if there is no parent route + */ +@Composable +inline fun NavBackStackEntry.parentHiltViewModel(navController: NavController): T { + val viewModelStoreOwner = remember(this) { + try { + navController.getBackStackEntry(this.destination.parent!!.id) + } catch (e: Exception) { + Timber.tag("scopedViewModel").e(e, "There is no parent route'") + throw e + } + } + + return hiltViewModel(viewModelStoreOwner) +} \ No newline at end of file diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index aa4ad2bd26..1195231904 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -11,4 +11,7 @@ android { dependencies { /** AndroidX */ implementation(deps.androidx.fragment.ktx) + + implementation(deps.compose.foundation) + implementation(deps.compose.navigation) } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensRouter.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensRouter.kt index 7dd58d2a5b..b6310d4e11 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensRouter.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensRouter.kt @@ -1,8 +1,12 @@ package com.tangem.features.managetokens.navigation -import androidx.fragment.app.Fragment +import androidx.compose.ui.unit.Dp +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController interface ManageTokensRouter { - fun getEntryFragment(): Fragment + val startDestination: String + + fun NavGraphBuilder.initialize(navController: NavHostController, onHeaderSizeChange: (Dp) -> Unit) } \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 1be0e9701e..8fb983d6d7 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.tangem.card.core) + implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt deleted file mode 100644 index 87b216af73..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.managetokens - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder -import com.tangem.features.managetokens.navigation.ManageTokensRouter -import com.tangem.managetokens.presentation.router.InnerManageTokensRouter -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -internal class ManageTokensFragment : ComposeFragment() { - - @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder - - @Inject - lateinit var manageTokensRouter: ManageTokensRouter - - private val innerManageTokensRouter: InnerManageTokensRouter - get() = requireNotNull(manageTokensRouter as? InnerManageTokensRouter) { - "internalManageTokensRouter should be instance of InnerManageTokensRouter" - } - - @Composable - override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - - innerManageTokensRouter.Initialize(viewModelStoreOwner = this) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt index 0f83676b10..28dd482aeb 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt @@ -8,13 +8,18 @@ import androidx.compose.material.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.SpacerH18 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.res.TangemTheme @@ -32,7 +37,7 @@ import com.tangem.managetokens.presentation.managetokens.ui.components.TokensLis import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar @Composable -internal fun ManageTokensScreen(state: ManageTokensState) { +internal fun ManageTokensScreen(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) { var alertState by remember { mutableStateOf(value = null) } EventEffect( @@ -43,43 +48,50 @@ internal fun ManageTokensScreen(state: ManageTokensState) { Alert(state = it, onDismiss = { alertState = null }) } - Content(state) + Content(state = state, onHeaderSizeChange = onHeaderSizeChange) } @Composable -private fun Content(state: ManageTokensState) { +private fun Content(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) { val keyboard by keyboardAsState() + val density = LocalDensity.current + var tokenListAlertBottomPadding by remember(keyboard is Keyboard.Opened) { mutableStateOf(0.dp) } + Box( modifier = Modifier .fillMaxSize() .navigationBarsPadding() .imePadding() - .background(color = TangemTheme.colors.background.primary) - .padding(top = TangemTheme.dimens.spacing32), + .background(color = TangemTheme.colors.background.primary), ) { Column { val listState = rememberLazyListState() val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } - val elevation by animateDpAsState( - targetValue = if (raiseSearchBar) { - TangemTheme.dimens.elevation8 - } else { - TangemTheme.dimens.elevation0 - }, + targetValue = if (raiseSearchBar) TangemTheme.dimens.elevation8 else TangemTheme.dimens.elevation0, label = "top_bar_elevation", ) + Surface( elevation = elevation, - modifier = Modifier, + modifier = Modifier.onGloballyPositioned { + with(density) { onHeaderSizeChange(it.size.height.toDp()) } + }, ) { TokensSearchBar( state = state.searchBarState, modifier = Modifier .background(color = TangemTheme.colors.background.primary) - .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing20), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing4, + ), ) } + + SpacerH18() + val tokens = state.tokens.collectAsLazyPagingItems() val query = state.searchBarState.query @@ -89,7 +101,15 @@ private fun Content(state: ManageTokensState) { onEmptySearchResult = state.onEmptySearchResult, ) - TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton) + TokensList( + modifier = Modifier.padding(bottom = tokenListAlertBottomPadding), + tokens = tokens, + addCustomTokenButton = state.addCustomTokenButton, + ) + } + + state.selectedToken?.let { selectedToken -> + ManageTokensBottomSheet(selectedToken = selectedToken, state = state) } state.derivationNotification?.let { @@ -97,14 +117,13 @@ private fun Content(state: ManageTokensState) { DerivationNotification( config = it.config, modifier = Modifier - .align(Alignment.BottomCenter), + .align(Alignment.BottomCenter) + .onGloballyPositioned { + with(density) { tokenListAlertBottomPadding = it.size.height.toDp() } + }, ) } } - - state.selectedToken?.let { selectedToken -> - ManageTokensBottomSheet(selectedToken = selectedToken, state = state) - } } } @@ -158,7 +177,7 @@ private fun Preview_ManageTokensScreen_LightTheme( state: ManageTokensState, ) { TangemTheme(isDark = false) { - ManageTokensScreen(state) + ManageTokensScreen(state) {} } } @@ -169,7 +188,7 @@ private fun Preview_ManageTokensScreen_DarkTheme( state: ManageTokensState, ) { TangemTheme(isDark = true) { - ManageTokensScreen(state) + ManageTokensScreen(state) {} } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt index 475f9a8687..d6c2b505ea 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt @@ -17,8 +17,12 @@ import com.tangem.managetokens.presentation.managetokens.state.TokenItemState private const val PLACEHOLDER_ITEMS_COUNT = 50 @Composable -internal fun TokensList(tokens: LazyPagingItems, addCustomTokenButton: AddCustomTokenButton) { - LazyColumn { +internal fun TokensList( + tokens: LazyPagingItems, + addCustomTokenButton: AddCustomTokenButton, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { item { Text( text = stringResource(id = R.string.manage_tokens_title), diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt index 5a4ec5b232..5ecebe79fe 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt @@ -1,25 +1,22 @@ package com.tangem.managetokens.presentation.router -import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.fragment.app.Fragment +import androidx.compose.ui.unit.Dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModelStoreOwner +import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import androidx.navigation.navigation import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController -import com.tangem.managetokens.ManageTokensFragment -import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.core.ui.extensions.parentHiltViewModel +import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenScreen import com.tangem.managetokens.presentation.addcustomtoken.ui.ChooseDerivationScreen import com.tangem.managetokens.presentation.addcustomtoken.ui.ChooseNetworkCustomScreen import com.tangem.managetokens.presentation.addcustomtoken.ui.CustomTokensChooseWalletScreen -import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenScreen import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel +import com.tangem.managetokens.presentation.common.state.ChooseWalletState import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel import kotlin.properties.Delegates @@ -30,64 +27,69 @@ internal class DefaultManageTokensRouter( private var navController: NavHostController by Delegates.notNull() - override fun getEntryFragment(): Fragment = ManageTokensFragment() + override val startDestination: String = ManageTokensRoute.ManageTokens.route - @Composable - override fun Initialize(viewModelStoreOwner: ViewModelStoreOwner) { - NavHost( - navController = rememberNavController().apply { navController = this }, - startDestination = ManageTokensRoute.ManageTokens.route, - ) { - composable(ManageTokensRoute.ManageTokens.route) { - val viewModel = hiltViewModel().apply { router = this@DefaultManageTokensRouter } - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + override fun NavGraphBuilder.initialize(navController: NavHostController, onHeaderSizeChange: (Dp) -> Unit) { + this@DefaultManageTokensRouter.navController = navController - ManageTokensScreen(state = viewModel.uiState) + composable(ManageTokensRoute.ManageTokens.route) { + val viewModel = hiltViewModel().apply { + router = this@DefaultManageTokensRouter } + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) - navigation( - startDestination = ManageTokensRoute.AddCustomToken.Main.route, - route = ManageTokensRoute.AddCustomToken.route, - ) { - composable( - ManageTokensRoute.AddCustomToken.Main.route, - ) { - val viewModel = hiltViewModel(viewModelStoreOwner).apply { + ManageTokensScreen( + state = viewModel.uiState, + onHeaderSizeChange = onHeaderSizeChange, + ) + } + + navigation( + route = ManageTokensRoute.AddCustomToken.route, + startDestination = ManageTokensRoute.AddCustomToken.Main.route, + ) { + composable( + ManageTokensRoute.AddCustomToken.Main.route, + ) { backStackEntry -> + val viewModel = + backStackEntry.parentHiltViewModel(navController = navController).apply { router = this@DefaultManageTokensRouter } - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) - AddCustomTokenScreen(state = viewModel.uiState) - } - composable( - ManageTokensRoute.AddCustomToken.ChooseNetwork.route, - ) { - val viewModel = hiltViewModel(viewModelStoreOwner).apply { + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + AddCustomTokenScreen(state = viewModel.uiState) + } + composable( + ManageTokensRoute.AddCustomToken.ChooseNetwork.route, + ) { backStackEntry -> + val viewModel = + backStackEntry.parentHiltViewModel(navController = navController).apply { router = this@DefaultManageTokensRouter } - ChooseNetworkCustomScreen( - state = viewModel.uiState.chooseNetworkState, - ) - } - composable( - ManageTokensRoute.AddCustomToken.ChooseDerivation.route, - ) { - val viewModel = hiltViewModel(viewModelStoreOwner).apply { + ChooseNetworkCustomScreen( + state = viewModel.uiState.chooseNetworkState, + ) + } + composable( + ManageTokensRoute.AddCustomToken.ChooseDerivation.route, + ) { backStackEntry -> + val viewModel = + backStackEntry.parentHiltViewModel(navController = navController).apply { router = this@DefaultManageTokensRouter } - ChooseDerivationScreen( - state = requireNotNull(viewModel.uiState.chooseDerivationState), - ) - } - composable( - ManageTokensRoute.AddCustomToken.ChooseWallet.route, - ) { - val viewModel = hiltViewModel(viewModelStoreOwner).apply { + ChooseDerivationScreen( + state = requireNotNull(viewModel.uiState.chooseDerivationState), + ) + } + composable( + ManageTokensRoute.AddCustomToken.ChooseWallet.route, + ) { backStackEntry -> + val viewModel = + backStackEntry.parentHiltViewModel(navController = navController).apply { router = this@DefaultManageTokensRouter } - CustomTokensChooseWalletScreen( - state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose, - ) - } + CustomTokensChooseWalletScreen( + state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose, + ) } } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/InnerManageTokensRouter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/InnerManageTokensRouter.kt index 4c45b39d4a..999ec29252 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/InnerManageTokensRouter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/InnerManageTokensRouter.kt @@ -1,19 +1,10 @@ package com.tangem.managetokens.presentation.router -import androidx.compose.runtime.Composable -import androidx.lifecycle.ViewModelStoreOwner import com.tangem.core.navigation.AppScreen import com.tangem.features.managetokens.navigation.ManageTokensRouter internal interface InnerManageTokensRouter : ManageTokensRouter { - /** - * Initialize router - **/ - @Suppress("TopLevelComposableFunctions") - @Composable - fun Initialize(viewModelStoreOwner: ViewModelStoreOwner) - /** Pop back stack */ fun popBackStack(screen: AppScreen? = null) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index a24c30a994..e2d5cb6d0f 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -73,10 +73,11 @@ dependencies { //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) - implementation(projects.features.tester.api) /** Feature Apis */ implementation(projects.features.wallet.api) implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) + implementation(projects.features.tester.api) + implementation(projects.features.manageTokens.api) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index e8c0881cfe..c13cfa5e90 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.wallet.navigation.WalletRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -22,6 +23,9 @@ internal class WalletFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder + @Inject + internal lateinit var manageTokensRouter: ManageTokensRouter + /** Feature router */ @Inject internal lateinit var walletRouter: WalletRouter @@ -38,7 +42,10 @@ internal class WalletFragment : ComposeFragment() { setSystemBarsColor(systemBarsColor) } - _walletRouter.Initialize(onFinish = requireActivity()::finish) + _walletRouter.Initialize( + onFinish = requireActivity()::finish, + manageTokensRouter = manageTokensRouter, + ) } companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 5a7e3675da..ecf64015f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -1,10 +1,10 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel @@ -27,6 +27,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import kotlin.properties.Delegates @@ -41,7 +42,7 @@ internal class DefaultWalletRouter( override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit) { + override fun Initialize(onFinish: () -> Unit, manageTokensRouter: ManageTokensRouter) { this.onFinish = onFinish NavHost( @@ -54,7 +55,27 @@ internal class DefaultWalletRouter( subscribeToLifecycle(LocalLifecycleOwner.current) } - WalletScreen(state = viewModel.uiState.collectAsStateWithLifecycle().value) + var bottomSheetHeaderHeight by remember { mutableStateOf(0.dp) } + val innerController = rememberNavController() + + WalletScreen( + state = viewModel.uiState.collectAsStateWithLifecycle().value, + bottomSheetHeaderHeightProvider = { bottomSheetHeaderHeight }, + bottomSheetContent = { + NavHost( + navController = innerController, + startDestination = manageTokensRouter.startDestination, + ) { + // Manage Tokens + with(manageTokensRouter) { + initialize( + navController = innerController, + onHeaderSizeChange = { bottomSheetHeaderHeight = it }, + ) + } + } + }, + ) } composable( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index c8f7b955ce..d5d53d879f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.navigation.AppScreen import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.wallet.navigation.WalletRouter /** @@ -25,7 +26,7 @@ internal interface InnerWalletRouter : WalletRouter { */ @Suppress("TopLevelComposableFunctions") @Composable - fun Initialize(onFinish: () -> Unit) + fun Initialize(onFinish: () -> Unit, manageTokensRouter: ManageTokensRouter) /** Pop back stack */ fun popBackStack(screen: AppScreen? = null) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 6f86d37b02..f9e1705b63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarCon import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer +import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -23,9 +24,12 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class WalletStateController @Inject constructor() { +internal class WalletStateController @Inject constructor( + private val manageTokensFeatureToggles: ManageTokensFeatureToggles, +) { val uiState: StateFlow get() = mutableUiState + val value: WalletScreenState get() = uiState.value private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) @@ -81,6 +85,7 @@ internal class WalletStateController @Inject constructor() { onWalletChange = {}, event = consumedEvent(), isHidingMode = false, + manageTokenRedesignToggle = manageTokensFeatureToggles.isRedesignedScreenEnabled, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 9285575d9e..b4a5a8dc25 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -11,4 +11,5 @@ internal data class WalletScreenState( val onWalletChange: (Int) -> Unit, val event: StateEvent, val isHidingMode: Boolean, + val manageTokenRedesignToggle: Boolean, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index e491dcb364..8341a0792c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -9,20 +10,26 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.FabPosition -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.paging.compose.collectAsLazyPagingItems +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R @@ -40,9 +47,14 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balances import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch @Composable -internal fun WalletScreen(state: WalletScreenState) { +internal fun WalletScreen( + state: WalletScreenState, + bottomSheetHeaderHeightProvider: () -> Dp, + bottomSheetContent: @Composable () -> Unit, +) { BackHandler(onBack = state.onBackClick) // It means that screen is still initializing @@ -58,6 +70,8 @@ internal fun WalletScreen(state: WalletScreenState) { snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + bottomSheetContent = bottomSheetContent, ) var alertConfig by remember { mutableStateOf(value = null) } @@ -76,19 +90,21 @@ internal fun WalletScreen(state: WalletScreenState) { ) } -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") @Composable private fun WalletContent( state: WalletScreenState, walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, + bottomSheetHeaderHeightProvider: () -> Dp, onAutoScrollReset: () -> Unit, + bottomSheetContent: @Composable () -> Unit, ) { var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) } val selectedWallet = state.wallets[selectedWalletIndex] - BaseScaffold(state = state, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState) { + val scaffoldContent: @Composable () -> Unit = { val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) val lazyTxHistoryItems = (selectedWallet as? TxHistoryStateHolder)?.let { walletState -> @@ -183,6 +199,130 @@ private fun WalletContent( onAutoScrollReset = onAutoScrollReset, ) } + + if (state.manageTokenRedesignToggle) { + BaseScaffoldManageTokenRedesign( + state = state, + selectedWallet = selectedWallet, + snackbarHostState = snackbarHostState, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + bottomSheetContent = bottomSheetContent, + ) { + scaffoldContent() + } + } else { + BaseScaffold( + state = state, + selectedWallet = selectedWallet, + snackbarHostState = snackbarHostState, + ) { + scaffoldContent() + } + } +} + +@Suppress("LongParameterList", "LongMethod") +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@Composable +private fun BaseScaffoldManageTokenRedesign( + state: WalletScreenState, + selectedWallet: WalletState, + snackbarHostState: SnackbarHostState, + bottomSheetHeaderHeightProvider: () -> Dp, + bottomSheetContent: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + val scaffoldState = rememberBottomSheetScaffoldState() + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + val systemUiController = rememberSystemUiController() + val navigationBarColor = TangemTheme.colors.background.primary + val navigationBarColorWithout = TangemTheme.colors.background.secondary + + DisposableEffect( + navigationBarColor, + navigationBarColorWithout, + ) { + systemUiController.setNavigationBarColor(navigationBarColor) + onDispose { + systemUiController.setNavigationBarColor(navigationBarColorWithout) + } + } + + val keyboardShown by keyboardAsState() + // expand bottom sheet when keyboard appears + LaunchedEffect(keyboardShown is Keyboard.Opened) { + if (keyboardShown is Keyboard.Opened) { + scaffoldState.bottomSheetState.expand() + } + } + + val keyboardController = LocalSoftwareKeyboardController.current + val sheetHasBeenHidden = scaffoldState.bottomSheetState.targetValue == SheetValue.PartiallyExpanded + // hide keyboard when bottom sheet is about to be hidden + LaunchedEffect(sheetHasBeenHidden) { + if (sheetHasBeenHidden) { + keyboardController?.hide() + } + } + + val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + val coroutineScope = rememberCoroutineScope() + + BottomSheetScaffold( + topBar = { + WalletTopBar(config = state.topBarConfig) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + containerColor = TangemTheme.colors.background.secondary, + sheetContainerColor = TangemTheme.colors.background.primary, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetDragHandle = { + Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary)) + }, + sheetContent = { + BoxWithConstraints { + Box( + modifier = Modifier + .sizeIn(maxHeight = maxHeight - statusBarHeight) + .align(Alignment.BottomCenter), + ) { + bottomSheetContent() + } + } + + // hide bottom sheet when back pressed + BackHandler( + keyboardShown is Keyboard.Closed && + scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded, + ) { + coroutineScope.launch { scaffoldState.bottomSheetState.partialExpand() } + } + }, + content = { paddingValues -> + val pullRefreshState = rememberPullRefreshState( + refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + ) + + Box( + modifier = Modifier + .pullRefresh(pullRefreshState) + .padding(paddingValues), + ) { + content() + + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + }, + ) } @OptIn(ExperimentalMaterialApi::class)