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
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ internal fun App(
// gives. Pushing it instead reads as a modal on a stack
// the user never navigated. Mirrors iOS
// DeepLinkController's requestedCardMint.
codeNavigator.navigateAll(listOf(AppRoute.Sheets.Wallet))
codeNavigator.navigateAll(listOf(AppRoute.Tabs.Wallet))
cardExpansion.beginExpanded(action.mint)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import com.flipcash.app.core.navigation.DeeplinkAction
import com.flipcash.app.core.navigation.LocalTabBarVisibility
import com.flipcash.app.core.navigation.NavBarButton
import com.flipcash.app.core.navigation.TabBarVisibilityController
import com.flipcash.app.core.navigation.asNavBarTab
import com.flipcash.app.core.navigation.destinationRoute
import com.flipcash.app.core.ui.transitions.CardExpandTransition
import com.flipcash.app.core.tokens.TokenInfoEntry
Expand Down Expand Up @@ -102,8 +101,7 @@ internal fun AppContent(
// A tab press replaces the whole back stack (tab-bar semantics — see AppNavigationBar), so every
// tab home was destroyed and rebuilt on each switch: the wallet re-fetched its balances and the
// chat list scrolled back to the top. Hold each tab home's ViewModels and scroll state outside the
// back stack so a switch shows what the tab last had. Only the four routes a tab press produces
// are held, so variants of a tab route (a resumed Tips, say) still get a fresh screen.
// back stack so a switch shows what the tab last had.
val tabHomeKeys = remember {
NavBarButton.entries.map { it.destinationRoute().toString() }.toSet()
}
Expand All @@ -113,7 +111,7 @@ internal fun AppContent(
// push keeps its tab home on the stack, so this fires on a replaceAll away from the tabs.
LaunchedEffect(entryState, codeNavigator.backStack) {
snapshotFlow {
codeNavigator.backStack.any { (it as? AppRoute)?.asNavBarTab() != null }
codeNavigator.backStack.any { it is AppRoute.Tabs }
}.collect { onTabs -> if (!onTabs) entryState.releaseAll() }
}

Expand Down Expand Up @@ -166,7 +164,7 @@ internal fun AppContent(
// into a detail screen and slides in. Pops always slide back out (a pop is always
// leaving a detail). Sheets/overlays keep their own (no) transition.
transitionSpec = {
val landsOnTab = (codeNavigator.currentRouteKey as? AppRoute)?.asNavBarTab() != null
val landsOnTab = codeNavigator.currentRouteKey is AppRoute.Tabs
when {
targetState is OverlayScene<*> || initialState is OverlayScene<*> ->
EnterTransition.None togetherWith ExitTransition.None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import com.flipcash.app.currencycreator.CurrencyCreatorFlowScreen
import com.flipcash.app.tipping.FindByUsernameScreen
import com.flipcash.app.tipping.NewChatScreen
import com.flipcash.app.tipping.NewGroupFlowScreen
import com.flipcash.app.tipping.TippingFlowScreen
import com.flipcash.app.tipping.ChatsScreen
import com.flipcash.shared.transactionhistory.ActivityHistoryScreen
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.DeeplinkAction
Expand Down Expand Up @@ -90,26 +90,26 @@ fun appEntryProvider(
SheetContent(key, resultStateRegistry, barManager)
}
annotatedEntry<AppRoute.Main.AppRestricted> { key -> AppRestrictedScreen(key.restrictionType) }
annotatedEntry<AppRoute.Main.Scanner> { ScannerScreen() }
annotatedEntry<AppRoute.Main.RegionSelection> { RegionSelectionScreen() }
annotatedEntry<AppRoute.Main.InviteContact> { key -> InviteContactScreen(key.phoneNumber) }

// Sheets (inner content — wrapped in Main.Sheet by navigateTo())
// Route type is `Give` but the screen is the Cash/Give screen the flows call cash_screen.
annotatedEntry<AppRoute.Sheets.Give>(testTag = "cash_screen") { key -> CashScreen(key.mint, key.fromTokenInfo) }
annotatedEntry<AppRoute.Sheets.Tips> { key ->
TippingFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}
annotatedEntry<AppRoute.Sheets.TokenSelection> { key -> TokenSelectScreen(key.purpose) }
annotatedEntry<AppRoute.Sheets.Wallet> {
// Tabs (nav bar homes — flat on the root backstack, never wrapped in Main.Sheet)
annotatedEntry<AppRoute.Tabs.Scanner> { ScannerScreen() }
annotatedEntry<AppRoute.Tabs.Wallet> {
// The wallet hosts the card-expand overlay in-entry so a pushed action (Give/Convert/Withdraw)
// covers the expanded currency-info with correct z-order (iOS WalletScreen structure).
CardExpandHost { WalletScreen() }
}
annotatedEntry<AppRoute.Tabs.Chats> { ChatsScreen() }
annotatedEntry<AppRoute.Tabs.Menu> { MenuScreen() }

// Sheets (inner content — wrapped in Main.Sheet by navigateTo())
// Route type is `Give` but the screen is the Cash/Give screen the flows call cash_screen.
annotatedEntry<AppRoute.Sheets.Give>(testTag = "cash_screen") { key -> CashScreen(key.mint, key.fromTokenInfo) }
annotatedEntry<AppRoute.Sheets.TokenSelection> { key -> TokenSelectScreen(key.purpose) }
annotatedEntry<AppRoute.Sheets.ShareApp> { ShareAppScreen() }
annotatedEntry<AppRoute.Sheets.ActivityHistory> { ActivityHistoryScreen() }
annotatedEntry<AppRoute.Sheets.TransactionDetails> { key -> TransactionDetailsScreen(key.id) }
annotatedEntry<AppRoute.Sheets.Menu> { MenuScreen() }

// Messaging
annotatedEntry<AppRoute.Messaging.Chat> { key ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.navigation3.runtime.NavEntryDecorator
import androidx.navigation3.runtime.NavKey
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.LocalTabBarPadding
import com.flipcash.app.core.navigation.asNavBarTab
import com.getcode.navigation.core.CodeNavigator

/**
Expand Down Expand Up @@ -41,7 +40,7 @@ fun NavTabBarInsetEntryDecorator(
val route = remember(entry.contentKey) {
navigator.backStack.firstOrNull { it.toString() == entry.contentKey } as? AppRoute
}
val isTabHome = route?.asNavBarTab() != null
val isTabHome = route is AppRoute.Tabs
val padding = if (isTabHome) PaddingValues(bottom = tabBarHeight.value) else PaddingValues()

CompositionLocalProvider(LocalTabBarPadding provides padding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,24 @@ class BuildNavGraphForLaunchTest {
@Test
fun `logged in without deeplink opens on the Chats tab`() {
val result = build(AuthState.Ready)!!
assertEquals(listOf(AppRoute.Sheets.Tips()), result.baseRoutes)
assertEquals(listOf(AppRoute.Tabs.Chats), result.baseRoutes)
assertTrue(result.deeplinkRoutes.isEmpty())
assertEquals(listOf(AppRoute.Sheets.Tips()), result.resolvedBackStack())
assertEquals(listOf(AppRoute.Tabs.Chats), result.resolvedBackStack())
}

@Test
fun `logged in with Navigate deeplink includes deeplink routes`() {
val routes = listOf(AppRoute.Main.Scanner)
val routes = listOf(AppRoute.Tabs.Scanner)
val result = buildReady(DeeplinkAction.Navigate(routes))
assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes)
assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes)
assertEquals(routes, result.deeplinkRoutes)
}

@Test
fun `logged in with OpenCashLink fires eagerly via pendingAction`() {
val action = DeeplinkAction.OpenCashLink("testEntropy")
val result = buildReady(action)
assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes)
assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes)
assertTrue(result.deeplinkRoutes.isEmpty())
assertEquals(action, result.pendingAction)
}
Expand All @@ -80,7 +80,7 @@ class BuildNavGraphForLaunchTest {
fun `logged in with Login action fires eagerly via pendingAction`() {
val action = DeeplinkAction.Login("seed")
val result = buildReady(action)
assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes)
assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes)
assertTrue(result.deeplinkRoutes.isEmpty())
assertEquals(action, result.pendingAction)
}
Expand All @@ -91,18 +91,18 @@ class BuildNavGraphForLaunchTest {
// no pushed screen, so nothing lands in deeplinkRoutes. See DeeplinkAction.OpenToken.
val action = DeeplinkAction.OpenToken(
mint = Mint(MINT),
routes = listOf(AppRoute.Sheets.Wallet, AppRoute.Token.Info(Mint(MINT))),
routes = listOf(AppRoute.Tabs.Wallet, AppRoute.Token.Info(Mint(MINT))),
)
val result = buildReady(action)
assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes)
assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes)
assertTrue(result.deeplinkRoutes.isEmpty())
assertEquals(action, result.pendingAction)
}

@Test
fun `logged in with None action opens the Chats tab without deeplink routes`() {
val result = buildReady(DeeplinkAction.None)
assertEquals(listOf(AppRoute.Sheets.Tips()), result.baseRoutes)
assertEquals(listOf(AppRoute.Tabs.Chats), result.baseRoutes)
assertTrue(result.deeplinkRoutes.isEmpty())
}

Expand Down Expand Up @@ -195,13 +195,13 @@ class BuildNavGraphForLaunchTest {
fun `token deeplink pushes token info onto the Wallet tab without a sheet`() {
val result = buildReady(
DeeplinkAction.Navigate(
listOf(AppRoute.Sheets.Wallet, AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink))
listOf(AppRoute.Tabs.Wallet, AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink))
)
)

val stack = result.resolvedBackStack()
assertEquals(2, stack.size)
assertEquals(AppRoute.Sheets.Wallet, stack[0])
assertEquals(AppRoute.Tabs.Wallet, stack[0])
assertIs<AppRoute.Token.Info>(stack[1])
assertTrue(stack.none { it is AppRoute.Main.Sheet }, "a tab home must never be wrapped in a sheet")
}
Expand All @@ -211,18 +211,18 @@ class BuildNavGraphForLaunchTest {
val result = buildReady(
DeeplinkAction.Navigate(
listOf(
AppRoute.Sheets.Tips(),
AppRoute.Tabs.Chats,
AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(ChatId(listOf(1, 2, 3, 4)))),
)
)
)

val stack = result.resolvedBackStack()
assertEquals(2, stack.size)
assertIs<AppRoute.Sheets.Tips>(stack[0])
assertIs<AppRoute.Tabs.Chats>(stack[0])
assertIs<AppRoute.Messaging.Chat>(stack[1])
// The launch home must be replaced by the target tab, not left underneath it.
assertTrue(stack.none { it == AppRoute.Sheets.Wallet })
assertTrue(stack.none { it == AppRoute.Tabs.Wallet })
assertTrue(stack.none { it is AppRoute.Main.Sheet })
}

Expand All @@ -231,7 +231,7 @@ class BuildNavGraphForLaunchTest {
val result = buildReady(
DeeplinkAction.Navigate(
listOf(
AppRoute.Sheets.Menu,
AppRoute.Tabs.Menu,
AppRoute.Menu.MyAccount,
AppRoute.Verification(
origin = AppRoute.Menu.MyAccount,
Expand All @@ -245,15 +245,15 @@ class BuildNavGraphForLaunchTest {

val stack = result.resolvedBackStack()
assertEquals(3, stack.size)
assertEquals(AppRoute.Sheets.Menu, stack[0])
assertEquals(AppRoute.Tabs.Menu, stack[0])
assertTrue(stack.none { it is AppRoute.Main.Sheet })
}

@Test
fun `pending actions still launch on the Wallet tab`() {
val action = DeeplinkAction.OpenCashLink("testEntropy")
val result = buildReady(action)
assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes)
assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes)
assertEquals(action, result.pendingAction)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class BaselineProfileGenerator {
// The chats tab lists conversations; open the first one and send a text message. A
// message is fund-free (money-safety: never tap Send Cash / confirm a spend). The
// conversation is not hardcoded — send_contact_row resolves to the first row.
openTab("nav_chats", "tips_screen")
openTab("nav_chats", "chats_screen")

device.wait(Until.findObject(By.res("send_contact_row")), TIMEOUT)?.click()
device.wait(Until.findObject(By.res("chat_screen")), LOGIN_TIMEOUT)
Expand All @@ -132,7 +132,7 @@ class BaselineProfileGenerator {

// Back to the conversation list, fling it for coverage, then back to the scanner.
device.pressBack()
device.wait(Until.findObject(By.res("tips_screen")), TIMEOUT)
device.wait(Until.findObject(By.res("chats_screen")), TIMEOUT)
device.waitForIdle()
flingScroll("chat_list", Direction.UP, 2)
flingScroll("chat_list", Direction.DOWN, 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ sealed interface AppRoute : NavKey, Parcelable {

@Serializable
data class AppRestricted(val restrictionType: RestrictionType) : Main
@Serializable
data object Scanner : Main

// TODO: is there a better place for this to live?
@Serializable
Expand Down Expand Up @@ -168,20 +166,36 @@ sealed interface AppRoute : NavKey, Parcelable {
get() = steps
}

/**
* The homes of the hoisted nav bar's tabs. They sit flat on the root backstack, so the bar stays
* visible over them, and a route list leading with one is applied as a tab switch (see
* `navigateAll`). `NavBarRoutes.kt` maps each to its `NavBarButton`.
*/
@Serializable
@Parcelize
sealed interface Sheets : AppRoute {
sealed interface Tabs : AppRoute {
@Serializable
data class TokenSelection(val purpose: TokenPurpose) : Sheets
data object Scanner : Tabs

@Serializable
data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets
data object Wallet : Tabs

/** Tip DMs and groups. */
@Serializable
data class Tips(val resumed: Boolean = false): Sheets {
}
data object Chats : Tabs

/** Settings, plus the user's tip card. The nav bar labels it "You". */
@Serializable
data object Wallet : Sheets
data object Menu : Tabs
}

@Serializable
@Parcelize
sealed interface Sheets : AppRoute {
@Serializable
data class TokenSelection(val purpose: TokenPurpose) : Sheets
@Serializable
data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets

/** Full unified paged activity history — the "dive in" from the wallet's recent-activity preview. */
@Serializable
Expand All @@ -197,9 +211,6 @@ sealed interface AppRoute : NavKey, Parcelable {
@Serializable
data class TransactionDetails(val id: ID) : Sheets

@Serializable
data object Menu : Sheets

@Serializable
data object ShareApp : Sheets

Expand Down Expand Up @@ -335,9 +346,8 @@ sealed interface AppRoute : NavKey, Parcelable {
/**
* Node 10127:117987 — the two ways to start a chat, reached from the "+" on the Chats list.
*
* A top-level route rather than a step of the tipping flow, even though the Chats list it
* is reached from is one: the flow is a tab home, so a step pushed inside it keeps the tab
* bar. This covers it, the way [Chat] does. [FindByUsername] and [NewGroup] are pushed on
* A top-level route rather than something drawn inside [Tabs.Chats]: that is a tab home, so
* anything inside it keeps the tab bar. This covers it, the way [Chat] does. [FindByUsername] and [NewGroup] are pushed on
* top of it for the same reason.
*/
@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.flipcash.app.core.extensions
import androidx.compose.runtime.snapshots.Snapshot
import androidx.navigation3.runtime.NavKey
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.asNavBarTab
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.core.NavOptions

Expand Down Expand Up @@ -35,12 +34,12 @@ fun CodeNavigator.openAsSheet(route: AppRoute, innerRoutes: List<AppRoute> = emp
}

/**
* True when [routes] leads with a route that is a tab home.
* True when [routes] leads with an [AppRoute.Tabs] home.
* Such a list is applied as a *tab switch* (the leading route replaces the stack) rather than
* stacked on top of whatever tab the user was on.
*/
private fun leadsWithTab(routes: List<NavKey>): Boolean =
(routes.firstOrNull() as? AppRoute)?.asNavBarTab() != null
routes.firstOrNull() is AppRoute.Tabs

/**
* Navigate to multiple routes, wrapping [AppRoute.Sheets] in [AppRoute.Main.Sheet].
Expand All @@ -50,9 +49,8 @@ private fun leadsWithTab(routes: List<NavKey>): Boolean =
* If a sheet is already open and the new routes include a sheet, the current sheet
* is animated closed before the new one opens.
*
* The tab homes — `Sheets.Wallet`, `Sheets.Tips`, `Sheets.Menu` — are *not* sheets, so a route
* list leading with one switches to that tab (replacing the stack) and pushes the rest on top
* of it. See [resolveRoutes].
* A route list leading with an [AppRoute.Tabs] home switches to that tab (replacing the stack)
* and pushes the rest on top of it. See [resolveRoutes].
*/
fun CodeNavigator.navigateAll(
routes: List<NavKey>,
Expand Down Expand Up @@ -100,10 +98,9 @@ fun CodeNavigator.navigateAll(
* [AppRoute.Main.Sheet] with inner routes, mirroring what [navigateAll] pushes
* onto the backstack. Useful for predicting the resulting stack without navigating.
*
* The tab homes — `Sheets.Wallet`, `Sheets.Tips`, `Sheets.Menu` — are top-level tab destinations
* rather than modals, so they stay flat on the root backstack (which keeps the hoisted nav bar
* visible and lets back/pop behave like a tab stack). Anything after the tab route is resolved
* independently, so a genuine sheet later in the list still wraps.
* [AppRoute.Tabs] homes are not sheets, so they stay flat on the root backstack (which keeps the
* hoisted nav bar visible and lets back/pop behave like a tab stack). A sheet later in the list
* still wraps, along with everything after it.
*/
fun resolveRoutes(routes: List<NavKey>): List<NavKey> {
if (routes.isEmpty()) return emptyList()
Expand All @@ -112,11 +109,6 @@ fun resolveRoutes(routes: List<NavKey>): List<NavKey> {
if (sheetIndex < 0) return routes

val sheetRoute = routes[sheetIndex] as AppRoute.Sheets

if (sheetRoute.asNavBarTab() != null) {
return routes.take(sheetIndex + 1) + resolveRoutes(routes.drop(sheetIndex + 1))
}

val before = routes.take(sheetIndex)
val innerRoutes = routes.drop(sheetIndex + 1).filterIsInstance<AppRoute>()
return before + AppRoute.Main.Sheet(sheetRoute, innerRoutes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.flipcash.app.core.AppRoute
* [com.getcode.navigation.core.NavOptions.PopUpTo.ClearAll], exactly like a tab switch from the
* nav bar.
*/
val homeRoute: AppRoute = AppRoute.Sheets.Wallet
val homeRoute: AppRoute = AppRoute.Tabs.Wallet

/**
* The tab a cold launch opens on when no deeplink directs it elsewhere: Chats. Built from the tab's
Expand Down
Loading
Loading