A page or a link shared from another app now carries its address. The app asks whether to keep it among the trip's links (Collab, Links) or to read a booking from it. A link whose page the phone cannot read is no longer refused: it is handed over as the link alone, which can be kept. With Collab off a page is read as a booking, as before.
The watch's snapshot was rebuilt every fifteen minutes, so a tick in the app or one arriving over the WebSocket did not reach the watch until then. It is now rebuilt as soon as the packing lists or the to-dos change in the offline tables. A tick on the watch goes as a message while the phone is in reach, which wakes the phone app, and a phone app in the background writes it to the server itself, so the trip's other devices hear of it at once instead of when the phone app is next opened.
The licence testers of the internal test buy from a release build, so the
plan screen no longer hides the purchase on Android before the opening.
Their purchases are Play test purchases, which the cloud serves. A real
purchase made before the opening is refused with ordering-closed and never
acknowledged, so Google Play refunds it after three days; the claim and the
restore now say that instead of a general error. The app still acknowledges
a purchase only after a redeem that returned an instance. iOS is unchanged.
The Android shell now reports keyCommands and windowLayout, so the large
screen layout no longer assumes an iPad:
- The keyboard host hands the shortcuts to the app whenever the shell has
the keyCommands feature, whatever the platform. Before it also required
iOS, so an Android tablet with a keyboard answered the keys in the page
and never listed them in the system's shortcuts.
- useWindowFold follows the layout the Android shell reports (feature
windowLayout). When a vertical fold separates the window, a foldable half
open like a book, MobileShell marks it with data-fold and its place, and
the leading pane (the trip's plan, a tab's summary, a list beside its
detail) ends at the fold while the trailing one starts there, on either
reading direction. A flat fold that does not separate stays one screen.
- The split size class starts at 840pt, Android's expanded width, instead
of 900pt, so a foldable unfolded on its side (about 841dp) gets two panes
like an iPad does. An 11" iPad upright (834pt) stays a single pane. The
address screen of the native shell follows the same width.
- TrekShell exports NativeWindowLayout for it.
Also: the sheet's width and anchor types and the collab board type stay
private to their modules, which nothing else imports (knip).
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.
The complication timeline has entries for every planned day in its window
and rebuilds at midnight; its countdowns count by themselves. Times of
day are wall-clock hours and minutes, never minutes added to midnight,
and every formatter follows the zone the watch is in. A ride ends in its
arrival's zone, which the snapshot now carries. Bookings without a time
are all-day and get no countdown, a day of untimed places shows its
first place instead of "day done", idle complications say what fits the
trip's phase. The watch finishes WatchConnectivity deliveries in a
background task, sends a tick to the phone only until it is handed over,
and keeps nothing in UserDefaults, so its bundles use no API that needs a
privacy declaration.
The hero trip goes to the watch in full: every day with its stops and
coordinates, the bookings with reference, seat, platform and both ends,
the packing list and to-dos with ids. Ticks from the watch come back
through TrekShell.takeWatchActions and are written through the trip
store's toggles, so they take the repos and the mutation queue like a tap
in the app.
- wallet.ts: the Google Wallet state is read through a function after it was
set, so TypeScript no longer narrows it to its old value (TS2367)
- iap.ts: a store listener that cannot be added is reported through
reportNativeFailure instead of an empty catch
- iap.test.ts: the restored purchase is checked instead of asserted non-null
- trekShell.ts: the fold's rectangle is typed as a DOMRect pick, geometry the
RTL lint does not read as text sides
- The location shim runs at high accuracy only while a watcher wants it and
drops back once that watcher is gone, instead of keeping the 2 s GPS rate
for the rest of the session.
- The purchase client can hand over a Play subscription that was paid but
never delivered (a pending payment that cleared later), and the plans screen
redeems it at once, as a restore would, before Google Play refunds it after
three days.
The Android app asks the server once whether Google Wallet works there and
offers the button only after it says yes; a 404 from the pass route takes the
button away again. The wallet button's test mocks the new exports and covers
the Google case.
The new methods sit at the end of the interface and the restore's Android
answer has a type of its own, so the watch and large-screen branches, which
add their methods after storeRestore, merge without a conflict.
The UI now turns these on by feature on Android as well as iOS:
- widgets, widgetTrips and the travel day (liveActivity) write the same
snapshot to the Android app's Glance widgets; the travel day also sends
the next stop's minutes and the stops' colours for the Live Update.
- geolocation answers navigator.geolocation from the app's location stream.
- tileCache: Android keeps Mapbox's resources in its WebView client, so the
map is offline-capable there too, without the iOS trek-tile:// rewrite.
- playBilling: the purchase client buys through Google Play on Android,
with the purchase token as the receipt the control plane verifies.
- googleWallet: a booking's button adds it to Google Wallet on Android,
through the save link the server asks the TREK services for.
- The claim screen says, right above the buy button, that the TREK Cloud
terms govern the instance and links them (liketrek.com/terms?app=1, the
view without the site's order buttons); the plan screen links them too,
next to Apple's EULA, which is now named as Apple's.
- On Android the payment note and the empty restore say Google account and
Google Play's own path, not Apple ID and App Store.
- The redeem sends the app's language, so the contract confirmation mail
arrives in it.
- Orders closed at the cloud: the app shows the plans and that TREK Cloud
opens soon, and sells only in a test build (TestFlight, App Review), whose
sandbox purchases the cloud still sets up; a store purchase it would refuse
is never offered. The plans arrive without prices then, the store's own are
shown where it gives them.
- A purchase paid but never set up (Ask to Buy approved later, the app closed
mid-purchase) is picked up by Restore purchases and goes on to the claim with
that purchase, instead of failing; nothing is bought twice.
- The buy button names the period with the price (guideline 3.1.2).
- The privacy link opens the policy without the site's sales header.
- The privacy manifest declares what TREK Cloud keeps: email, name, the store
purchase, photos, other user content and saved places, all for the app to
work, linked to the account, never for tracking.
- The email hint promises messages about the contract, not receipts, which the
store sends.
"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
Opening a .pkpass in the iOS app brought up the generic share sheet. It now
shows Wallet's own sheet with the pass and "Add", and a pass that is already in
Wallet opens there instead of being offered twice (TrekShell.addToWallet,
feature wallet). A broken pass or a device without Wallet falls back to the
share sheet as before.
Passes carry an "Apple Wallet" badge in the trip's files and a wallet icon
where they hang on a booking, so it is clear what a tap does.
A share extension puts TREK in the share sheet of Mail, Safari, Files, Wallet
and any app that shares a document, a link, text or a photo. It shows what came
in (a Wallet pass, a PDF, a mail, the page as Safari rendered it, text, a photo)
and the user's trips with their covers, the next one already picked, and hands
both to the app, which opens that trip's booking import with the file in place.
"Choose later" leaves the trip question to the app's import sheet as before.
- ShareExtension target (add-extension.rb learns "share": JavaScript and the
localizations go in as resources), bundle com.liketrek.trek.share, signed in
the release lane, its plists linted in native.yml
- Safari hands over the rendered page (SharePage.js), so a confirmation behind
a sign-in arrives whole; other apps' links are fetched
- The item travels through the shared keychain group the widgets already use;
the app takes it on com.liketrek.trek://share or when it next becomes active
- Feature shareSheet; strings in the app's 28 languages
The plan picker sent buyers to the website, which the App Store does not allow.
It now sells through StoreKit 2 in the TrekShell plugin (feature storeKit):
App Store price strings in the buyer's currency, the purchase sheet, the signed
transaction redeemed at the control plane, and the transaction finished only
once the instance exists. A purchase whose redeem failed is redeemed again on
retry, never bought twice.
Restore purchases, the auto-renewal terms and the links to the terms of use and
the privacy policy sit under the plans. Where no store purchase is possible
(Android, an older binary) the picker shows the plans with no way to buy and no
pointer to the web.
- 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
Widgets followed the dashboard's trip only, which is the one under way, so a
packing list or to-dos for a trip still being planned could not sit on the
home screen. The trip, today, bookings, to-dos, packing and group widgets now
take a trip in Edit Widget (an AppIntent with the trips running or ahead,
Automatic by default, which keeps following the dashboard); the world map
needs none.
- The app writes a snapshot per trip running or ahead (at most six, the
dashboard's order) and the list to pick from, under their own keys
(feature widgetTrips, so an older binary never mistakes one for the
dashboard's); bags, chat and polls are kept ten minutes, covers per trip
- The widget extension targets iOS 17, which configurable widgets need; the
apps are not released, so no installed widget goes away
- The choices and their texts in the 28 languages of the gallery
- The entry carousel draws the card at the centre larger while the swipe is
still moving (focusIndex, a transform), and the active card and the map
follow 90 ms after it stops instead of 150; the grow takes 200 ms
- The journey map's zoom buttons set their display inline, which beat the
class that hides them on touch screens; the display is a class now (#3001)
- The packing widget data no longer carries a colour literal
- Packing: a ring of what is in the bags, each bag against its weight limit,
the items still missing in their bag's colour, a nudge when departure is
two days or less away
- World map: the world as dots from the Atlas country shapes
(WorldGrid.swift, generated by WidgetPreviews/make-world-grid.py), the
visited countries glowing, the latest one ringed, continents as bars and
the latest countries with their flags; it needs no trip
- Travel group: who comes along as avatars, the latest chat messages as
bubbles and the open poll with its votes as bars
- The to-dos widget is about to-dos only; the packing ring moved out
The app sends the bags, the Atlas countries (an hour's cache) and the chat
and poll with the snapshot; all optional, as before.
With several trips running at once, the widgets took whichever came first
on the device, so an old test trip spanning today pushed out the journey
under way, and the today widget counted its days. The dashboard's choice
moves to utils/heroTrip.ts (running first, earliest start, else the next to
come) and the widgets ask the same function, so the home screen always
shows the trip the app opens on.
Four widgets instead of one plain text block, drawn in the phone tokens
(warm paper by day, near black by night) with contour lines and a route arc
as the travel motif:
- Trip (small, medium, large, lock screen): the cover photo, a countdown ring
and a plane on its arc before the trip; the day of the trip as segments,
the weather and the next stops on frosted glass while it runs
- Today (medium, large): the day as a timeline in its category colours, with
a now line, the day's progress and the weather
- Bookings (small, medium, large, lock screen): flights, trains, hotels and
tables ahead as coloured tiles, the next one featured, status dots
- To-dos (small, medium, large, lock screen): to-dos and packing as nested
rings, the most pressing open to-dos with priority and due dates
The app now sends the destination, a small cover JPEG, the weather where the
trip is, the bookings ahead, the open to-dos and the packing count; every
new field is optional on both sides, so older binaries and servers keep
working. Timelines step every quarter hour so the now line moves without
the app. client/ios/WidgetPreviews renders every widget in every size, light
and dark, on the CI's Mac, uploaded as the widget-previews artifact.
- 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
The iOS app pulls every file of a build into WebKit's cache so it can start
offline. It did so four at a time at normal priority, for all 1,900 files of
every new build including all 27 translations, and a page opened meanwhile
queued behind it: right after a server update, a first visit to Vacay took
five to seven seconds. The warm-up now runs two at a time at low priority,
takes only the user's language and English, and walks a build once.
The day rail in the trip header lit the new day only after the pager's snap
had landed and the plan had re-rendered, a second or two after the swipe.
The pager now reports the day a swipe heads for as soon as its page is the
nearer one, and the rail and its pill move with the finger; the plan still
switches when the snap lands.
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.
The Android app now subscribes like the iOS one: TrekShell.registerPush asks
for the notification permission (Android 13+), creates the device's P-256
Web Push keys and returns the relay endpoint for its FCM token
(push.liketrek.com/v1/fcm/<token>). TrekPushService receives the relay's
data message, decrypts the record (RFC 8291, TrekWebPush) and shows the
notification. A tap opens the linked page, also when it starts the app.
The keys are sealed with an AES key kept in the Android Keystore. Firebase
is configured from res/values/firebase.xml, so no google-services plugin is
needed. The decryption is tested against the RFC 8291 example, and the
native workflow now runs the Android unit tests. On the web side push is no
longer limited to iOS: it follows the nativePush capability on both platforms.
A WidgetKit extension (TREKWidgets) with two parts: the next trip on the home
screen, with a countdown before it starts and today's day and next stops
while it runs, and a Live Activity for the travel day on the lock screen and
in the Dynamic Island showing the next timed stop and the day's progress.
The web app builds both from the trips stored offline on the device
(native/widgets.ts) and hands them over through TrekShell
(setWidgetSnapshot, syncTripDay, endTripDay). The texts are already
translated, so the widget needs no localisation of its own. The snapshot
lives in the keychain group the app shares with its extensions, so no App
Group is needed. Taps open com.liketrek.trek://navigate/<path>.
NativeWidgetsSync refreshes on start, on returning to the foreground and every
quarter hour. Both parts are gated behind the widgets / liveActivity
capabilities, so older binaries and the PWA are unaffected.
The app subscribes to Web Push like a browser, only the app makes the
subscription: it asks for permission, registers with APNs and keeps its own
P-256 key pair and auth secret in a keychain group a notification extension
can share. The endpoint is liketrek's relay for this device's token, so the
server encrypts exactly as for a browser and nobody on the way can read the
notification. The settings card, the test push, sign-in resync and the logout
teardown all take this path where the binary has nativePush; a tapped
notification opens the place it points at, also from a cold start.
The App ID has push enabled, the app carries the aps-environment and
keychain-group entitlements.
TREK now appears under "Open in" and in the share sheet for PDFs, calendar
entries, Wallet passes and mails (iOS document types, Android VIEW and SEND
filters). The native side reads the file (up to 15 MB) and hands it to the UI,
which asks which trip it belongs to, upcoming trips first, then opens that
trip's booking import with the file already chosen.
WKWebView asks for the location per server origin, under the server's name,
and again across launches. Binaries with the geolocation feature answer
navigator.geolocation from CoreLocation instead: one When-In-Use prompt as
the app, a stream that runs only while something listens, at the accuracy
the most demanding caller asked for. Every caller keeps the browser API.
A tap on the status bar now scrolls the list in the middle of the screen
back to the top, as in every iOS app.
A switch in the account settings, inside the app on a device that can unlock
that way (iOS LocalAuthentication, Android BiometricPrompt with the screen
lock as fallback). Turning it on asks once, so nobody locks themselves out.
With it on, TREK asks at launch and after half a minute in the background
before showing anything, and the app switcher shows a blurred cover (iOS) or
no snapshot (Android) instead of the screen. The setting stays on the
device, not the account.
WKWebView runs no service worker for a self-hosted server, so on iOS nothing
kept a map tile once it scrolled away. Binaries with the tileCache feature
answer a trek-tile:// scheme from a store on disk: a tile viewed once is
served from there for up to 30 days (Mapbox's limit for a device cache,
applied to every provider), then fetched anew; the store keeps under 250 MB,
dropping the least recently used tiles first, sends a unique app User-Agent,
and never prefetches. Mapbox's own resources (style, tiles, glyphs, sprites)
and Leaflet's raster tiles go through it, telemetry never does.
Offline, a Mapbox map in such an app now stays Mapbox where it was viewed,
and the pill offers the stored trip region instead. The offline settings
show what the store takes against its limit, with a way to empty it.
WebKit's swipe from the left edge slid away a screenshot of the whole page,
closed no sheet and took every drag near the edge from the UI. Binaries from
this one turn it off and report an edgeBack feature, and the UI draws its own:
the page follows the finger, a release past a third or a flick slides it off
and goes back, anything else springs it home, with a light detent.
Open sheets now register in one overlay stack, so a way back closes the one on
top first, through its own close path (unsaved changes still ask): the edge
swipe on iOS and Android's back gesture alike.
The UI ships with the server and the binary through the stores, so the two
drift. TrekShell.getCapabilities now reports the binary's shell API, version
and native features by name (iOS and Android), and the UI turns each native
feature on only where the binary has it; a binary from before the handshake
counts as shell API 1 with none. The other way round, the server names the
oldest binary its UI works with (minNativeShellApi), and an app below it is
asked to update with a link to its store.
The day swipe only rubber-banded the current day, then cut to the next one
after a fixed slide and a synchronous re-render of the whole planner. The
previous, current and next day now sit side by side in a horizontal
scroll-snap strip, so the finger drags the neighbour in, and momentum,
edge bounce and axis locking are the browser's own. The day changes once
the strip comes to rest, with a light haptic tick in the app.
The pager is one building block, MDayPager, used by the plan timeline and
the road trip stages alike. Each page renders its own day through the
timeline and stage hooks, which now take the day as a parameter, and keeps
its state when it slides into the middle.
* 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>