Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 45 additions & 78 deletions apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@ import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.LocalUserManager
import com.flipcash.app.core.extensions.navigateAll
import com.flipcash.app.core.navigation.DeeplinkAction
import com.flipcash.app.core.navigation.NavBarButton
import com.flipcash.app.core.navigation.NavBarConfig
import com.flipcash.app.core.ui.NavigationBar
import com.flipcash.app.core.ui.rememberNavigationBarState
import com.flipcash.app.core.verification.email.LocalEmailCodeChannel
import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.featureflags.LocalFeatureFlags
import com.flipcash.app.featureflags.model.BackgroundResetTimeout
import com.flipcash.app.internal.ui.navigation.AppContent
import com.flipcash.app.internal.ui.navigation.NewAppContent
import com.flipcash.app.internal.ui.navigation.appEntryProvider
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBlockingOverlayEntryDecorator
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
Expand Down Expand Up @@ -80,6 +86,7 @@ import kotlinx.coroutines.flow.first
internal fun App(
tipsEngine: TipsEngine,
) {
val features = LocalFeatureFlags.current
val router = LocalRouter.current!!
val analytics = rememberAnalytics()
val viewModel = getActivityScopedViewModel<HomeViewModel>()
Expand Down Expand Up @@ -110,6 +117,8 @@ internal fun App(
val session = LocalSessionController.current!!
val userState by userManager.state.collectAsStateWithLifecycle()

val isNewUi by features.observe(FeatureFlag.NewUi).collectAsStateWithLifecycle()

FlipcashTheme {
rememberQrBitmapPainter(
content = stringResource(
Expand Down Expand Up @@ -149,82 +158,40 @@ internal fun App(
LocalSharedTransitionScope provides this,
) {
CoinbaseOnRampHandler {
AppNavHost(
navigator = codeNavigator,
resultStateRegistry = resultStateRegistry,
decorators = listOf(
rememberNavMessagingEntryDecorator(
codeNavigator.backStack,
barManager
),
rememberNavBlockingOverlayEntryDecorator(),
),
sceneStrategies = listOf(
ModalBottomSheetSceneStrategy(
codeNavigator.resultStore
) {
codeNavigator.backStack.getOrNull(
codeNavigator.backStack.lastIndex - 1
)
},
SinglePaneSceneStrategy(),
),
transitionSpec = {
val shouldCrossfade =
initialState.key == AppRoute.Loading.toString() ||
targetState.key == AppRoute.Loading.toString() ||
targetState.key.toString()
.startsWith("Login")
when {
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
tween(300)
)

targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
EnterTransition.None togetherWith ExitTransition.None

else -> slideInHorizontally(initialOffsetX = { it }) togetherWith
slideOutHorizontally(targetOffsetX = { -it })
}
},
popTransitionSpec = {
val shouldCrossfade =
initialState.key == AppRoute.Loading.toString() ||
targetState.key == AppRoute.Loading.toString() ||
targetState.key.toString()
.startsWith("Login")
when {
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
tween(300)
)

targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
EnterTransition.None togetherWith ExitTransition.None

else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
}
},
predictivePopTransitionSpec = {
val shouldCrossfade =
initialState.key == AppRoute.Loading.toString() ||
targetState.key == AppRoute.Loading.toString() ||
targetState.key.toString()
.startsWith("Login")
when {
shouldCrossfade -> fadeIn(tween(300)) togetherWith fadeOut(
tween(300)
)

targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
EnterTransition.None togetherWith ExitTransition.None

else -> slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
if (isNewUi) {
NewAppContent(
codeNavigator = codeNavigator,
resultStateRegistry = resultStateRegistry,
barManager = barManager,
deepLink = { deepLink },
onPendingAction = { action ->
deeplinkHandled = true
when (action) {
is DeeplinkAction.OpenCashLink ->
session.openCashLink(action.entropy)
is DeeplinkAction.PresentTipCard ->
session.resolveTipCard(action.userId)
is DeeplinkAction.Login ->
viewModel.handleLoginEntropy(
action.entropy,
onSwitchAccount = {
codeNavigator.replaceAll(
AppRoute.OnboardingFlow(
seed = action.entropy,
fromDeeplink = true
)
)
},
onDismissed = { }
)
else -> {}
}
deepLink = null
}
},
onBack = { codeNavigator.navigateBack() },
entryProvider = appEntryProvider(
)
} else {
AppContent(
codeNavigator = codeNavigator,
resultStateRegistry = resultStateRegistry,
barManager = barManager,
deepLink = { deepLink },
Expand All @@ -251,9 +218,9 @@ internal fun App(
else -> {}
}
deepLink = null
},
),
)
}
)
}

ScrimOverlay(scrimController)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.flipcash.app.internal.ui

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
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.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.NavBarButton
import com.flipcash.app.core.navigation.NavBarConfig
import com.flipcash.app.core.navigation.asNavBarTab
import com.flipcash.app.core.navigation.destinationRoute
import com.flipcash.app.core.ui.NavigationBar
import com.flipcash.app.core.ui.rememberNavigationBarState
import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.featureflags.LocalFeatureFlags
import com.getcode.manager.BottomBarManager
import com.getcode.navigation.core.CodeNavigator
import com.getcode.theme.CodeTheme

/**
* The hoisted v2 navigation bar — root chrome, not owned by any screen. It renders over whichever
* top-level route is a tab home and switches tabs by **swapping the current screen** (single
* backstack, like a tab bar — hence [CodeNavigator.replaceAll], not a sheet).
*
* Only visible when [FeatureFlag.NewUi] is on and the current route maps to a tab; v1 keeps its
* in-screen bar (see ScannerNavigationBar). When v1 is dropped, this becomes the only nav bar.
*
* Self-positions as a full-size, touch-transparent overlay pinned to the bottom, so it can be
* dropped into any container (it does not require a BoxScope from its caller).
*/
@Composable
internal fun AppNavigationBar(
navigator: CodeNavigator,
modifier: Modifier = Modifier,
) {
// Selection follows the base of the backstack (the tab "home"), so it stays correct while a
// sheet/modal sits on top and is right on launch. The top route only gates visibility.
val selectedTab = navigator.backStack.firstNotNullOfOrNull { (it as? AppRoute)?.asNavBarTab() }
val topTab = (navigator.currentRouteKey as? AppRoute)?.asNavBarTab()

// A BottomBar message (e.g. the deposit-options modal) renders above the nav host; hide the bar
// while one is showing so it sits below the modal instead of floating over it.
val bottomBarMessages by BottomBarManager.messages.collectAsStateWithLifecycle()

Box(
modifier = Modifier
.then(modifier),
contentAlignment = Alignment.BottomCenter,
) {
AnimatedVisibility(
visible = topTab != null && bottomBarMessages.isEmpty(),
enter = slideInVertically { it } + fadeIn(),
exit = slideOutVertically { it } + fadeOut(),
) {
val state = rememberNavigationBarState(
isNewUi = true,
config = NavBarConfig(order = NavBarButton.v2Order),
selectedTab = selectedTab ?: NavBarButton.Wallet,
)
NavigationBar(
modifier = Modifier
.navigationBarsPadding()
.padding(horizontal = CodeTheme.dimens.grid.x8)
.padding(bottom = CodeTheme.dimens.grid.x3),
state = state,
onButtonClick = { button ->
// Tab bar semantics: swap the current screen (single backstack).
button.destinationRoute()?.let(navigator::replaceAll)
},
)
}
}
}
Loading
Loading