Commit Graph

42 Commits

Author SHA1 Message Date
Maurice 52f0fb882f feat(native): redesign the app's first screens around a living landscape
The screens the app ships itself (welcome, plans, address, own server and
the states between) move from the dark card layout to a light design on an
animated aerial landscape drawn by a WebGL shader (native-shell/scenery.ts):
an island, a coastal town, farmland and mountains in one world.

- Welcome: the landscape with the TREK wordmark, three perks in one card,
  "Explore cloud plans" and "I already have a TREK" as two buttons.
- Plans: one tab per plan; choosing one flies the camera to its place,
  further inland with every plan. Each plan reads as a kind of trip, the
  price sits in a card with a monthly/yearly switch and counts to its new
  amount in the store's own format (native-shell/amount.ts), and a yearly
  price shows what it makes a month. Restore purchases, the renewal terms
  and the links stay as the store rules require.
- Address: the address writes itself on a glass card over the landscape;
  free, the password rules and the agreement sit on white cards.
- Own server: the server address on the glass card; a failed check turns
  the card red with what went wrong, proxy sign-in and client certificate
  panels in the same style.
- States: server not reachable, certificate needed, already subscribed,
  setting up (with its steps, then "Arrived") and the slow setup.
- iPad, an unfolded phone and a phone on its side get two columns, the
  landscape beside the content; on a dual-screen device each half takes
  one screen. Arabic and Hebrew mirror through logical sides only.
- The shader caps the pixel ratio at 2, drops to a lower resolution on a
  slow device, stops in the background and is one still frame with
  reduced motion.
- The sign-in and error page bars over a proxy's page (iOS and Android)
  take the same look: a white card and a dark button.
- Plus Jakarta Sans is bundled for the text, with Montserrat standing in
  for Cyrillic; the wordmark fonts and the old preview images are gone.
- New strings in all 28 languages; the keys of the old screens removed.
- The dev sample plans follow the cloud's plans (start, basis, plus, pro).
2026-11-12 14:18:42 +01:00
Maurice a1d3ffc2ce feat(native): the 'server not reachable' screen names the cause, small, for a report
A load that failed sent the app to the address screen with a general message only, and a tester's screenshot told nothing. The web view's own error (net::ERR_..., an HTTP status, NSURLErrorDomain and its code, a client certificate the proxy asked for) is kept natively and shown under the message once.
2026-11-12 10:11:01 +01:00
Maurice 46b9df54bf feat(native): the TREK Cloud screens show the app and make the choice of a plan easy
The cloud screen shows real screens of the app. The plans are a choice now: one tap marks a plan, the recommended one is marked, what every plan has is said once above them, storage says how many photos it holds, and the yearly switch shows what a year saves; one button at the foot carries the chosen plan on. The claim screen names the plan with a way to change it, shows the address as it will be once it is free, ticks off the password rules while typing, says in three words where the servers are, that it can be cancelled and that there are no ads, and puts the photo behind the form away.
2026-11-12 10:04:31 +01:00
Maurice 3c0c3b3f6e fix(native): a store account with a running TREK Cloud subscription buys no second one
One subscription per Apple ID or Google account. Choosing another plan in the app changed the running subscription on the App Store, and the redeem then failed with 'We could not set up your TREK'. The app now asks the store first and shows a screen of its own: the account already has TREK Cloud, a new one can be taken out once it has ended, with Open my TREK and the store's page to manage it. The native purchase refuses as well; a paid one that was never set up is still redeemed. On the address screens the link 'What is a TREK server?' is gone, and the small print under a purchase is set in the system text face at regular weight.
2026-11-12 09:28:56 +01:00
Maurice cc5eca5553 feat(native): a client certificate can be added on the address screen before connecting
The import was only offered once the check saw the proxy ask for a certificate during the handshake. A proxy that refuses without asking left no way in. The wiki also points to the certificate's new place in Settings and explains why a certificate from an iOS configuration profile cannot be used by the app.
2026-11-12 08:42:16 +01:00
Maurice e0022c12c4 fix(native): an error page in place of the server's keeps a way back to the address screen
A proxy's block page or a refused client certificate (Cloudflare's "Sorry, you have been blocked") left the app on a page without TREK's UI and no way to change the server. Any 4xx main-frame page that the client-certificate path does not handle now shows the sign-in bar with its own hint and "Change server", on iOS and Android. TestFlight feedback, 5.1.2.
2026-11-11 20:41:24 +00:00
Maurice 66b8f2c906 feat(native): ask for the separate TREK Cloud deviations agreement before an in-app purchase
The claim screen loads the agreement text and its version from /api/iap/consent, shows it with its own checkbox and buys nothing until it is ticked. The redeem sends consent { acceptedDeviations, version }; a refusal for an outdated text reloads it and asks again without a second purchase.
2026-11-11 20:41:23 +00:00
Maurice 0b3d2873c2 feat(native): a proxy's sign-in page in the app has a way out and says what cannot work there
Behind Pangolin or authentik the app shows the proxy's sign-in page itself, and
nothing on it led back to the address screen: a failed passkey left users stuck
on every launch. A bar over that page now offers to change the server and says
that passkeys and security keys do not work there, as a web view cannot use them
for a domain the app is not tied to. It steps aside while the keyboard is up.
On iOS the page also gets the room the keyboard takes, so a button under it can
be scrolled into view. The address screen hands the bar its words in the user's
language when it connects.
2026-11-10 09:36:50 +01:00
Maurice 5cbe4e2fa3 feat(native): the app needs TREK 5.1.3 on the server and checks a remembered one once after the update
The fixes that keep the app's sign-in in the app live in the server's UI,
so an updated app on a 5.1.2 server still ended in the browser. The server now
reports nativeApi 2 and the app refuses anything below it, naming 5.1.3.

