Skip to content

Native Swift app for iOS and macOS - #5

Draft
frahlg wants to merge 10 commits into
mainfrom
claude/native-swift-ftw-app-d7j9w4
Draft

frahlg wants to merge 10 commits into
mainfrom
claude/native-swift-ftw-app-d7j9w4

Conversation

@frahlg

@frahlg frahlg commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

What this is

A pure Swift FTW client for iPhone, iPad and Mac. It replaces iosApp/ and the Kotlin Multiplatform dependency on Apple platforms. Android keeps the KMP core unchanged.

Scope set by Fredrik on 2026-09-25: Swift for iOS and macOS, replace iosApp directly, cover the whole web app, and derive the wrap key from the passkey the way the web app does (HKDF), so one passkey opens a home in both. Native pairings made before this change scan the QR again.

Layout

  • appleApp/FTWKit: Swift package with everything that is not a pixel. CBOR, frames, Noise IK, transport, relay and Noise carriers, the session, pairing, vault, PRF derivations, recovery blob, escrow, the simulated box for the demo, and the state every screen reads. Builds and tests on Linux and on Apple platforms (CryptoKit there, swift-crypto on Linux only).
  • appleApp/FTW: the SwiftUI app. Pair, Now (sentence, energy flow, price card, plan brief, today, fuse, live line, charger sheet), Plan, History with daily energy, Box (identity, access and viewer invites, notification rules, restart, sealed copy, sign out). A Mac also gets a menu bar glance.
  • appleApp/FTW.xcodeproj: hand written, objectVersion 77 with a synchronized FTW/ folder and FTWKit as a local package. Bundle id energy.ftw.app, iOS 18 and macOS 15.
  • iosApp/, shared/src/iosMain and the iOS framework targets in shared/build.gradle.kts are removed. README and CLAUDE.md describe the new split.

Evidence

  • swift test in appleApp/FTWKit: 95 tests green on Linux (Swift 6.3) and on macOS in CI (Xcode 26.3, CryptoKit).
  • ./gradlew :shared:jvmTest after removing the iOS targets: 41 passed, 0 failed, 4 skipped (the live box tests, which need FTW_LIVE_BOX).
  • Cross implementation vectors from the web app's own code and the box: Noise, frames, rendezvous handles, a recovery blob sealed by the web app opens here, escrow ids and write keys match, the vault copy opens.
  • Live run against a box built from srcfl/ftw master, through wss://relay.ftw.energy: paired, streaming after 1.2 s, /api/status over the passthrough, plan, history tiles, site.mode.set applied. That run found a real bug (times sent with a fraction) that is fixed here.
  • The freshness band wording, the price card's cheapest two hours and the sealed copy switch live in FTWKit with tests ported from the web app's cases.

State at handoff

Work moves to Fredrik's Mac from here. This session stops pushing.

  • CI run 3 compiled the app for the iOS Simulator and stopped on one Swift 6 error in Passkeys.swift (a non Sendable credential crossing a continuation). That is fixed in 2a24e9a. The run for the latest head had not finished at handoff, so the app compiling end to end is not yet confirmed. Expect possibly a few more strict concurrency errors in Xcode.
  • The Linux job failed earlier on a Swift 6.1 literal typing quirk in a test helper, fixed in 2a24e9a.

Continue on a Mac

  1. git fetch && git checkout claude/native-swift-ftw-app-d7j9w4
  2. cd appleApp/FTWKit && swift test
  3. Open appleApp/FTW.xcodeproj, pick your team under Signing & Capabilities, build the FTW scheme for an iPhone simulator and for My Mac. Fix whatever Xcode flags.
  4. In the simulator, "Try the live demo" on the pairing screen runs the whole app without a box. appleApp/scripts/demo-screenshots.sh photographs every tab of the demo after a Debug build.
  5. Real pairing needs passkeys, which need step 6.
  6. Add .well-known/apple-app-site-association to app.ftw.energy (srcfl/ftw-webapp) with <TEAM_ID>.energy.ftw.app under webcredentials and applinks.
  7. On a device: register a passkey in the web app and open the same home in the native app, then the other way round. That proves wrap key parity with a real PRF, which no test here can.

