Skip to content

iPhone M1 foundation: Swift app cut to Pair, Now and widgets - #8

Closed
frahlg wants to merge 3 commits into
mainfrom
claude/ios-m1-foundation
Closed

frahlg wants to merge 3 commits into
mainfrom
claude/ios-m1-foundation

Conversation

@frahlg

@frahlg frahlg commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Replaces #5. This keeps #5's protocol core and cuts it down to the iPhone M1 that Fredrik approved on 2026-10-12: pairing, a lean Now, a car view and price widgets. This PR is the foundation. Two follow-up branches start from it: price widgets, and the car view with Now. #5 stays open.

What changed

Taken from #5: appleApp/, the Apple workflow and the KMP change that drops the iOS targets. iosApp/ and shared/src/iosMain go. Android and shared/ still build and test.

Scope: iPhone only, iOS 26. Gone: History, Plan, PlanText, HistoryGeometry, BoxModels, BoxView, BoxCode, escrow (Escrow, RecoveryBlob, the sealed-copy switch, passkey recovery), GlanceView, LiveSheet, EVSheet, PlainWebSocket, the macOS target, swift-crypto and the Linux job. Session and Messages keep prices, the API passthrough and commands, without history or plan.

Modules: one Swift package, appleApp/FTWKit, with three targets.

  • FTWKit: the app's logic. The app alone links it.
  • FTWShared: plain Sendable values for widgets and a later notification extension. It holds the App Group cache, PriceStrip, Freshness, PowerFormat and PriceUnits, with English and Swedish words for the band.
  • FTWDemo: the simulated box, out of the product module. The app injects it into AppModel.

