Files
verdande/docs/ios.md
T
kristianwindandClaude Opus 5.5 ce1e12417b feat(app): iOS-skallen — Capacitor, Xcode-projektet og et CI-job, der bygger den
Femte trin mod iOS-appen. Fladen fra trin 3 pakket ind i en rigtig app.

- `app/`: Capacitor 8 med SPM (ingen CocoaPods). `npm run sync` bygger web
  med VERDANDE_TARGET=app og kopierer det ind i Xcode-projektet; `npm run
  open` åbner det på en Mac. Bundle-id `dk.nolimit.verdande`.
- `app/ios/` er committet. Webbygget og den genererede konfiguration i det er
  ikke — sync lægger dem der.
- `NSAllowsLocalNetworking`: en hjemmeserver uden certifikat kan nås med http
  på det lokale net; alt på internettet skal stadig være https.
- Ikon og opstartsbillede tegnes fra web/static/icon.svg
  (scripts/generate-ios-assets.js). Kvadratisk og uden gennemsigtighed, fordi
  iOS runder hjørnerne selv.
- CI-jobbet "iOS app shell" bygger og synker på Linux og tjekker, at
  app-bygget ikke registrerer en service worker. Ingen Xcode i CI; det dækker
  alt op til den.
- docs/ios.md: hvordan appen adskiller sig fra en browser, hvordan den bygges,
  hvad der er ejerens skridt (Apple-konto, signering, TestFlight), og hvad der
  mangler — notifikationer via APNs venter på en beslutning om en relay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 14:21:44 +02:00

3.6 KiB

The iOS app

The iOS app is the web interface bundled into a native shell with Capacitor. It is not a second interface: the same Svelte build runs in both, and everything that differs between a browser and the app lives in web/src/lib/platform.js.

It talks to any Verdande server. The first screen asks for the address, checks it with /api/v1/ping, and then signs in there.

How the app differs from a browser

Browser App
Where the interface comes from the server inside the app, capacitor://localhost
Which server the one it was loaded from the one its owner typed in
Session httpOnly cookie a session token, sent as Authorization: Bearer
Live sync WebSocket with the cookie WebSocket opened with a one-use ticket (POST /ws-ticket)
Pictures and downloads plain <img> and links fetched with the token and shown from a blob (nativemedia.js)
Service worker, Web Push yes no — WKWebView has neither
Passkeys yes not yet — a passkey is bound to the server's domain
Creating the first account yes no — done in a browser, then sign in from the app

The server side of this is client: "app" on /auth/login, the capacitor://localhost CORS rule, and the ticket. All three are in docs/openapi.yaml.

An app session slides: every use moves its end forward, up to VERDANDE_APP_SESSION_TTL (180 days) unused. It is listed under Enheder in settings with the name the app gave itself, and can be signed out from there.

Building

Everything up to Xcode runs anywhere, and runs in CI (iOS app shell):

cd app
npm ci
npm run sync      # builds web with VERDANDE_TARGET=app, copies it into ios/

On a Mac with Xcode:

npm run open      # opens ios/App/App.xcodeproj

Choose a simulator and press Run. In the app, type the address of a Verdande — http://localhost:8080 reaches a dev server on the same Mac from the simulator.

Plain http is allowed to local addresses only (NSAllowsLocalNetworking in Info.plist), so a home server without a certificate works on the same network; anything on the internet has to be https.

app/ios/ is committed. The web build inside it (App/App/public) and the generated config are not — npm run sync puts them there.

The icon and launch image come from web/static/icon.svg:

cd web && node scripts/generate-ios-assets.js

Putting it on a phone

These steps are the account owner's — they involve Apple's terms, a payment and signing keys.

  1. Apple Developer Program membership (99 USD a year) on the Apple ID that should own the app.
  2. In Xcode → Signing & Capabilities: choose that team. The bundle id is dk.nolimit.verdande; it can change until the first upload and never after.
  3. Product → Archive, then Distribute App → TestFlight.
  4. In App Store Connect, add testers to the TestFlight build.

Not yet

  • Notifications. Web Push does not exist in the app. Notifications on an iPhone go through Apple's APNs, and the key that sends them is bound to the app's bundle id — so it cannot live on every self-hosted server. That means a small relay run by the app's publisher, which every instance with an app user would talk to. That is a new connection from other people's servers, and it waits on a decision before it is built.
  • The session token in the Keychain. It is in the WebView's own storage for now, behind one object in platform.js so the Keychain plugin can replace it without anything else changing.
  • Passkeys in the app.
  • Share sheet (share text or a link into Verdande from another app).