A multi-day hike restocks on the way: bread in the morning, food for
lunch and supper every day or two. Neither could be searched for.
- Explore pill: new `bakery` and `supermarket` chips. `supermarket`
already existed on the server (shop=supermarket|convenience) but no
pill offered it; `bakery` is new (shop=bakery).
- Along the tour: `bakery` and `supermarket` are offered to a hike,
after its existing kinds so a new hike still starts on the same three.
- TREK places index: `bakery` maps to food_and_drink>casual_eatery>bakery;
`supermarket` is narrowed to grocery and convenience stores, since the
whole food_and_beverage_store branch also brought tobacconists and
wine shops. Both paths checked against a live answer of the index.
As the more specific term, a bakery is no longer listed as a place to
eat when only `food` is asked along a tour.
- Category guess: a bakery or grocer picked from the map was preselected
as Bar/Cafe or Shopping even where the admin had made a Bakery or
Groceries category. The guess now looks for such a category first, by
name in the usual languages or by icon, and keeps the broad one when
there is none. The category icon picker offers Croissant and
ShoppingBasket, the icons the map chips use.
- Labels in every locale; MCP tool description names the new kinds.
Every file under assets/ was compressed again on each download, at the low
level a request path can afford. They carry their content hash in the name
and never change, so the client build now writes a Brotli copy beside each
compressible one (postbuild, scripts/precompress.mjs) and production points
a browser that accepts Brotli at that copy before express.static serves it.
Measured on a production build of 5.1.2, fetching the 652 precached files
the way a service worker install does: 16.3 MB -> 13.9 MB on the wire and
about 1.0 s -> 0.2 s of server CPU per install. A client without Brotli, a
range request and a build without copies are served exactly as before.
The login card is a fixed light surface whose text colours are written out,
not tokens. Four of them moved with the two-factor step, so the baseline moves
them too: 58 to 54 for the page, 4 for the new component, the same total.
Both files had grown past their size limit with this round's fixes. The
two-factor step moves to pages/login/MfaCodeStep.tsx and the reservation row
types to reservation-rows.ts, re-exported from the repository, and both size
baselines come down to match.
On the wide and split size classes the touch shell no longer sends the tours tab back to the plan. It gets the desktop's tour editor, wired over the shell's own planner (useTripPlannerPageOf): on split the trip's tours and the editor share a pane beside the map, on wide the pane and the map take turns. A phone keeps its tours in the places browser.
The desktop's import dialog keeps its markup and moves its logic into
useGoogleRouteImport, unchanged: the link previewed, its stops in order with
the unresolved ones named, and the resolved ones appended to the day picked,
at least two, under one request id per opening and day. The large screens
get the same flow as a sheet, offered on the stage card under the track
offer to a member who may add places to days. The phone has no entry to it.
The desktop's track dialog comes to the large screens as a sheet over the
planner's followTrack, the hook RoadtripTrackModal runs on: the trip's tracks
nearest to the day first, the drive fitted to the one picked with via points,
and those vias dropped again. The stage card offers it in the wide and split
size classes, to an editor of days, on a trip that holds a track, and names
the track the day follows. The phone and the folded Duo have no entry to it.
On the phone the road trip's driving figures stay a read-out, set at the
desktop. A large screen is that table, so in the wide and split size classes
the same sheet edits the two driving limits, the day's window and where it
ends, the vehicle with its range and fill, and what the route may avoid,
through the planner's saveRoadtripLimit, the desktop dialog's own save. A
range worked out from the vehicle's own figures stays read-only, as on the
desktop, and a reader without the right to edit days sees every field
disabled. The phone and the folded Duo are unchanged.
- The trip's segmented control writes its fit through a ref, not into the
element held in state (react-hooks/immutability).
- The amount field of a payment uses the body type tier, which follows the
text size setting, instead of a raw size.
- The form sheet tests wait the frame the edge fade now measures on, and read
the wide variant from the size class the shell writes.
- The native dashes spec lists its locales through node:fs, so shared needs no
vite types for it (knip).
- Baselines lowered where the fixes removed warnings: no-misused-promises,
no-non-null-assertion in tests, the MSheet rtl entry.
- Top bar: the segmented control keeps to the room between the back button
and the trailing button, draws its segments tighter and then scrolls with
faded edges instead of covering the back button. On a wide window every
list tab puts its primary pill in the middle and its round buttons at the
trailing end, so the pill no longer jumps between tabs.
- Day strip: a chevron over the faded end pages the days along on a wide
window, and the fade reaches further; the chips keep clear of it.
- Plan pane: the day card ends on the rail's bottom line beside the map (as
tall as its day across the page), scrolls inside a frame and fades the
rows at its edges. Addresses and the hotel chip end in an ellipsis instead
of being cut mid-letter; the weather chip keeps its width.
- Map: in the dark theme the map beside the pane takes the dark basemap.
- Places: the All / Unplanned / Planned chips are never narrower than their
names and scroll with a faded edge; on a short screen Places/Tours shares
their row.
- Chat: the list fades out under the floating controls instead of cutting a
bubble. Notes and to-do chip rows fade at their edges and New Note stays
outside the scrolling chips.
- Costs: the balance amounts share a line, the settle-up action wraps to a
line of its own, the totals stand under their labels.
- Bookings and transports: one column until two cards fit, titles take two
lines with the type under them, the time cell is never cut and a range
names its unit once.
- Files: the preview pane shows a type tile beside the name where no PDF can
be drawn in place, its actions keep their labels on one line and wrap as a
row, link labels are badges, and a tile that opens its file is no toggle.
- Polls get a fill that differs from card and track, packing filters are two
segmented controls, empty checkboxes get an edge with 3:1 contrast, and the
middle dots between values are gone.
- useCollabChatData: the resize observer that keeps the chat at its newest
message attaches when the list is drawn (after the messages load), not at
mount when there is no list yet. The test renders the real list after load.
- MCollabTab: the chat stays mounted, hidden, while a board fills the page on
its own, so leaving two panes with Notes picked keeps the half typed
message and the scroll position. The links card hugs its links on a wide
window instead of a page-high empty card.
- openingView: the floored opening zoom and the second fit for the map's own
size apply only beside the pane in the touch shell; the desktop planner
and the phone open at Leaflet's nearest level again.
In the touch shell the back, forward, disclosure, date-range and flow
arrows kept pointing right in Arabic, so Settle up showed the debt the
wrong way round and Vacay's year arrows pointed inward. The shell now
mirrors lucide's direction arrows in right-to-left languages; an icon
that names its own treatment keeps it, and the lightbox arrows stay on
their physical sides.
Latin user text in Arabic rows lost its beginning when cut and moved its
punctuation to the wrong end. Names, titles, addresses and file names take
their direction from their own text (dir=auto) and still line up with the
row; markdown paragraphs, chat messages, poll options and journal stories
are isolated; signed balances, amounts, sizes, rates and leg figures read
as one left-to-right run; URL fields read left to right. Name pairs joined
by an arrow isolate each name and point from the first to the second.
The hero progress and poll result bars fill from the start edge. Arabic
and Hebrew uppercase caption labels grow a step and take the muted ink,
and a todo's due date in a notification is written in the reader's
language instead of as YYYY-MM-DD.
From the large-screen review (visual findings of the shell and the top-level
screens):
- Rail: 80px wide, one caption size for every name (no shrinking per item),
balanced two-line names with every entry as tall as the tallest, short rail
names where a language's word is too long (mobileNav.rail*, all locales),
and near-opaque glass so a map's colours no longer smear through it.
- Rail head: always one 56px circle, so the entries never jump between pages:
the create action, the screen's own action (the vacation calendar's
year/editor switch moves there instead of floating over the days) or the
quiet logo on settings and administration. It stays put while a short rail
scrolls.
- Vacation calendar: weekday letters over each month on a wide window; the
editor's mode switch sits at the foot with room kept for it.
- Docks: More is marked with a ring rather than the centre button's fill, the
dock glass is denser, and lists fade out under the dock instead of showing
in the gap below it.
- Dashboard: trip names get the card's width with the dates under them, the
hero's facts wrap as whole chips, the filter chips scroll with a fade, the
hero ends above the dock on a phone on its side and its buttons sit on a
dark ground; the header bar stands in the content's column.
- Journeys: on a wide window the header is the same glass bar, in the column
of the cards, and the other journeys' label starts with them.
- Notifications: the round chevron back button, in the list's column.
- Settings and administration share one frame and width. Admin's new-user
action sits under the section list's title, the users card header wraps
inside the card, stat labels ellipsize, role badges read in dark mode.
- Collections: one card per place with the status inside it, worded in a
readable mix of its colour.
- Atlas and the journey map frame clear of the rail and the header; the
journey's day bar sits on a glass plate, and a phone on its side gets
smaller cards.
- Rail label tests rewritten for the new rule; FE-MOB-PANES-016 duplicate
renumbered to 017.
On an iPad or an unfolded foldable the list screens kept one phone-wide
column in the middle of the page. They now lay out for the room they have:
- Transports, bookings, polls and tasks put their cards in a grid of as many
phone-wide columns as the page holds (CardGrid), three on a 13" iPad on
its side, two upright, one in a narrow Split View.
- Costs: with room for two panes (split size class) the summary (total,
owe and owed, settle up, final budget, categories) stands in a pane of its
own beside the expenses, each scrolling on its own (TabPanes). The search
field sits in the expenses' column again instead of running past it.
- Files: a grid of tiles with each photo's picture; with room for two panes
the picked file is previewed beside the grid with what it belongs to and
its actions, the way Files on an iPad does. The signed preview address
comes through fileRepo.previewUrl.
- Packing: the categories stand in columns, each card whole in its column;
the action menu drops from its button at a popover's width.
- To-dos: the four smart filters as tiles and the trip's lists in a pane
beside the tasks, which take the filter's name as their head.
- Collab: the chat stays in view at the trailing edge beside the notes,
links or polls, and the header switches only the board. The chat keeps its
place in the tree, so a fold or a rotation into two panes keeps the draft;
a chat at its newest message stays there when its box changes size. Notes
stand as a board in columns.
- Administration and settings share a section list beside the page
(MSectionNav); the stats grid takes four columns.
- Collections: the lists stand in a column beside the places, which take two
columns, and the head names the list on show.
- Journal: the entries become a feed beside the map, under their days, with
photo, place and the start of the story, in step with the map like the
strip of cards it replaces; it fades out under the header.
- The assistant opens as a tall panel at the trailing edge over a dimmed
page, as Slide Over would, instead of covering the whole screen.
- The rail fits every name: a word too wide for it shrinks the label before
it is hyphenated, and the admin entry uses its short name.
- The journeys and notifications headers keep to their column.
- The address screen of the native shell sets the photo as a panel beside
the choices on a large screen on its side.
On a foldable that reports its fold, a leading pane ends at the fold and a
trailing one starts there. Phones keep their layout.
The app shows the touch shell at every size now, an iPad's and an unfolded
foldable's included, instead of switching to the desktop pages from 768px.
One route tree survives rotation, Split View, Stage Manager and folding, and
the app's own features (Wallet, sharing into a trip, the edge swipe back)
are there on the big screen too.
Two size classes drive the layout (hooks/sizeClass.ts, Tailwind variants
wide: and split:):
- wide (600pt and taller than a phone on its side): a glass navigation rail
on the leading edge replaces the dock and holds every destination with its
name; sheets become centred form sheets; the trip's More menu is a popover
beside the rail; lists keep a readable measure; dashboard, journey and
vacay get columns; full-bleed maps run under the floating rail.
- split (900pt): the trip's plan, places or road trip chain sit in a pane
beside the map, which follows the plan's day; the list/map switch hides
and shows the pane like a sidebar; on a foldable that reports its fold the
pane ends at the fold. Settings show their sections beside the page and the
dashboard keeps its widgets in a column of their own.
A hardware keyboard gets the shortcuts an iPad app has (⌘1 to ⌘9 for the
sections, ⌘N, ⌘, , ⌥←/⌥→ through the days, ⌃⌘S for the pane, ⌘[ back),
answered by the page or, with a binary that has the keyCommands feature,
handed to it as native key commands.
The day strip fades at an edge that runs on, journey cards grow on a wide
window and a card without a photo takes the day's colour, and the Leaflet
map measures itself again when its box changes size.
Everything the iOS shell gained, built natively for Android 16 with Material
You, and back to Android 7:
- Home-screen widgets in Jetpack Glance: trip, today, bookings, to-dos,
packing, world map and travel group, from the same snapshot the iOS widgets
read. Wallpaper colours on Android 12+, resizable in three layouts, a trip
picker per widget (reconfigurable), live previews in Android 15's picker
and preview images before it.
- The travel day as a Live Update: a promoted ongoing notification with the
next stop as the status-bar chip, a countdown and the day's stops as
segments (ProgressStyle), a quiet ongoing notification before Android 16.
- A share sheet of its own: a Material bottom sheet with the trip picker for
files, mails, pages and links shared from any app, and the coming trips as
launcher and direct-share shortcuts.
- Push: channels per kind of notification (chat, trip updates, invitations,
general) by the event the server now names, stacked per channel.
- Location for navigator.geolocation from the fused provider, a store for
Mapbox's map resources (30 days, 250 MB), Google Play Billing behind the
StoreKit methods, and Google Wallet save links.
- Large screens: every orientation and window size without a restart,
the window's size classes and a foldable's posture and hinge for the UI
(windowLayout), the UI's keyboard shortcuts as the app's (keyCommands),
predictive back, per-app language.
- The native strings in the 28 languages of the iOS app, generated from its
strings files (scripts/generate-android-strings.mjs).
Debug builds carry a widget gallery that draws every widget at every size.
"Add to Apple Wallet" on a booking in the iOS app turns it into a TREK pass:
a boarding pass from and to for flights, trains, buses and boats, check-in and
check-out for a stay, pickup and drop-off for a car, the time for anything
else, with the confirmation code as a QR code. Wallet brings it up on the lock
screen around its time and place: times carry the offset of the endpoint's zone
(looked up from the coordinates where the endpoint has none), stays and
restaurants their place's coordinates. The labels come from the booking forms'
own strings, so nothing new to translate on the pass.
Only liketrek holds the Pass Type ID certificate, so the server sends the pass's
texts to the TREK services' signer (TREK_WALLET_PASS_URL, push.liketrek.com)
once per tap; it stores nothing, and TREK_SERVICES=off turns it off with the
other services. Texts are cleaned of links and shortened here, the answer must
be a .pkpass within 512 KB, and a user gets 20 passes a minute.
- New wallet-pass domain: GET /api/trips/:tripId/reservations/:id/wallet-pass
behind the trip guard, the booking checked to belong to the trip; trip title
and place come from the reservations domain (findPassContext)
- MWalletButton in the transport and booking sheets, only where the app binary
has the wallet feature; undated bookings and failures are told apart
- TREK_WALLET_PASS_URL in .env.example, compose, Helm and the wiki
MDemoWelcome used raw rem sizes, which ignore the text size setting and broke
theme:lint, and was missing from feature-pairs.json, which broke lint:pairs.
- The carousel and timeline tests step by the new 90 ms settle
- The journey search keeps its suggestion type through the fallback
- nearestCardIndex leaves useJourneyCarouselSync, which had grown past its
line limit; WidgetBag is not exported; the test baseline drops by the
non-null assertions a test no longer needs
- RTL: the measured pills and centred controls use logical forms or a marked line
- i18n: the two native widget numbers are positions, not counts (NOT_PLURAL)
- Types: styleOverride on the lazy GL map, the location and shared-file payloads,
the capabilities fallback, the transport sheet's stay metadata
- ESLint: latest-value refs move to layout effects, usePresence keeps the last
open state in state, the booking import opens clean with a shared document
kept in the list (the reset effect used to clear it again)
- Layers: the phone import sheet lives under mobile/ and reads through tripRepo;
the offline host check moves to utils/tileUrl
- Sizes: plugin layer geometry, hardNavigate and the exit animations get
modules of their own
- Tests follow the status radios, the stay chip overview, the dock + button
for the first list and the field token; store registry lists the new stores
Atlas bucket list, open route in, open in TREK, the place actions and the
MCP/OAuth client sheets now show the icon tile header (or, where a sheet
closes only by its buttons, the same title row without the close).
Admin sheets: their frame draws the icon tile header (an icon per sheet, the
plugin sheets' icons moved out of the title into the tile), the soft scroll
edge and 42 px footer buttons; admin fields use the sheets' eyebrow labels
and round inputs. The settings picker, the bulk category sheet and the save
to list sheet get the common header (TileHeader may now leave out the icon
for a plain picker). Every phone sheet now uses the glass material, as the
trip settings do; it stays 94 % opaque, so forms over the map remain
legible.
The shared confirm sheet (44 places) gets the icon tile with the title, a
warning icon for destructive questions, and the sheets' 42 px buttons with
the danger colour on a destructive confirm. The dashboard's and the
collections' own confirm dialogs now use it.
Vacay invite, shared calendars and settings get the icon tile header; the
settings' rows separated by hairlines become cards on the inner surface, as
the trip settings' sections.
Both get the icon tile header (the entry keeps its move buttons between
title and close), glass, the soft scroll edge and the sheets' 42 px footer
buttons; the journey name is a round field, delete in the entry's footer a
round icon button. The collection share sheet's invite button is round like
the trip settings' one.
The collections sheets had a header of their own with a hairline, white
square fields and short buttons. Their kit now draws the icon tile header
every phone sheet has (an icon per sheet), round 42 px fields on the field
surface and 42 px footer buttons; the sheets sit on glass with the soft
scroll edge. New list and add place lose their last own field styles: the
labels are the sheets' eyebrows, the search fields, the category picker and
the status choices are round fields of the same height.
A tour tapped in the plan opened the desktop detail dialog in a hand-built
overlay: no slide, no drag to close, the desktop's frame on a phone. The
dialog now renders into the frame it is given; on the phone that is a sheet
like every other detail (MTourDetailShell), with the tour's name as its
title, its whole content and every action, the back gesture and an exit
animation.
The journey settings sheet looked like a different app next to the trip
settings. It now uses the same parts: the glass card with the icon tile
header and the journey's name under the title, sections as inner cards with
a title, count badge and hint, 42 px round fields and buttons, the shared
link field, and switch rows for tracks, entry fields and the public link.
Those parts (SheetSection and the SHEET_* button and field classes) moved
into MTripSheetUi for every sheet to use.
Fields and buttons sitting directly in a column no longer shrink below
their height; name, subtitle and the archive button were squeezed flat.
Collections: the empty screen's own New list button is gone; the dock's +
starts the first list while there is none and adds places once there is one.
Trip settings: counts sit in a badge next to the section title instead of
reading as part of it (ACCESS 1), and every round button of the sheet,
invite and add guest as well as the member and guest actions, is one 42 px
circle; the icon-only buttons were wider than tall.
Journey settings: the public link is the same field as the trip's share and
invite links, through one shared MLinkField; the shared map's extras hang
under the map row with a guide line and smaller labels; removing the link is
a full-size button.
Add place on the phone: the place details showed the mascot half a screen
high while nothing was picked or nothing was found, and pushed the name field
below the fold. Both states are now one quiet line, as in the desktop dialog,
and the details sit in a card in the mobile tokens.
Sheets with a header over a scroll box (trip settings, day, export,
transport, files and more) cut their content off in a hard line right under
the header icon. The scroll box now keeps a little room under the header and
fades its content out at the edges.
Copy and share next to a share or invite link are round icon buttons of the
same size; the share button showed no icon because the copy button's padding
left it no room.
Page transitions: on the phone, pages move like a native navigation stack,
built on the View Transitions API (mobile/routeTransition.ts). A page opened
from another slides in from the trailing edge, back slides it away, and
switching between the dock's sections cross-fades while the dock stands
still. Lazy routes do not flash their skeleton: the swap waits, behind the
frozen snapshot, until the new page rendered. The edge-swipe back gesture
already moves the page and skips the transition. RTL mirrors the motion,
reduced motion and desktop get none.
Pull-to-refresh on the mobile dashboard: the page follows the finger with
resistance, ticks once past the threshold and re-reads trips and
notifications quietly, without a loading state.
Share: trip share and invite links and the journey link get a share button
next to copy, opening the system share sheet (Capacitor Share in the apps,
Web Share in browsers that have it).
Haptics: a selection click for switches and segmented controls, a tick when
a sheet is flung closed, and a firmer bump for the chat's long press.
Theme lint: this branch's new screens use the type and z-index tokens, the
Vacay company tint is a token, and the baseline is lowered to the new
counts.
Selecting a place no longer spiderfies a city-sized cluster: the reveal
only fans open bubbles of up to 12 stops, or stacks whose stops all share
one coordinate, so a zoomed-out trip with hundreds of places stays usable
(#2990).
The selected pin now carries a white border inside a pulsing blue ring on
both renderers (static under reduced motion), and the map measures the
place card instead of assuming a fixed 320px, so the selection pans the
pin above even a tall card on portrait tablets and eases it back out when
the card grows over it later (#2991).
Closes#2990Closes#2991
* chore(capacitor): base files for capacitor
* chore(capacitor): base plugins
* feat(auth): hand a browser sign-in over to the native app
The native apps show the server's own UI in a WebView, where neither SSO nor
passkeys can run. They sign in through the system browser instead and need a
way to carry that session back.
The app keeps a random verifier and sends only its sha256 into the browser.
Once signed in there, the browser trades that challenge for a one-time code
(cookie session only, so an API or MCP token cannot mint itself a browser
session), and the WebView redeems the code together with the verifier. The
code rides through a URL scheme any app could claim, so on its own it is
worthless; it is single use and good for a minute.
Also: /api/health/features reports nativeApp so the apps can refuse servers
that predate this, and hashed files under assets/ are served immutable,
which the iOS app needs to start offline without a service worker.
* feat(native): the apps load the server's own UI
The apps ship only an address screen now. Once a server is picked, the
native side points the WebView at it: TrekBridgeViewController on iOS and a
config copy with server.url on Android. The UI then comes from that server,
in the version it runs and on its own origin, so the session cookie,
uploads, /ws and plugins work exactly as in a browser and no app release
has to keep pace with the servers out there.
The address screen (native-shell/, built into native-dist/) probes
/api/health/features through native HTTP, tries https before http when no
scheme was typed, and refuses servers without the nativeApp flag. It also
serves as the error page when the remembered server does not load: retry,
change server, or on iOS the stored copy of the server's start page.
TrekShell is the app's own plugin for all of that: remember, change and
reload the server, sign in through ASWebAuthenticationSession or a Custom
Tab, and on iOS keep and open the offline copy of the start page, because
WKWebView runs no service worker for a domain the app does not list.
Plugins the frame does not use are gone (push, camera, geolocation,
calendar, file transfer, in-app browser and more): the WebView covers the
camera and location itself, and push needs a relay before it is worth
shipping. With them go the push entitlement, the calendar usage strings
and the Firebase bits. The iOS scheme falls back to capacitor://, since
Capacitor drops an iosScheme of https, and the arm64 capability replaces
armv7.
* feat(native): the UI knows when it runs inside the app
Everything the server's UI does differently inside the iOS and Android app
lives under src/native and is inert in a browser.
Sign-in: SSO and the passkey button go through the system browser
(signInThroughBrowser), because identity providers refuse embedded
browsers and WebAuthn needs a domain association no build can have for
every server. The browser end is the new /native-handoff page: it signs
the user in if needed and hands the session over only after an explicit
tap, so a link carrying somebody else's challenge cannot pass a session
on silently. The verifier is hashed with @noble/hashes because
crypto.subtle is missing on the plain-http servers many home networks
use. An OIDC-only install no longer bounces to the IdP inside the
WebView, and passkey management is hidden in the app.
Downloads: the WebView cannot save a blob, so downloadBlob, downloadFile
and openFile write to the app cache and open the share sheet. The eight
places that built their own download anchor now use downloadBlob, which
they should have anyway.
The rest: "Change server" on the login page and in the account settings,
the splash hides after the first frame, Android's back button walks
history before it leaves the app, the status bar follows the scheme, and
on iOS the start page is refreshed and every chunk of the build (listed
in the new asset-manifest.json) is pulled into WebKit's cache so the app
can start without network.
* ci(native): build the Android and iOS shells on every change to them
Compiles the Swift side on a macOS runner so nobody needs a Mac to know it
still builds, and leaves a debug APK on every run to install on a phone.
Unsigned: signing and store uploads need secrets fork PRs do not get.
* feat(native): offline vector maps in the iOS app
In a browser the service worker answers MapLibre from the gl-map-offline
cache that glPrefetcher fills for trips marked for offline. The iOS app has
no service worker, so nothing read that cache there and the map went blank
without network.
A trek-offline:// protocol takes the service worker's place. Inside the iOS
app the MapLibre style URL is rewritten to it, the handler rewrites every
https URL inside the style and its TileJSON the same way, and then serves
documents network first and tiles, glyphs and sprites cache first. Outside
the iOS app nothing changes. The default OpenFreeMap basemap is covered;
Mapbox GL has no protocol hook and stays online-only in the app, as does a
custom raster template, whose opaque responses only a service worker can
store.
* fix(native): keep the iOS offline copy inside the app
loadSimulatedRequest went to the server URL without its trailing slash, and
Capacitor keeps a navigation in the WebView only when it starts with the
bridge's serverURL, slash included. The offline start page would have
opened in Safari instead. Also covers the platform helpers and the
hand-off API module.
* feat(native): the small things that give a WebView away
- A short haptic tap on success, warning and error toasts, which is where
every save and delete already reports. Info toasts stay silent.
- No "‹ › Done" strip above the iOS keyboard.
- No Safari preview when a link is long-pressed.
- The swipe from the left edge goes back on iOS, through the same history
Android's back button walks.
* test(mobile): the backup-code download revokes its URL after the shared delay
The mobile twin of the AccountTab test still expected the object URL to be
revoked in the same tick. Since both screens download through downloadBlob,
the revoke happens after its 100 ms grace period.
* fix(native): remember the login by default inside the app
With the switch off the app got a session cookie, which dies with the app
process, so every time the phone unloaded TREK the user had to sign in
again. Found while running the Android build in the emulator.
* fix(native): no overscroll stretch on Android
The UI asks for no overscroll effect with overscroll-behavior: none on html,
and Chrome honours it. The Android WebView draws the stretch on its root
scroller natively, though, and it scales the whole view, so the top bar and
the bottom dock were pulled along with the page. Found in the emulator.
* fix(native): Android shows "server not reachable" instead of a blank page
Capacitor injects its bridge on Android only into the server's origin once
server.url is set, so the local error page it switches to when the server
does not load had no bridge. It could neither retry nor change the server
and stayed blank.
TrekWebViewClient now catches a failed page load (a network error, or a 5xx
from a proxy while TREK restarts) and the app starts over on the bundled
address screen, where the bridge is present and the screen offers exactly
those two choices. The server config no longer carries errorPath.
Two traps on the way, both found in the emulator: a restart during launch
has to be a fresh task, since recreate() leaves the system splash lying over
the new page; and a restart shows no launch splash at all, because the
splash plugin cannot take it down again when the page hides it early.
* feat(native): TREK's own app icon and launch screen, named just TREK
The shells still carried Capacitor's placeholder icon and splash, the blue
X on a grid. They now show the white TREK mark on a dark, slightly glassy
tile: a soft light from the top left and a sheen over the upper half, in
the app's launch colour #09090b. Android 12+ draws its launch screen on the
same colour instead of light grey.
scripts/generate-app-icons.mjs renders the sources into assets/ from
public/icons/icon-white.svg, and `npm run icons:app` turns them into every
size both platforms ask for through @capacitor/assets. The Android
foreground mark is sized for the adaptive icon's round mask.
The home screen label is TREK: the old "TREK - Plan your trips!" was cut
off under the icon, and the longer title belongs to the store listings.
* fix(native): new lint warnings and the MapLibre protocol type after the dev merge
Awaits that sat in loops in the native-auth tests run together, the hand-off hook marks its navigations as ignored, the native tests drop their non-null assertions, and the offline protocol answers with the data type MapLibre 6.15 expects.
* fix(native): strict types in the address screen, Prettier on the native-auth e2e suite, file-local exports
The address screen's element helper returns the plain element instead of Object.assign's intersection, which strict mode could not represent; the haptics test reads an optional argument as optional; three exports only their own file used are local now.
* fix(native): Prettier on the native client files, rate-limit e2e requests in sequence
Formats the files the client formatting gate listed, and NATIVE-E2E-005 sends its twenty exchange attempts one after another: all at once they reset the test server's connections.
* fix(native): hide passkeys in the app through usePasskeys
The flag in useAccountSettings grew the identical destructuring in both account views past the duplication threshold; the shared passkeys hook already decides when its card is hidden, so the app check lives there now.
* feat(native): everything the App Store and Google Play submissions need
- iOS: microphone usage text for "Take Video", no-export-compliance flag,
ExportOptions.plist for App Store Connect uploads, the unused push
forwarders gone; CI builds with Xcode 26 and lints the property lists.
- Android: version and signing from Gradle properties (unsigned without
them), R8 with keep rules for TREK's and Capacitor's reflective classes,
no Auto Backup of the session cookie, no external file provider path,
template and Firebase leftovers removed, keystores and service accounts
ignored, full-size adaptive icon layers with a monochrome layer; CI also
builds the release APK.
- native-release.yml: signed bundle to a Play testing track and archive to
TestFlight, by hand or on a v* tag, behind the store environment.
- Version handshake: the server reports nativeApi 1, the app refuses a
server below its minimum and tolerates an app older than the offline copy.
- Address screen: privacy policy link, "Try the demo" and "What is a TREK
server?"; the privacy link also in the app's server settings. Three new
native strings in all 27 locales.
- Donation links and the notices asking for one are hidden in the app.
- Store listings for "TREK Travel Planner" in client/fastlane: texts, review
notes with placeholders, privacy and account deletion page drafts, the
feature graphic and icon, a length and image check in CI, and a Playwright
store project with the scripts that turn its captures into screenshots.
- Docs: wiki/Native-Apps.md, README no longer says there is no store app.
* fix(native): read a notice button's href without a weak type
An action button carries no href, so a parameter typed as only an optional href refused the notice DTO; the button type now names its kind as well.
* feat(native): a welcoming onboarding for the address screen
The app's own screen now opens on a welcome: the TREK mark, a line about what
TREK is, and two clear paths — connect the TREK you already open in the browser,
or get TREK Cloud. Both lead to polished screens: the address field with live
validation, a shake and haptic on a bad address and a success tick before the
server takes over; and a TREK Cloud intro with what it offers and a way in.
Screens slide and stagger in, buttons give a tap, and it all respects the safe
areas, light and dark, and reduced motion. Self-hosters are unaffected: cloud is
its own path, never in the way. New native strings are added across all locales
(English placeholders outside en/de, to translate).
* feat(native): a plan picker for TREK Cloud, with live prices
The Get TREK Cloud path now leads to a plan picker that reads the live plans
from the site (/api/plans), so the prices and quotas always match the website.
Monthly and yearly in a segmented toggle, the year marked with its saving, each
plan a card with its price and what it includes. Subscribe is wired to finish on
the web order for now; it becomes an Apple/Google in-app purchase once the store
products exist. New strings across all locales (en and de translated).
* feat(native): light status bar and a Go key on the address screen
The address screen sits on the near-black surface, so set the status bar to draw
its clock and icons in light while it is up; the server's own UI takes over the
style once it loads. The address field's on-screen return key now reads Go. Both
fail quietly where there is no native status bar (web, the simulator).
* i18n(native): translate the onboarding into es, fr, it, nl and pl
The welcome, TREK Cloud and plan-picker strings were English placeholders in
every locale but en and de. The five largest remaining European languages are
now translated in full; the others still carry the English text and are a
follow-up for native speakers.
* feat(native): the in-app TREK Cloud subscription flow
After picking a plan, the app now carries the whole purchase itself when the build
can take one: choose your address (checked for free as you type, against the same
endpoint the website uses), your email, and subscribe. The receipt goes to
/api/iap/redeem, and a Setting up your TREK screen waits on the new address and
loads it the moment it answers.
The purchase sits behind an IapClient seam (src/native/iap.ts): a fake drives the
whole flow in development and preview, the native client reports itself
unavailable until the store plugin and products exist, and until then the plan
picker still finishes on the web, so nothing half-wired faces a customer. New
claim and provisioning strings across all locales (en, de and the five largest
European languages translated).
* i18n(native): translate the app into the remaining 20 languages
The onboarding, plan picker, claim and provisioning strings were English
placeholders outside en, de and the five largest European languages. They are
now translated in full across the other 20 locales (ar, az, br, ca, cs, et, gr,
hu, id, ja, ko, ru, sk, sv, th, tr, uk, vi, zh, zh-TW), placeholders and brand
kept. The app now speaks every language TREK does.
* fix(i18n): add the Hebrew native strings, and correct the LoginPage size baseline
The he locale had every domain but native.ts, which broke i18n parity, the shared
coverage specs and the client parity test once the native strings existed. Add
he/native.ts with all native keys translated. Also correct the LoginPage size
baseline to its current line count.
* i18n(native): excuse the TREK Cloud brand, mark Email same-as-en, exempt the quota counts
The strict i18n checks, which the feature-branch CI does not run, catch three
things now that the native strings exist in every locale: TREK Cloud read as an
untranslated English phrase (it is a product name, like Vacay and Atlas, so add
it to the brand list); Email left identical in Italian, Indonesian and
Vietnamese, where it is the local word too (mark same-as-en) and in Greek (give
it a Greek term); and the GB and AI-document quota strings carrying {n} without
being plural groups (both are figures that never show a singular, like the other
unit-symbol counts in NOT_PLURAL).
* fix(native): typecheck, lint and knip on the native code; wire he's native bundle
The full suite, which the feature-branch CI does not run, catches four things the
native work still had: a non-narrowed union in the claim flow (tsc), a helper
named useFake that the react-hooks rule read as a hook (eslint), two iap types
exported but used only inside the file (knip), and the Hebrew native strings
present as a file but never spread into he's merged bundle (the aggregated key
parity test). All four fixed.
* fix(native): a flat redeem result, so the claim flow typechecks
The discriminated union did not narrow under the native-shell tsconfig, so the
failure code read as absent on the whole type. Use a flat result with optional
instanceUrl and code instead: both are always present on the type, no narrowing
needed, and the ok plus instanceUrl guard still routes success from refusal.
* style: prettier-format the native and i18n changes
The feature-branch CI does not run the formatting check, so the conflict-resolved
files (AboutTab, useAccountSettings, useTripExport), the native shell and iap
module, and the i18n files touched by the translation scripts were never run
through Prettier. Format them all; no behaviour changes.
* chore(native): re-trigger CI on the formatted tree
---------
Co-authored-by: Maurice <mauriceboe@icloud.com>
The packing row tests read the TREK tooltip and the accessible name instead
of the title the badges no longer carry, the journey delete suite takes its
service from the module over a snapshot db and seeds through the row
factories, the storage notifier suite builds its subject once, the POI
layer follows the view only while it has POIs, the collections repository
reads the list's places through its own query, and the baselines the moved
code left smaller come down with it.
Array.prototype.at is past the client's lib target, so the journey open-at
rule indexes the array. The packing row test expects the title that names
every bringer, a carousel test acts without a floating promise, and the
lint baseline drops to the counts this branch reaches.
Moves the Leaflet map's event bridges (click, context menu, viewport, POI drop
and the longitude-frame follower) out of MapView into leafletMapBridges, and
shares one POI drop target between the Leaflet and GL maps, so both map files
shrink instead of growing. The repair migration does its arithmetic in one
UPDATE per table, and the new tests drop their non-null assertions and loops.
The admin reset link is a component of its own instead of one made during render, the version line of the plugin panels moved into small modules, the new settings rows hand their save on as void, the date picker takes an accessible name for fields without a visible label (the Vacay hire date), the Vacay and poll tests type through the shared picker, and the theme, size and lint baselines are lowered to what the files hold now.
An iPhone or iPad running TREK from the Home Screen ignores print(), in
the page and in an iframe alike, so "Save as PDF" in the preview did
nothing there. Mobile Safari prints the same preview fine.
In the installed iOS app the preview now shows "Open in Safari" in
place of the dead button, with a line saying why. The link opens the
trip with ?export=pdf, and both shells open their export dialog or
sheet for it, so the PDF is one tap away once Safari is up. The
parameter is dropped once read and survives the login redirect.
Everywhere else the preview prints as before.
The preview overlay moves out of TripPDF.tsx into pdfPreview.ts, which
TripPDF had no room left for under lint:size, and its backdrop and
stacking now use the --overlay and --z-modal tokens.
- MAdminOidcCard is markup over the settings section's state, so it is
listed under mobileOnly with that reason.
- The default user settings views take their presets from
useDefaultUserSettings now and import only a type from mapSettingsModel,
so that pair names the map settings views alone.
- The owner sign-in and the photo provider the suite runs with move into
tests/helpers/journeyDetail.ts, so the page test is back under its line
baseline, which drops to 4028.
- Drop help/registry.base.ts, an unused copy of the help registry whose key
builders the i18n key check could not resolve.
Desktop and phone show what the last check found (can be freed, unused for
less than a week, freed), why a category was skipped, the switch that lets
the nightly run delete and a report-only check on demand, through the
shared storage panel hook. All 28 languages, and the Admin-Storage wiki
page explains the check, the week's wait and when to leave it off.