Open

  • Notifications: the box speaks web push and ntfy, not APNs. The Box screen manages the box's rules and history and says plainly that this app does not receive them yet.
  • Nobody has looked at the screens yet. Please review them in the simulator before this leaves draft.
  • CONTRIBUTING asks for a DCO sign-off on each commit. These commits carry none; Fredrik signs off his own.

🤖 Generated with Claude Code

https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t

FTWKit is a Swift package that builds and tests on Linux and Apple
platforms. CryptoKit supplies the primitives; swift-crypto stands in on
Linux only. The Cacophony vectors and the interop vectors that srcfl/ftw
generates from the web app's code pass byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
Enrollment parsing, the box code, the PRF derivations, the vault and
the recovery copy follow the web app byte for byte. Vectors produced by
running the web app's own identity code prove it: a copy the web app
sealed opens here under the same passkey, and the escrow ids and write
keys match. The escrow client pads, signs and versions its requests the
way the service expects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
RelayCarrier, NoiseCarrier and Session follow the web app's carrier and
session rules: rotating rendezvous handles with bounded epoch
correction, a silent box ending the handshake and a retry, requests that
always settle, one API call on the wire at a time, and the three command
deadlines. A SimulatedBox speaks the same frames for the demo and the
tests; the tests drive the whole stack through fake sockets on a manual
clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
A live run against a box built from srcfl/ftw, over the production relay,
found that the box drops a hist.query or price.get whose times carry a
fraction: it decodes them into int64. Times now go out whole, and the
simulated box refuses fractions the same way so the unit tests hold it.

LiveBoxTests pairs with a freshly minted code and exercises the stream,
the API passthrough, the plan, history, the E_USE_CMD refusal and a mode
command. It runs only when FTW_LIVE_URL is set; on Linux, whose
Foundation has no WebSocket client, it reaches the relay through a local
bridge named by FTW_LIVE_RELAY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
SiteModel keeps freshness honest the web app's way: a carrier is claimed
only once it delivers a reading, ages come from box uptime, and a quiet
stream stops reading as live. The cached snapshot is sealed on disk
under a key kept beside the vault, so a cold start paints before any
passkey prompt. Feature models cover the plan and modes, prices, energy
by day, history tiles with a cache, the charger sheet with its commands
and goal, sharing, notification rules, restart, pairing, recovery and
the demo. Every sentence is ported from the web app and pinned by tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
The native app is now pure Swift on Apple platforms. FTWKit moves to
appleApp/FTWKit so the Xcode project can sit beside it, and the app in
appleApp/FTW covers every screen the web app has: pairing (camera, a
picture of the QR on a Mac, passkey recovery, the demo), Now (the
sentence, the energy flow, price, the plan brief, today, the fuse, the
live line and the charger sheet), Plan, History with daily energy, and
Box (access, notifications, restart, the sealed copy, sign out).

The Keychain holds the store, passkeys run through AuthenticationServices
with PRF on the web app's relying party and salt, and a Mac gets a menu
bar glance. Three pieces of logic moved into FTWKit with tests so both
apps say the same thing: the freshness band's wording, the price card's
cheapest two hours and tones, and the sealed copy switch.

CI now builds the app, unsigned, for the iOS Simulator and for macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
Xcode 26 refuses to resume a continuation with the platform's passkey
credential, which is not Sendable. The delegate now copies the credential
id and the PRF output into plain values on the main actor and passes those.
The Linux test client stops mixing an Int literal into a UInt8 array, which
Swift 6.1 does not accept.

iOS and macOS are pure Swift now, so iosApp/, shared/src/iosMain and the
iOS framework targets in shared/build.gradle.kts go. Android keeps the KMP
core unchanged. README and CLAUDE.md describe the Apple app, its scope as
chosen by Fredrik on 2026-09-25, what is proven and what still needs a
device, and keep the Android gate as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
@frahlg
frahlg force-pushed the claude/native-swift-ftw-app-d7j9w4 branch from 4cd5ac2 to 2a24e9a Compare September 25, 2026 06:54
Debug builds take -FTWDemo YES and -FTWTab <now|plan|history|box>, so
appleApp/scripts/demo-screenshots.sh can open every screen of the
simulated home and save a picture of each, dark and light. CI runs it
after the build and keeps the pictures as an artifact. It is evidence for
whoever reviews the screens, not a gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015czRWjvgq7kpxB3pdJXm4t
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants