Files
TREK/client/src/utils/startDestination.ts
T
Maurice e7851e79ff fix(client): honour the start page on a device that never mirrored it
The mirror answered "dashboard" for a browser that had simply never been
told, and RootRedirect took that as the user's choice - so the setting was
ignored on every device except the one that set it, and on that one too
after a logout cleared the mirror. The setting itself was stored correctly
the whole time, which is what made it look like nothing happened.

readStartDestination now says "unknown" rather than guessing, and a device
in that state waits for the real settings instead. That costs one wait,
once per device; afterwards the mirror answers and the launch is as fast
as before. Bounded by a timeout, because loadSettings deliberately leaves
isLoaded false on a failed request so it can retry - without the floor, a
launch with no backend would sit on the spinner forever.
2026-08-08 23:01:40 +02:00

104 lines
3.9 KiB
TypeScript

import { isTripTabId } from '../constants/tripTabs'
import type { Settings } from '../types'
/**
* Where opening TREK lands. 'dashboard' is what TREK has always done; with
* 'active_trip' the app skips straight into the trip the dashboard would
* feature, on the tab picked below — three clicks and three loads less for
* someone entering expenses while they're actually travelling.
*/
export type StartPage = 'dashboard' | 'active_trip'
export const DEFAULT_START_PAGE: StartPage = 'dashboard'
export const DEFAULT_START_TRIP_TAB = 'plan'
/**
* The route that resolves the destination (RootRedirect in App.tsx). Anywhere
* that used to navigate to '/dashboard' for lack of a better idea sends people
* here instead, so the preference is applied in exactly one place.
*/
export const START_DESTINATION_ROUTE = '/'
/**
* How long a device that has never seen the preference waits for the settings
* before giving up and opening the dashboard. Long enough for a normal request,
* short enough that a launch with no backend isn't stuck staring at a spinner.
*/
export const SETTINGS_WAIT_MS = 2500
export interface StartDestination {
page: StartPage
tab: string
}
// Mirrored in localStorage, not read from the settings store, because the
// redirect has to be decided on the first paint — waiting for GET /api/settings
// would put a spinner in front of every launch, including the default one that
// isn't redirecting anywhere. Same trick as 'app_language' and the theme boot
// snapshot: the server's answer wins on the next load, this is only what we
// knew last time.
const PAGE_KEY = 'trek_start_page'
const TAB_KEY = 'trek_start_trip_tab'
const isStartPage = (value: unknown): value is StartPage =>
value === 'dashboard' || value === 'active_trip'
/**
* What this device last knew, or null if it has never been told — a fresh
* browser, another machine, or one that has logged out since.
*
* The null case matters: the preference lives on the server, so "this device
* doesn't know" is not the same as "this user wants the dashboard". Callers
* have to wait for the real settings rather than assume the default, or the
* setting is silently ignored on every device that didn't set it.
*/
export function readStartDestination(): StartDestination | null {
try {
const page = localStorage.getItem(PAGE_KEY)
if (!isStartPage(page)) return null
const tab = localStorage.getItem(TAB_KEY)
return { page, tab: isTripTabId(tab) ? tab : DEFAULT_START_TRIP_TAB }
} catch {
// Private-mode Safari and friends throw on access, not just on write.
return null
}
}
/** Called whenever the settings store learns a value — load, single or bulk save. */
export function rememberStartDestination(settings: Partial<Settings>): void {
try {
if ('start_page' in settings) {
const page = settings.start_page
if (isStartPage(page)) localStorage.setItem(PAGE_KEY, page)
else localStorage.removeItem(PAGE_KEY)
}
if ('start_trip_tab' in settings) {
const tab = settings.start_trip_tab
if (isTripTabId(tab)) localStorage.setItem(TAB_KEY, tab)
else localStorage.removeItem(TAB_KEY)
}
} catch {
// A browser that won't persist this just falls back to the dashboard.
}
}
/** On logout, so the next account on this browser starts on its own preference. */
export function forgetStartDestination(): void {
try {
localStorage.removeItem(PAGE_KEY)
localStorage.removeItem(TAB_KEY)
} catch {
// Nothing to clean up if storage is unavailable.
}
}
/**
* The planner URL for a startup redirect. The tab always goes on the query
* string, including 'plan' — leaving it off would hand the decision back to
* whichever tab the last session happened to end on.
*/
export function tripStartPath(tripId: number, tab: string): string {
const safeTab = isTripTabId(tab) ? tab : DEFAULT_START_TRIP_TAB
return `/trips/${tripId}?tab=${safeTab}`
}