Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-13 09:56:02 +03:00
parent b97ddcb3f2
commit c6ddab601b
20 changed files with 370 additions and 160 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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) {

View file

@ -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 <reified T : ViewModel> 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<T>(viewModelStoreOwner)
}

View file

@ -11,4 +11,7 @@ android {
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.compose.foundation)
implementation(deps.compose.navigation)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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<AlertState?>(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) {}
}
}

View file

@ -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<TokenItemState>, addCustomTokenButton: AddCustomTokenButton) {
LazyColumn {
internal fun TokensList(
tokens: LazyPagingItems<TokenItemState>,
addCustomTokenButton: AddCustomTokenButton,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Text(
text = stringResource(id = R.string.manage_tokens_title),

View file

@ -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<ManageTokensViewModel>().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<ManageTokensViewModel>().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<AddCustomTokenViewModel>(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<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
AddCustomTokenScreen(state = viewModel.uiState)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseNetwork.route,
) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
AddCustomTokenScreen(state = viewModel.uiState)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseNetwork.route,
) { backStackEntry ->
val viewModel =
backStackEntry.parentHiltViewModel<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
ChooseNetworkCustomScreen(
state = viewModel.uiState.chooseNetworkState,
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseDerivation.route,
) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
ChooseNetworkCustomScreen(
state = viewModel.uiState.chooseNetworkState,
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseDerivation.route,
) { backStackEntry ->
val viewModel =
backStackEntry.parentHiltViewModel<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
ChooseDerivationScreen(
state = requireNotNull(viewModel.uiState.chooseDerivationState),
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseWallet.route,
) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
ChooseDerivationScreen(
state = requireNotNull(viewModel.uiState.chooseDerivationState),
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseWallet.route,
) { backStackEntry ->
val viewModel =
backStackEntry.parentHiltViewModel<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
CustomTokensChooseWalletScreen(
state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose,
)
}
CustomTokensChooseWalletScreen(
state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose,
)
}
}
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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(

View file

@ -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)

View file

@ -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<WalletScreenState> get() = mutableUiState
val value: WalletScreenState get() = uiState.value
private val mutableUiState: MutableStateFlow<WalletScreenState> = MutableStateFlow(value = getInitialState())
@ -81,6 +85,7 @@ internal class WalletStateController @Inject constructor() {
onWalletChange = {},
event = consumedEvent(),
isHidingMode = false,
manageTokenRedesignToggle = manageTokensFeatureToggles.isRedesignedScreenEnabled,
)
}
}

View file

@ -11,4 +11,5 @@ internal data class WalletScreenState(
val onWalletChange: (Int) -> Unit,
val event: StateEvent<WalletEvent>,
val isHidingMode: Boolean,
val manageTokenRedesignToggle: Boolean,
)

View file

@ -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<WalletAlertState?>(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<Boolean>,
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)