219 lines
12 KiB
Markdown
219 lines
12 KiB
Markdown
# OpenChamber Mobile Handoff
|
|
|
|
Status and process reference for the native iOS/Android apps. Written so work can continue after
|
|
merge — either by finishing CI/release automation, or by adding features as follow-up fixes. The
|
|
apps are feature-complete for a first public/TestFlight-style test; CI/signing is the main gap.
|
|
|
|
## What this package is
|
|
|
|
`packages/mobile` is a Capacitor workspace that wraps the **hosted mobile web UI** (the `MobileApp`
|
|
renderer), not the desktop shell. The native app is a WKWebView (iOS) / Android WebView loading a
|
|
bundled copy of the web build; native capabilities are added via Capacitor plugins and two iOS app
|
|
extensions.
|
|
|
|
- App id / package: `com.openchamber.app`; app name `OpenChamber`.
|
|
- Capacitor config: `capacitor.config.ts` (Keyboard `resize: 'none'`, StatusBar overlay, Push
|
|
`presentationOptions: []`).
|
|
- Renderer: the web build's `mobile.html` entry (`MobileApp`), copied into `dist/` and served by
|
|
Capacitor. Mobile-only surfaces (connection onboarding, `Instances`, QR pairing, widgets) exist
|
|
only in the Capacitor shell — hosted `mobile.html` in a plain browser does not expose them.
|
|
|
|
## Build pipeline (how a native build is produced)
|
|
|
|
```
|
|
bun run --cwd packages/web build # web/dist
|
|
→ scripts/prepare-web-assets.mjs # copy web/dist → mobile/dist, mobile.html → index.html
|
|
→ cap sync # copy dist → native, sync plugins/config
|
|
→ xcodebuild / gradle assembleDebug # native binary
|
|
```
|
|
|
|
`sync` (in `packages/mobile/package.json`) runs `bun run build && cap sync` inside the mobile env
|
|
wrapper. Everything native-facing goes through `scripts/with-mobile-env.mjs`.
|
|
|
|
### `with-mobile-env.mjs` (toolchain wrapper — read this before debugging build env issues)
|
|
|
|
Every build/deploy script runs through it. It sets, with env overrides honored first:
|
|
|
|
- `DEVELOPER_DIR` — `$DEVELOPER_DIR` → `xcode-select -p` → `/Applications/Xcode.app/...`. It
|
|
intentionally honors `xcode-select` so an Xcode beta / non-default install is used (hardcoding
|
|
the path previously forced builds onto the wrong Xcode / Command Line Tools).
|
|
- `JAVA_HOME` — `$JAVA_HOME` → `/opt/homebrew/opt/openjdk@21`.
|
|
- `ANDROID_HOME` / `ANDROID_SDK_ROOT` — `$ANDROID_HOME` → `/opt/homebrew/share/android-commandlinetools`.
|
|
- `PATH` — prepends `$JAVA_HOME/bin` and `$ANDROID_HOME/platform-tools` (so `adb` resolves).
|
|
|
|
On another machine, override these env vars rather than editing the script. `xcode-select` may
|
|
point at Command Line Tools; the wrapper's `DEVELOPER_DIR` handling covers that for mobile commands.
|
|
|
|
## Commands
|
|
|
|
Root aliases (from repo root):
|
|
|
|
```sh
|
|
bun run mobile:build # web build + prepare-web-assets
|
|
bun run mobile:sync # build + cap sync
|
|
bun run mobile:build:android:debug # sync + gradle assembleDebug
|
|
bun run mobile:build:ios:simulator # simulator build (strips MLKit pod, see quirks)
|
|
bun run mobile:open:ios # open in Xcode
|
|
bun run mobile:open:android # open in Android Studio
|
|
bun run type-check:mobile
|
|
bun run lint:mobile
|
|
```
|
|
|
|
Android physical-device deploy (adb-based, in `scripts/android-device.mjs`) — **not aliased at
|
|
root**, run from the package:
|
|
|
|
```sh
|
|
bun run --cwd packages/mobile android:devices # list adb devices (want `device`, not `unauthorized`)
|
|
bun run --cwd packages/mobile android:install # adb install -r the debug APK
|
|
bun run --cwd packages/mobile android:launch # am start MainActivity
|
|
bun run --cwd packages/mobile android:run # install + launch
|
|
bun run --cwd packages/mobile android:logcat # app logs
|
|
```
|
|
|
|
Typical device iteration: `bun run --cwd packages/mobile build:android:debug` then
|
|
`android:run`. APK path: `android/app/build/outputs/apk/debug/app-debug.apk`.
|
|
|
|
iOS Simulator helpers: `mobile:sim:{boot,install,launch,run,serve,list,kill}` (see
|
|
`scripts/ios-sim.mjs`; `serve-sim` for a browser preview of the simulator).
|
|
|
|
## Native capabilities implemented
|
|
|
|
- **Connection onboarding** — server URL entry, password unlock for locked servers, client-token
|
|
issuance, saved connections, `Instances` management sheet, auto-connect to the last instance on
|
|
launch. Deleting the active instance resets the runtime to the connect screen.
|
|
- **QR pairing** — `@capacitor-mlkit/barcode-scanning`. Android uses the CameraX-backed
|
|
`startScan()` flow with the barcode model bundled in the app, so scanning works offline and
|
|
without Google Play Services. iOS uses the plugin's native scanner. CAMERA permission +
|
|
`NSCameraUsageDescription` declared.
|
|
- **Secure storage** — `@aparajita/capacitor-secure-storage` for connection tokens.
|
|
- **Deep links** — `openchamber://` URL scheme; a reusable intent vocabulary (`apps/deepLinks.ts`)
|
|
used by notification taps, widgets, and Control Center. Cold-launch intents are stashed.
|
|
- **Push notifications** — iOS APNs + Android FCM (see below). Presence-aware routing suppresses a
|
|
device's push when an interactive (desktop/web) client is visible.
|
|
- **iOS widgets + Control Center + Notification Service Extension** — WidgetKit extension
|
|
(`OpenChamberWidget`), a Control Center control, and an NSE (`OpenChamberNotificationService`)
|
|
that refreshes widgets from push. All share the App Group `group.com.openchamber.app`.
|
|
- **Native chrome** — status bar (iOS overlay + safe-area; Android inset + themed background),
|
|
keyboard handling (iOS CSS inset; Android native `adjustResize`), edge-swipe session switch,
|
|
back-button handling, app-icon badge.
|
|
- **App icons** — iOS `AppIcon`; Android adaptive launcher icon; notification small icon
|
|
(`ic_stat_notify`).
|
|
|
|
## Push / notifications architecture
|
|
|
|
- Registration: on launch the app registers a device token — **iOS → APNs, Android → FCM** — and
|
|
sends it to the connected server tagged with `platform` (`ios`/`android`).
|
|
- The server forwards notification-worthy events to a signed **relay**; the relay routes each token
|
|
to APNs or FCM by its bound platform. The app itself only needs to obtain and register the token.
|
|
- **Presence-aware suppression**: each client reports foreground visibility + its platform; a
|
|
mobile push is skipped while an interactive (desktop/web/vscode) client is visible (it already
|
|
shows the in-app notification). Gated on the desktop's visibility, never the phone's own.
|
|
- Foreground behavior: iOS suppresses the banner via `presentationOptions: []`; the web/PWA service
|
|
worker suppresses when a window is focused.
|
|
|
|
## Platform config specifics
|
|
|
|
### iOS (`ios/App`)
|
|
|
|
- Extensions: `OpenChamberWidget` (WidgetKit, deployment 17.0) and `OpenChamberNotificationService`
|
|
(NSE, 15.5), both hand-wired into `App.xcodeproj/project.pbxproj` and embedded via a copy phase.
|
|
- App Group `group.com.openchamber.app` in all three targets' entitlements (app + widget + NSE).
|
|
- `Info.plist`: `CFBundleURLTypes` scheme `openchamber`, `NSCameraUsageDescription`.
|
|
- Push entitlement (aps-environment) required.
|
|
- APNs `mutable-content: 1` (set server/relay side) wakes the NSE to refresh widgets.
|
|
|
|
### Android (`android/app`)
|
|
|
|
- `google-services.json` (committed; Firebase project `openchamber-8bf7e`). The Google Services
|
|
Gradle plugin is applied conditionally when the file exists; `@capacitor/push-notifications`
|
|
brings `firebase-messaging`.
|
|
- Manifest: permissions `INTERNET`, `CAMERA` (+ optional camera feature), `POST_NOTIFICATIONS`
|
|
(Android 13+; older versions allow notifications by default). `windowSoftInputMode=adjustResize`.
|
|
FCM `default_notification_icon=@drawable/ic_stat_notify`.
|
|
- Adaptive launcher icon: full-bleed color background + `ic_launcher_foreground` (sources under
|
|
`packages/mobile/assets/`, regenerable with `@capacitor/assets`).
|
|
|
|
## Quirks / gotchas
|
|
|
|
- **iOS Simulator + MLKit**: `GoogleMLKit` barcode has no arm64-simulator slice, so
|
|
`scripts/ios-sim-build.mjs` temporarily strips the `CapacitorMlkitBarcodeScanning` pod, builds,
|
|
then restores it. Device builds include it normally.
|
|
- **Android WebView version**: the UI uses `color-mix()` (Tailwind v4 + theme) which needs
|
|
Chromium **111+**. An outdated Android System WebView renders translucency/selection wrong — tell
|
|
testers to keep Android System WebView updated (or use a device with a current one).
|
|
- **Capacitor stream transport is locked to SSE** on the native apps (native WebSocket streaming is
|
|
unreliable on Android). The Chat transport setting shows SSE selected and disables the others in
|
|
the Capacitor shell.
|
|
- **Android push needs the app rebuilt with `google-services.json`**; without it `register()` used
|
|
to crash ("Default FirebaseApp is not initialized"). Registration is gated to iOS/Android natives.
|
|
|
|
## Validation
|
|
|
|
```sh
|
|
bun run type-check:mobile
|
|
bun run lint:mobile
|
|
bun run mobile:build:android:debug
|
|
bun run mobile:build:ios:simulator
|
|
```
|
|
|
|
Web-inherited build warnings (KaTeX font URLs, `onnxruntime-web` eval, chunk-size) are expected and
|
|
non-fatal.
|
|
|
|
## The gap: CI / release automation (next work)
|
|
|
|
The apps build and deploy locally; there is no CI/signing/publishing yet. To take them to
|
|
TestFlight / Play internal testing:
|
|
|
|
### iOS
|
|
|
|
- Apple Developer account; App IDs for the app **and** both extensions
|
|
(`com.openchamber.app`, `.OpenChamberWidget`, `.OpenChamberNotificationService`), each enabled for
|
|
the **App Group** and (app) **Push**.
|
|
- Signing certificate + provisioning profiles for all three targets (extensions need their own).
|
|
- App Store Connect API key for non-interactive TestFlight upload (`xcodebuild archive` +
|
|
`notarytool`/`altool`, or fastlane `gym`+`pilot`).
|
|
- Runner: macOS with the same Xcode as `DEVELOPER_DIR`.
|
|
|
|
### Android
|
|
|
|
- Release keystore (kept as a CI secret); build a signed **AAB** (`bundleRelease`) — the debug
|
|
scripts here produce an unsigned debug APK.
|
|
- Play Console app + internal testing track; a Play service account for automated upload (fastlane
|
|
`supply` or the Play Developer API).
|
|
- `google-services.json` is committed, so FCM builds in CI without extra setup.
|
|
- Runner: Linux with the Android SDK + `openjdk@21`.
|
|
|
|
### Notes for CI
|
|
|
|
- Reuse `with-mobile-env.mjs`'s env contract (`DEVELOPER_DIR`, `JAVA_HOME`, `ANDROID_HOME`) — set
|
|
them in the workflow instead of relying on local Homebrew paths.
|
|
- Relay/push secrets (APNs key, FCM service account) live in the relay infrastructure, not app CI.
|
|
- Version/build-number bumping is not automated yet.
|
|
|
|
## Store review readiness
|
|
|
|
Xcode build warnings do not block review; the concrete items are store requirements, not code
|
|
quality. Done in-repo vs. to-do at release time:
|
|
|
|
**Done in-repo (this branch):**
|
|
|
|
- iOS app **Privacy Manifest** (`ios/App/App/PrivacyInfo.xcprivacy`) — declares no tracking and the
|
|
required-reason UserDefaults API (App Group snapshot). Bundled SDKs ship their own manifests.
|
|
- iOS **`ITSAppUsesNonExemptEncryption = false`** in `Info.plist` (skips the per-build export-
|
|
compliance prompt).
|
|
- iOS camera + local-network usage strings; Android SDK levels (`target/compile 35`, `min 24`) meet
|
|
Play's current requirements.
|
|
|
|
**To-do at release (console / infra, not code):**
|
|
|
|
- **Privacy policy URL** — required by both stores because the app uses camera + notifications.
|
|
- iOS **App Privacy nutrition label** (App Store Connect) and Android **Data Safety** form — declare
|
|
what's collected (device push token; the app otherwise talks only to the user's own server).
|
|
- **Production APNs** for App Store / TestFlight builds: the app's `aps-environment` must be
|
|
`production` in the release build, and the relay must send to production APNs (not sandbox).
|
|
- **Demo instance + credentials** for reviewers — the app connects to a user's server, so review
|
|
needs a reachable test instance (App Store 2.1 / Play).
|
|
- **Guideline 4.2 (minimum functionality)** — WebView-wrapper apps can be scrutinized; cite the
|
|
native features (push, widgets, Control Center, QR pairing) in the review notes.
|
|
- Signing/upload as covered in the CI section above (all three iOS targets; signed Android AAB).
|