A watchOS app embedded in the iPhone app: today's next stop with its
countdown, the next booking with reference, seat and platform, the trip
with its days and places (Directions into Apple Maps), and the packing
list and to-dos to tick off. The phone sends the snapshot through
WatchConnectivity; ticks go back as queued user info. Three
complications (next stop, countdown, trip day) in every accessory family,
scored for the Smart Stack. 28 languages.
The native workflow captures every screen from watchOS simulators in a
screenshot mode, on the largest and the smallest watch.
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
- Tile store: the WebView's headers go along (a URL-restricted Mapbox token
is checked against the Referer), and anything but a tile is left to the
WebView, so a 401/403 never comes from the store.
- Widgets read their snapshot inside the composition, so every write reaches
a running Glance session; the trip picker updates only the widget the id
belongs to.
- Location: Approximate counts as allowed; the stream pauses with the app,
resumes with it, and stops when the page reloads or the activity goes.
- Billing: an already-owned subscription comes back as the purchase, so the
shell redeems it; a payment that clears outside the sheet is announced
(storeTransaction) and found again at start (storeUnfinished).
- getWindowLayout answers with the last posture and hinge, not the bare window.
- Share sheet: keeps the picked trip through rotation and folding, and takes
content:// only, never a file:// path read with TREK's own rights.
- 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.
Android lint counts every string without a translation once the app has
languages; the app name and the URL scheme are names, not text. Removing
location updates is guarded for a permission revoked meanwhile.
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.
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.
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.
It now matches the App Privacy answers in App Store Connect: user id, device id
(the push token for the relay) and emails or text messages (group chat, imported
booking mails), next to email, name, purchases, photos, other user content and
precise location; all for app functionality, linked, never for tracking.
- 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.
- TREK_WALLET_PASS_URL read as a secret to the env check (the word PASS); it
is TREK_WALLET_SIGNER_URL now, everywhere it is documented
- findPassContext compared the TEXT accommodation_id with an integer id, which
Postgres refuses; it compares them as numbers now, as both databases accept
- the wallet-pass service test takes its subject from createTestModule instead
of building it by hand, and no longer names the DOM-only BodyInit type
- Reservations.repository.ts is formatted, so it leaves the format baseline
- the wallet button test reads the translated texts the render helper gives
"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.
vi.mock is hoisted above the file's imports, so the managed-tab mock could not
use the icons and JSX imported at the top (ReferenceError before initialization).
The trip PDF's print view linked Poppins from fonts.googleapis.com, so printing
a trip sent the reader's IP address to Google, the case German courts treated as
a GDPR breach (LG München I, 3 O 17493/20). It now declares the Poppins files
TREK already ships under /fonts, and the content security policy no longer
allows fonts.googleapis.com or fonts.gstatic.com, so no font can be fetched
from Google again by accident.
The mobile admin never read managedAdminTabs, so a tab an install attaches
there (managed/index.tsx) existed on the desktop only. The phone's section
picker now lists them after the built-in sections, as the desktop sidebar does,
and shows `mobileElement` when a tab brings a phone layout of its own, else its
desktop `element`. TREK itself attaches nothing; the seam stays empty.
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.
components/ may not import from mobile/ (lint:layers). The phone dashboard
mounts the sheet itself, as it already mounted the welcome, and the desktop
dialog no longer asks which shell it is in.
On a phone the head took half the screen, the list scrolled in a small window
and the start button broke over two lines next to the links. The phone now gets
a bottom sheet of the phone shell: a small head with the countdown, the same
content as a list, a full-width start button with the two links under it. The
photo credits open in its place and back, as on the desktop.
- 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
The journal searched the full text only, which for a hotel in Osaka listed
the Japanese names the index holds. It now asks the planner's typed-ahead
search first (usePlaceSuggestions, the user's language, one billed session
per search), shows each place with where it is, and takes its point from the
details once picked; the full search still answers when there are no
suggestions. (#3003)
The track was w-10, which is rem and followed the text size setting, while
the knob and its travel are px; with a smaller text size the knob hung over
the end of the track. The track is 40 px now.
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.
- On a phone the shared journey's map stopped at the safe area, so Safari's
toolbar sat over a bare strip; it now runs to the edge (#3002)
- The shared journey on desktop gains the journal's day picker, every day and
its entries one click away (#2998)
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.
The active card grew its box from 136 to 164 px once the swipe had settled,
which moved every card after it and made the snap pull the strip a second
time. Every card now keeps the same box and the active one is drawn larger
with a transform, so nothing moves; the strips keep the room with a wider gap
and top padding. Where the browser reports scrollend, the card grows the
moment the snap lands instead of a beat later.
TestFlight builds now carry the release tag's message as What to Test.
- Names and descriptions of the four widgets in TREKWidgets/<lang>.lproj for
the 28 languages the app speaks, linted with the other property lists
- The preview renderer draws a cover photo state too, and the lock-screen
accessories as the system does, without a box of their own
- The large trip and today widgets put the stops right under the day and the
next booking at their foot instead of an empty middle; the large to-dos
widget names its two rings with their counts
- The trip and day for a travel day as a Live Activity: a progress ring, the
next stop large, the stops as segments, the Dynamic Island in the same green
- The place shows with a location arrow, and rows link through RowLink, which
the preview renderer draws without the Link it cannot render
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.
The days are a scroll-snap pager since the native UX round: a page turns once
the finger has carried it past half its width. The spec swiped 180 px on a
412 px phone and read the chip right after; it now swipes 40 % either side of
the middle and polls the chip until the snap and the selection have landed.