A server the app remembers from before is not loaded unchecked: the first
launch after the update shows the address screen filled in with it, says why
and checks it right away. A server that is new enough loads again with its
session; an older one gets the update hint.
2026-11-10 09:17:26 +01:00
Maurice 07448e662c feat(native): client certificates for servers behind mutual TLS
A proxy in front of TREK (Cloudflare, nginx, Caddy, Traefik, HAProxy) can
let in only devices that show a client certificate. The apps could not, so
such a server was unusable from them.

- Address screen: the server check runs natively and notices when the
  server asks for a certificate. Instead of "no server answered" it says so
  and offers to add a .p12/.pfx file; a wrong password asks again, an
  expired, not yet valid or keyless file is named as such.
- The certificate is shown only to the host and port it was added for.
  iOS keeps it in the keychain (this device only), Android seals the key
  with an Android Keystore key. The file's password is never stored.
- The web view answers the server's certificate request on both platforms;
  WebSocket, service worker and API requests go through with it. The
  offline copy, watch requests and the check use it too.
- When the server stops letting the app in (certificate removed, expired
  or no longer trusted) while the page still comes from the service worker
  or the offline copy, a native check sends the app to the address screen,
  which offers to add another. A server that only asks and lets everyone
  in is left alone.
- Cloudflare-style 403 and nginx-style 400 refusals without a certificate
  lead to the same screen instead of staying up as an error page.
- Settings, Account: a Client certificate card (only with one stored)
  shows subject, issuer, server and expiry, with Replace and Remove.
- Wiki: Native-Apps "Client certificates (mTLS)" with Cloudflare, nginx,
  Caddy and Traefik setup and how to make a certificate for the app.
- The app icon on the unreachable screen is black on the light surface.
2026-11-09 12:03:31 +01:00
Maurice 3a66f2a8da fix(native): the keyboard on the claim screen closes again
On "Almost there" nothing put the keyboard away: the web view keeps it open on
a tap beside the fields, and "Next" sent the half-filled form, whose check
focused a field again. A tap beside the fields or a drag of the screen now
closes it, as in an iOS app, on every first screen. "Next" walks from the
address to the email to the password, "Go" buys, and the keyboard goes as the
purchase starts.
2026-11-09 10:51:20 +01:00
Maurice 7f52e424e8 fix(native): a double tap on the app's first screens no longer zooms them on iOS 2026-11-09 10:49:57 +01:00
Maurice b3113e66b9 feat(native): buying TREK Cloud in the app asks for the password of the new TREK and opens it signed in
The claim screen takes the password (TREK's policy, checked before anything is
bought) and sends it with the purchase; the control plane answers with a
one-time welcome code, which the app hands to setServer. iOS and Android open
the instance's welcome page with it once after the first page, which signs the
buyer in. Without the code (a restore, an older control plane) the app opens
the instance as before.
2026-11-08 23:43:15 +00:00
Maurice 517efabbca feat(native): the address screen recognises a sign-in proxy in front of TREK and signs in to it inside the app, so its session works as in a browser 2026-11-08 20:44:26 +01:00
Maurice b16ac683cf fix(native): Android's back gesture steps back through the first screens, the login's language picker clears the status bar, and a slow trip open shows its splash sooner 2026-11-08 16:51:28 +01:00
Maurice 4ab42c06c9 fix(native): spell out the welcome screen's translucent mixes, which older Android web views drew opaque as halos and rings 2026-11-08 15:07:41 +01:00
Maurice 9f6ab11839 fix(native): no logo shadow on the Android welcome screen, where the web view drew it as a dark halo 2026-11-08 14:36:12 +01:00
Maurice dedbf16f03 fix(native): Android offers TREK Cloud while orders are closed and says a refused purchase is refunded
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.
2026-11-08 02:31:13 +00:00
Maurice 2a6c771ad9 fix(native): the first screens fit a phone on its side, fill an upright iPad, and read right in Arabic and Russian
On a phone or a folded foldable turned landscape the welcome, address,
TREK Cloud and plan screens were taller than the glass: the second choice
and the main button sat below the edge and Back was cut at the top. There
the explanation and the choices now stand side by side, the explanation
pinned while a long plan list scrolls. Every screen centres while it fits
and starts at the top when it does not.