App Group cache: the app writes prices-<site> and car-<site> after each read. Each record carries carrier, srcState and when it was read, and always reads back as cache. The car record keeps soc_source and when the level was read (the car's report time for vehicle). Files use completeFileProtectionUntilFirstUserAuthentication. CacheReader takes any number of CacheSources, so a copy from FTW Cloud (opt-in and readable, designed separately) can be one more source with the same records. Signing out clears it. The demo writes nothing.

Contract: protocol/registry.yaml is main's, unchanged. Contract.swift gains planner.prefs.set and the mode list without priority and weighted. ContractTests now also checks modes and tiers, the frozen field ids, the CarrierKind/SourceState enums and the names the demo box uses.

Session: it closes when the app goes to the background and opens again on return without a passkey. Inactive (Control Center, the app switcher) only slows the stream. A relay close with 4429 sets busy, and the band says "FTW is busy" / "FTW är upptaget" with no countdown.

Pairing: a pairing QR scanned with the iOS camera arrives as a universal link to https://app.ftw.energy/p#… and takes the same offer path as any pairing link. In the simulator, which has no camera, a picture of the QR from Photos pairs too (PairingImage, with tests).

Targets: FTWWidgets, a WidgetKit extension with one small price widget that reads the cache. It holds no key and opens no stream. The App Group group.energy.ftw.app is written once, in appleApp/FTW.xcconfig. Both entitlements files and both Info.plists read it, and Swift reads it from Info.plist. DEVELOPMENT_TEAM = L66M368PZ6 is set there too, for both targets, with automatic signing. Simulator builds still sign ad hoc and need no Apple account, so the entitlements travel with the app; CI no longer builds with CODE_SIGNING_ALLOWED=NO. No certificate, profile or key is committed.

For the next two branches

The project uses folders that Xcode keeps in step with the disk, so a new file needs no change to project.pbxproj.

  • Widgets: appleApp/FTWWidgets/**, appleApp/FTWKit/Sources/FTWShared/**, Tests/FTWSharedTests/**.
  • Car view and Now: appleApp/FTW/UI/**, appleApp/FTW/Car/** (new), and on the model side State/LoadpointsModel.swift and Format/EV.swift.

Checks

Check Result
swift test in appleApp/FTWKit (Xcode 27, Swift 6.4) 94 passed, 1 live test skipped
./gradlew :shared:jvmTest --rerun 42 tests, 0 failures, 1 skipped
./gradlew :androidApp:assembleDebug Builds
xcodebuild FTW scheme, generic/platform=iOS Simulator Builds with 0 warnings; app and widget carry the App Group entitlement
Demo in a new iPhone 17 simulator (iOS 26.5) Streams and shows Now with the price card; the App Group container exists
A picture of a pairing QR, picked from Photos in that simulator Read and paired with a test-vector box

Open

  • DCO: the commits carry no Signed-off-by. CONTRIBUTING asks for it, and an agent should not sign in Fredrik's name.
  • Manual step for Fredrik: register the App IDs energy.ftw.app and energy.ftw.app.widgets for team L66M368PZ6, the App Group group.energy.ftw.app on both, and Associated Domains on the app. Signing in to Xcode with the team and building for a phone does it through automatic signing; the developer portal works too.
  • The apple-app-site-association file on app.ftw.energy comes from a separate ftw-webapp PR. Passkeys and the camera QR link need it.
  • With no passkey PRF (as in the simulator), pairing still falls back to the local key without saying so. The plan wants that shown as "No passkey copy yet".
  • There is no sign-out screen now that the Box tab is gone. The car view branch can add a small one.
  • Pressing Charge now and leaving the app at once can close the session before the box confirms. A short background task could wait for the result.
  • The Now and Pair copy is still English only; FTWShared's band text is the first part with Swedish.

🤖 Generated with Claude Code

frahlg and others added 3 commits October 12, 2026 16:21
Taken as they stand from claude/native-swift-ftw-app-d7j9w4 (dbfce65):
appleApp/, .github/workflows/apple.yml, .gitignore and the KMP change
that removes the iOS targets. iosApp/ and shared/src/iosMain go, as in
#5. protocol/registry.yaml stays as it is on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iPhone only, iOS 26. Pairing, Now with the price card, and the demo stay.
History, Plan, Box admin, escrow, spoken box codes, the macOS target and
menu bar, swift-crypto, the Linux job and PlainWebSocket go. The charger
sheet goes too; the car view replaces it next.

- One Swift package, three modules: FTWKit (the app's logic), FTWShared
  (Sendable values for widgets and later a notification extension) and
  FTWDemo (the simulated box, out of the product module).
- FTWShared holds the App Group cache: prices-<site> and car-<site>,
  each with carrier, srcState and when it was read, written with
  completeUntilFirstUserAuthentication. A reader takes any number of
  sources, so a cloud copy can be one more source later. The price and
  freshness formatters moved here, in English and Swedish.
- The car record keeps soc_source and when the level was read.
- Contract.swift follows main's registry: planner.prefs.set and the
  modes without priority and weighted. ContractTests now also checks
  modes, frozen field ids, the shared enums and the demo box's names.
- The session closes when the app goes to the background and reopens on
  return without a passkey. A relay 4429 shows "FTW is busy".
- A WidgetKit extension with one price widget that reads the cache. The
  App Group is written once, in FTW.xcconfig; both entitlements and
  Info.plists read it. No DEVELOPMENT_TEAM; simulator builds sign ad hoc
  so the entitlements travel with the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- DEVELOPMENT_TEAM L66M368PZ6 in FTW.xcconfig, for the app and the
  widget, with automatic signing. Simulator builds still sign ad hoc and
  need no Apple account. The App IDs, App Group and Associated Domains
  still have to be registered for the team; the README lists the steps.
- A pairing QR scanned with the iOS camera arrives as a universal link
  to https://app.ftw.energy/p#…. The app takes it through onOpenURL and
  onContinueUserActivity, the same offer path as any pairing link.
- In the simulator, which has no camera, a picture of the QR from Photos
  pairs. PairingImage reads the link with CoreImage, with tests.
- FTW Cloud is now an opt-in, readable state cache. The cache comments
  and CLAUDE.md say so; M1 does not depend on it.
- Passkeys no longer uses the presentation anchor iOS 26 deprecated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg

frahlg commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Included in #14, which lands the whole iPhone M1 app as one PR. The branch stays.

@frahlg frahlg closed this Oct 12, 2026
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.

1 participant