feat(ios): relocate Capacitor iOS shell as apps/ios peer (LUM-1544)#31302
Conversation
Brings the Capacitor iOS wrapper into the new repo layout alongside apps/web, ahead of the platform → vellum-assistant deployment cutover. The platform copy at vellum-assistant-platform/web/ios remains the build source for TestFlight releases until the release pipeline is migrated in a follow-up. - apps/ios/ mirrors web/ios/ from the platform repo - apps/web/capacitor.config.ts configures ios.path: "../ios" - apps/web/ gains ios:setup / ios:open scripts and the @capacitor/cli + @capacitor/ios + plugin deps needed for cap sync to regenerate CapApp-SPM/Package.swift in the new tree - Package.swift and project.yml path references updated for the new apps/ios + apps/web layout (node_modules now resolves via ../../../web/node_modules from apps/ios/App/CapApp-SPM/)
The Scaffolding-only line is no longer accurate now that apps/ios and apps/web exist. The iOS release dispatch section now links to the canonical README (apps/ios/) with a follow-up note that TestFlight still builds from platform until LUM-1721 lands.
…itor-ios-0yskP # Conflicts: # apps/web/bun.lock
| │ │ ├── AppDelegate.swift # Default Capacitor template, no edits | ||
| │ │ └── Info.plist |
There was a problem hiding this comment.
🟡 README structure diagram mischaracterizes AppDelegate.swift as unmodified and omits key custom Swift files
The README structure diagram at line 436 labels AppDelegate.swift as "Default Capacitor template, no edits", but the actual file (apps/ios/App/App/AppDelegate.swift) contains significant custom code: Universal Link navigation via navigateWebView(to:), APNs device-token forwarding to Capacitor's NotificationCenter, and deep-link handling in application(_:continue:restorationHandler:). Additionally, the structure diagram omits MyViewController.swift, NativeAuthPlugin.swift, and NativeBiometricPlugin.swift entirely — these are the most important custom files in the iOS shell. A developer relying on this documentation would incorrectly assume the AppDelegate is stock and would be unaware of the three other custom Swift files.
| │ │ ├── AppDelegate.swift # Default Capacitor template, no edits | |
| │ │ └── Info.plist | |
| │ │ ├── AppDelegate.swift # Universal Links + APNs token forwarding | |
| │ │ ├── MyViewController.swift # CAPBridgeViewController subclass (plugin registration, viewport-fit, zoom prevention) | |
| │ │ ├── NativeAuthPlugin.swift # ASWebAuthenticationSession OIDC login flow | |
| │ │ ├── NativeBiometricPlugin.swift # Face ID / Touch ID Keychain token storage | |
| │ │ └── Info.plist |
Was this helpful? React with 👍 or 👎 to provide feedback.
| <key>aps-environment</key> | ||
| <string>development</string> |
There was a problem hiding this comment.
🚩 Dev target entitlements use aps-environment:development — correct for local builds, potential issue for future TestFlight
App-Dev.entitlements sets aps-environment: development while App.entitlements (used by both production and staging targets) sets aps-environment: production. For the current state where TestFlight builds come from the platform repo, this is irrelevant. However, when the LUM-1721 cutover points the CI pipeline at these files, the dev target's aps-environment: development could cause push notifications to fail in TestFlight builds (TestFlight uses Apple's production APNs gateway). The CI pipeline may need to override entitlements or use separate Debug/Release entitlements files when the cutover happens.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch but not changing it in this PR — these entitlements files are 1:1 copies of platform's web/ios/App/App/*.entitlements, so the same aps-environment mismatch exists in current TestFlight dev builds today. It's not a regression introduced by relocating the source tree.
The actionable piece ("CI may need to override entitlements or use separate Debug/Release entitlements files") is exactly LUM-1721 cutover scope. Noted there: https://linear.app/vellum/issue/LUM-1721#comment-de32a533-e517-4d99-ad2e-c31019a08f4f. That issue can also confirm whether the backend APNs gateway selection currently matches what TestFlight dev builds register against — independent of this PR.
Also worth noting: TestFlight respects whichever environment the entitlement specifies; the actual constraint is that the backend must send pushes to the matching gateway for the registered device token (aps-environment docs).
Generated by Claude Code
AppDelegate.swift is not stock — it handles Universal Link navigation and APNs device-token forwarding. The diagram also omitted MyViewController, NativeAuthPlugin, and NativeBiometricPlugin, which are the most important custom files in the shell.
There was a problem hiding this comment.
✦ APPROVE
Value: Establishes apps/ios/ as a first-class sibling to apps/web/ in the monorepo, completing the Capacitor shell relocation and making the iOS app buildable from a single bun run ios:open in the new repo — prerequisite for the LUM-1721 release pipeline cutover.
What this does: Moves the Capacitor iOS shell from vellum-assistant-platform/web/ios/ to apps/ios/, rebases all node_modules paths in Package.swift for the new tree structure, adds the required Capacitor deps to apps/web/package.json, and updates AGENTS.md to reflect the new canonical locations. Platform copy is intentionally left untouched until LUM-1721 cuts over the release pipeline.
CI: 39 pre-existing failures — none introduced by this PR.
edit-chat-session > *—ReferenceError: window is not definedat line 14 (window.sessionStorage.clear()). Bun test runner has nowindowglobal. This file predates the PR; no test files were touched.subscribeChatEvents idle watchdog > *(3 failures) — known 200ms timing sensitivity in the idle watchdog tests.- Lint ✅ · Type Check ✅ · Build ✅ · FlexFrame Lint ✅ · Socket Security ✅
apps/ios/App/CapApp-SPM/Package.swift — paths correct.
From apps/ios/App/CapApp-SPM/, three .. steps reach apps/; ../../../web/node_modules/ is exactly where bun install in apps/web/ deposits packages. Every plugin in package.json has a corresponding dependencies + targets entry. capacitor-swift-pm pinned to exact: "8.3.4" matches @capacitor/core. ✅
apps/web/capacitor.config.ts — clean.
ios.path: "../ios" points correctly to apps/ios/. contentInset: "never" comment fully explains the env(safe-area-inset-*) interaction (PRs #4821/#4832). SERVER_URL env routing is correct. appId placeholder comment is accurate about why the value diverges from the real bundle ID. ✅
NativeAuthPlugin.swift — security properties all intact.
- Host validation (
isAllowedBaseURL): exact match againstVellumAssociatedDomainInfo.plist — per-target, rejects unsubstituted$vars. Dev build can't drivewww.vellum.aiSSO (ATL-425). ✅ - State CSRF:
SecRandomCopyByteswith 32 bytes → 256 bits of entropy, base64url-encoded. Refuses rather than downgrading if RNG fails. ✅ errorparam checked beforestate— server-side auth errors surface correctly even when state is absent. ✅- Double-tap safety: cancel-then-nil before new session, completion deliberately doesn't clear
authSessionto avoid race with replacement session. The comment explaining why is correct. ✅ prefersEphemeralWebBrowserSession = true— prevents stale WorkOS cookie loops. ✅- Callback scheme: read from
CFBundleURLTypes, rejects unsubstituted$vars, fallback to"vellum-assistant". ✅
NativeBiometricPlugin.swift — solid.
- Preflight with
interactionNotAllowed = trueprevents spurious Face ID when no token is stored — correct pattern. ✅ kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly+.userPresence: device-only (no iCloud Keychain sync), biometric + passcode fallback. The comment justifying.userPresenceover.biometryCurrentSet(server-side token expiry makes enrollment-change lockout redundant) is accurate. ✅storeTokendeletes existing item before adding — avoidserrSecDuplicateItem. ✅deleteTokentreatserrSecItemNotFoundas success — idempotent. ✅opticIDcase in biometry switch for visionOS completeness +@unknown default. ✅
MyViewController.swift — correct.
Both WKUserScript injections use injectionTime: .atDocumentStart, which runs before WebKit parses the page's own <meta> — the only injection time that reliably locks in viewport-fit=cover before first layout. max(16px, 1em) for input zoom prevention is the right form (respects em scaling). Both plugins registered in capacitorDidLoad(). ✅
AppDelegate.swift — clean.
navigateWebView(to:) covers root CAPBridgeViewController, nav controller, and child hierarchy. APNs forwarding via NotificationCenter is the standard Capacitor pattern. ✅
apps/ios/App/project.yml — clean.
App.xcodeproj/ gitignored (no pbxproj churn). Three targets (App / App Staging / App Dev) each pick up their own AppIcon via xcconfig. optional: true on cap-managed resources handles the pre-cap sync state correctly. Pre-build staleness guard (project.yml -nt project.pbxproj) is a nice DX touch. ✅
AGENTS.md — accurate.
Root description now correctly lists both apps/web/ and apps/ios/. iOS release section updated with the LUM-1721 cutover framing. ✅
Non-blocking note: AGENTS.md hardcodes LUM-1721 as the release pipeline cutover ticket. The PR description flags that this Linear issue may not exist yet. If it doesn't, worth filing before this PR merges so the reference isn't a dead link — the cutover scope (wiring release-ios.yaml to target apps/ios/ instead of vellum-assistant-platform/web/ios/) is well-defined enough to ticket now.
Vellum Constitution — Trust-seeking: bringing iOS source-of-truth into the same repo as the web app makes the build surface auditable and reproducible from a single bun run ios:open, closing the gap between where the code lives and where it ships.
vellum-assistant is an open-source repo and external contributors don't have access to Linear. Replace specific issue references with plain descriptions of the work; the pre-existing "Notes for Vellum team members" section in AGENTS.md (line 90) keeps its general Linear workflow note, which is the intentional carve-out for internal devs.
Adds a Debugging section to apps/ios/README.md covering Safari Web Inspector (for the WKWebView contents — JS/CSS/network/console.log) and the Xcode debugger (for native Swift code), plus common recipes for plugin, streaming, and load-failure debugging. Also fixes three stale `web/ios/...` path references left over from the relocation to `apps/ios/` (#31302): - apps/ios/.gitignore — drop reference to deleted build-ios-slot.yaml - apps/web/src/runtime/native-auth.ts — update plugin file path - apps/web/src/runtime/native-biometric.ts — update plugin file path Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Part of Web App Repo Move: platform → vellum-assistant. Closes LUM-1544.
Relocates the Capacitor iOS shell from
vellum-assistant-platform/web/ios/tovellum-assistant/apps/ios/, sitting as a peer ofapps/web/instead of nested inside it. The platform copy is intentionally untouched — TestFlight releases continue to build fromvellum-assistant-platform/web/ios/until the release pipeline is migrated in a follow-up.What moved
web/ios/apps/ios/web/capacitor.config.tsapps/web/capacitor.config.ts(withios.path: "../ios")web/capacitor-shell/apps/web/capacitor-shell/apps/web changes
@capacitor/cli(dev),@capacitor/ios,@capacitor/file-viewer,@capacitor/haptics,@capacitor/push-notifications,capacitor-plugin-safe-area— pinned exact, matching the existing iOS shell's plugin set socap sync iosregenerates the samePackage.swift.ios:setup/ios:openscripts mirror the platform-side scripts but target the new relative paths (../ios/App/...).bun.lockregenerated.apps/ios changes
App/CapApp-SPM/Package.swift— node_modules paths rebased from../../../node_modules/...to../../../web/node_modules/...(the new tree puts node_modules inapps/web/, not at the iOS shell's grandparent).capacitor-swift-pmpin bumped to8.3.4to match@capacitor/corealready on this repo'sapps/web.App/project.yml— prebuild guard's error message referencesapps/ios/App/project.ymlandapps/web/instead ofweb/ios/.../web/.README.md— paths updated throughout; added a migration callout noting that the release pipeline lives invellum-assistant-platformuntil cutover.What's deliberately not in scope
vellum-assistant-platform/web/ios/,web/capacitor.config.ts,web/capacitor-shell/, and.github/workflows/release-ios.yamlstay put. They're the live TestFlight build source until we cut over.repository_dispatchfromvellum-assistant→vellum-assistant-platform/release-ios.yamlis unchanged. Pointing it at the new directory is a follow-up — needs new Linear issue if one doesn't exist.Test plan
cd apps/web && bun install(CI runs this with--frozen-lockfile)cd apps/web && bun run lint— cleancd apps/web && bun run typecheck— cleancd apps/web && bun run build— passes (covered bypr-web.yaml)cd apps/web && bun run ios:setup→ regeneratesapps/ios/App/App/capacitor.config.json,apps/ios/App/App/public/, andapps/ios/App/App.xcodeproj/without errorsbun run ios:openopensapps/ios/App/App.xcodeprojin Xcodedev-assistant.vellum.aiGenerated by Claude Code