Upright on an iPad, half of one in Split View and an unfolded foldable,
the photo spans the full width and fades only downwards, and the choices
stand centred in the room below it. Buttons and fields are pills like the
app's, choice cards take the app's card radius, and headings and subtitles
balance their lines.

In Arabic (and Hebrew, which never ran right to left here) chevrons point
along the reading direction and sit at the far edge of their card, the
Cloud lockup stays unmirrored on the leading edge, prices and addresses
read left to right, and a plan's period no longer touches its price. The
slides follow the reading direction. Russian gets Cyrillic stand-ins under
the same family names, so the headline keeps a rounded face.
2026-11-07 23:25:15 +01:00
Maurice bb725ac8d8 fix(native): the first screens on an upright iPad read as one page, not a phone column
On an iPad upright or an unfolded foldable the welcome, address, TREK Cloud
and plan screens kept the phone's narrow column in the middle of the glass,
with the photo fading out across the top left. Now the photo spans the
width as a band across the top, the choices stand under it in a wider
column, and the plans take two columns. Landscape keeps the photo as a
panel beside the choices; phones are unchanged.
2026-11-07 23:25:14 +01:00
Maurice 6906f80ff8 feat(native): Android tablets get the shortcuts, two panes meet at a foldable's fold
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).
2026-11-07 23:25:13 +01:00
Maurice 7083667cbf fix(mobile): grid cards keep their height, a previewed photo its proportions
Cards in the large-screen grid keep their own height instead of stretching
to a taller neighbour in their row, which drew a short booking as an empty
panel. The file preview beside the grid shows a photo at its own
proportions rather than letterboxed in a portrait frame.

On a 13" iPad on its side the cloud plans of the native shell stand two by
two in their half of the screen, so the whole offer is in view at once.
2026-11-07 23:25:12 +01:00
Maurice 0b7cee0b0d feat(mobile): the trip's tabs, administration, collections and the journal use the large screen
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.
2026-11-07 23:25:12 +01:00
Maurice 46f4cab1b7 fix(native): location at the accuracy asked for, and a Play payment that clears later is redeemed
- 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.
2026-11-07 14:57:50 +01:00
Maurice e1a08e82b9 fix(native): name our terms before the purchase and speak Google Play on Android
- 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.
2026-11-07 12:22:34 +01:00
Maurice 6e1c9e8e9f fix(native): App Review findings on buying TREK Cloud in the app
- 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.
2026-11-07 11:48:03 +01:00
Maurice 5462bce9c3 feat(native): TREK Cloud subscriptions through the App Store
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.
2026-11-07 11:25:17 +01:00
Maurice d18fb7f63d fix(native): drop the unused yearly-savings string and a dead initial value
The plan picker no longer shows a yearly badge, so its string reached nothing
and failed the i18n key check; the shell's start also initialised a value it
always overwrote.
2026-11-06 15:02:47 +01:00
Maurice 954d419671 feat(native): show the photo and a visible grid on the light welcome screen
The light scheme dropped the Kyoto band and drew the grid so faint it
vanished, leaving a bare white page. The photo now stays in both schemes
under a veil in the page colour (a dark dusk, or a bright haze in light
mode so the black wordmark still reads), and the light grid and wash are
strong enough to see.
2026-11-06 15:02:47 +01:00
Maurice 5135052446 feat(native): fix the claim fields into one address box, add placeholders, shorter fields, better title 2026-11-06 15:02:46 +01:00
Maurice f9e1dfe804 feat(native): equal plan cards, mark absent features with a dash, one-line unlimited label 2026-11-06 15:02:46 +01:00
Maurice bd42661a62 fix(native): more space between price and its per-period suffix 2026-11-06 15:02:46 +01:00
Maurice 3b4d220936 feat(native): sample plans in dev preview, drop the yearly badge, plain white primary, readable subtext 2026-11-06 15:02:46 +01:00
Maurice e3ac025a59 feat(native): drop the demo button from the connect screen 2026-11-06 15:02:45 +01:00
Maurice c04864f693 feat(native): TREK Cloud wordmark lockup on the cloud screen, fix a German string 2026-11-06 15:02:45 +01:00
Maurice a9b05bba15 feat(native): make TREK Cloud the primary action, shorter calmer subtext 2026-11-06 15:02:45 +01:00
Maurice 52ddf9b4f7 feat(native): blend the photo into the grid left-to-right, slimmer demo button 2026-11-06 15:02:44 +01:00
Maurice 61a7b5bceb feat(native): set the welcome headline in lowercase MuseoModerno, full-width demo button 2026-11-06 15:02:44 +01:00
Maurice 70cfe9bfb0 feat(native): Kyoto hero band, headline tagline, tidy demo and footer 2026-11-06 15:02:44 +01:00
Maurice b63f1f01b5 feat(native): use the full TREK wordmark on the welcome screen, per scheme 2026-11-06 15:02:43 +01:00
Maurice e9d9690844 feat(native): richer onboarding — Poppins display type, a masked grid backdrop and depth 2026-11-06 15:02:43 +01:00
jubnl d7c4324afc chore(capacitor): base files for capacitor (#2020)
* 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>
2026-11-05 21:09:04 +01:00