Files
recurring-checklist/mobile/README.md
T

219 lines
9.9 KiB
Markdown
Raw Normal View History

# Waters Checklist — native iOS & Android apps
The checklist app now ships to **three** targets, all built from the same front-end files in
`../server/public/`:
| Target | Where | Updates when |
|---|---|---|
| Website | `check.watersmedia.org` | you deploy — immediately |
| iOS app | App Store (unlisted) | a new build is uploaded **and released** |
| Android app | Play Store (unlisted track) | a new AAB is uploaded **and released** |
> **The one thing that matters:** the store apps bundle a *copy* of the front-end inside the
> app binary. They have no mechanism to notice a web deploy — not on relaunch, not on
> sign-out, not ever. Deploy the website alone and phones keep the UI they were last built
> with, silently, while the website moves on. So after any front-end change, run:
>
> ```bash
> ./release.sh
> ```
>
> from the project root (not from `mobile/`). That's the whole workflow.
---
## Everyday release
```bash
cd /Users/Studio/Documents/recurring-checklist-app
./release.sh # all three targets, bumps both build numbers
./release.sh --version 1.1.0 # …and set the version users see
./release.sh --web # website only — for a server/cron/email change
```
Once both API keys are configured (below), that one command deploys the website, uploads the
iOS build to App Store Connect and releases the Android build to the internal track — no
consoles, no dragging files. Builds also land in `dist-release/` either way.
**What "fully automated" can and cannot mean.** The website is live the second it deploys.
The stores are not, and no automation changes that: Play takes a few minutes to process an
internal-track release, and Apple takes ~15 minutes to process a build before TestFlight
testers can install it. Anything beyond TestFlight — a public App Store release — waits on
human review. The upload is automated; the stores' clocks are not.
---
## Architecture, in one screen
The apps are **not** a browser pointed at the website — Apple rejects that under Guideline
4.2. They bundle the front-end and treat the server as a JSON API.
That creates exactly one problem: on the web, front-end and API share an origin, so
`/api/state` and `/equip-images/x.jpg` just work. Inside an app the page is served from
`capacitor://localhost` (iOS) or `https://localhost` (Android), where those paths resolve
against the app bundle and 404. Two files solve it:
- **`../server/public/native-bridge.js`** — shared by *all three* targets, loaded before
`app.js`. In a browser it is a deliberate no-op. In an app it patches `window.fetch` to
re-point server-absolute paths, exposes `resolveUrl()` for the few `<img>`/`<a>` sites,
routes `navigator.vibrate` → native haptics, and opens external links in the system
browser instead of blanking the WebView.
- **`src/native-shell.js`** — native only, esbuild-bundled and injected into app bundles
only. Publishes `window.CapPlugins` synchronously, then handles splash handover, status
bar, Android's hardware Back button, foreground resume, and online/offline.
`server.js` carries a matching **`NATIVE_CORS_ORIGINS`** allowlist (an allowlist, not `*`;
auth is a Bearer token with no cookies, so `Allow-Credentials` is deliberately omitted).
`www/` is generated. It is wiped and rebuilt from `../server/public` on every sync — editing
it is always a mistake.
---
## Toolchain (already installed, no Homebrew on this Mac)
| | |
|---|---|
| JDK 21 | `~/.jdks/temurin-21` |
| Android SDK | `~/Library/Android/sdk` (API 36 + 35, build-tools, platform-tools) |
| iOS deps | **Swift Package Manager, not CocoaPods** — system Ruby is 2.6, too old for CocoaPods |
None of it is on the default `PATH`, so **every Android command must source the env first**:
```bash
. mobile/scripts/env.sh
```
Skip that and Gradle fails with the misleading *"Unable to locate a Java Runtime."*
`release.sh` does it for you.
To rebuild the toolchain on a new Mac, `scripts/env.sh` carries the exact reinstall commands
in its error messages.
---
## iOS one-time setup
**Signing is DONE** (2026-09-14). Team **LIVING WATERS CHURCH**, Team ID `M87US675B3`, in
`mobile/ios-signing.config`. `release.sh --ios` produces a signed App Store `.ipa`
unattended — `-allowProvisioningUpdates` created the distribution certificate, the
provisioning profile and the App ID registration on its own.
If you ever set this up again (new Mac, new team): sign into Xcode ▸ Settings ▸ Accounts,
then put the Team ID in `ios-signing.config` and run `./release.sh --ios`. Xcode does the
rest. Finding the Team ID is the fiddly part — Xcode 26 shows your teams but never prints
their IDs, and caches nothing readable. Decode any profile you already have instead:
```bash
security cms -D -i ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles/*.mobileprovision \
| grep -A1 -E "TeamIdentifier|TeamName"
```
Still to do in the portals:
1. **App Store Connect** → Apps → **+** → New App, bundle ID `org.waterschurch.checklist`.
Needed before a build can be uploaded.
2. For unlisted distribution, request it at
[Apple's unlisted app request form](https://developer.apple.com/contact/request/unlisted-app-distribution/).
Apple approves it manually — allow a few days. Until then use **TestFlight**, which works
immediately and is fine for volunteers.
### Automated uploads (App Store Connect API key)
With a key configured, `release.sh` validates the build against Apple's acceptance checks and
uploads it — no Organizer, no Transporter. Without one it just hands you the `.ipa`.
Creating the key needs your Apple ID and 2FA, so it's yours to do:
1. [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → **Users and Access** →
**Integrations** → **App Store Connect API** → **Team Keys**.
2. **+** → name it (e.g. `Checklist release`) → Access: **App Manager** → Generate.
*(App Manager is the minimum that can upload builds. Developer is not enough.)*
3. **Download the .p8 — you get exactly one chance.** Apple never shows it again; a lost key
can only be revoked and replaced.
4. Move it where `altool` looks, keeping Apple's filename exactly:
```bash
mv ~/Downloads/AuthKey_*.p8 ~/.appstoreconnect/private_keys/
```
5. Copy two values off that page into `ios-signing.config`:
- **KEY ID** — 10 chars, in the key's own row
- **Issuer ID** — a UUID shown *above the table*, shared by every key on the team
(a classic mix-up: the Issuer ID is not the key's ID)
```
ASC_KEY_ID=XXXXXXXXXX
ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
6. Confirm it works — authenticates only, uploads nothing:
```bash
./mobile/scripts/verify-asc-key.sh
```
The `.p8` is a credential that can ship builds as your team. It lives outside the repo in
`~/.appstoreconnect/private_keys/` (chmod 600), and `ios-signing.config` is gitignored. Back
the `.p8` up next to the Android keystore — losing it is recoverable (revoke and regenerate),
but it costs you the round trip.
## Android one-time setup
Already done — the upload key exists and `release.sh` produces a signed AAB.
⚠️ **Back up `mobile/keystore/upload-keystore.jks` and `mobile/keystore.properties` off this
Mac** (password manager / encrypted drive). Play identifies every update by that key. Both
files are gitignored and excluded from the deploy tarball.
Remaining: **Play Console** → Create app → bundle ID `org.waterschurch.checklist` → set up
an internal testing track → upload the AAB → add testers by email.
### Android uploads (Play Developer API)
With a service-account key present, `release.sh` uploads the `.aab` and releases it to the
internal track by itself. Without one it just leaves the file in `dist-release/`.
**Play requires the very first bundle of a new app to go through the Console UI by hand.**
The API can only take over from the second release onward — so do one manual upload first,
then this works forever after.
1. **Play Console** → *Users and permissions* → note you'll come back here in step 5.
2. [console.cloud.google.com](https://console.cloud.google.com) → create (or pick) a project →
**APIs & Services** → enable **Google Play Android Developer API**.
3. **IAM & Admin → Service Accounts → Create**. Name it `play-release`. No Cloud roles are
needed — permission comes from Play, not from Cloud IAM.
4. On that service account → **Keys → Add key → Create new key → JSON**. Save it as:
```
mobile/play-service-account.json
```
5. Back in **Play Console → Users and permissions → Invite user**, paste the service
account's `client_email` (ends `.iam.gserviceaccount.com`). Grant it, for this app:
*Release to testing tracks* and *View app information*. Nothing more.
6. Confirm it works — opens an edit and abandons it, changing nothing:
```bash
cd mobile && node scripts/upload-play.js --dry-run
```
Pick a different track with `PLAY_TRACK=alpha ./release.sh`. Default is `internal`.
The JSON key can ship releases as you — it's gitignored, and belongs in your password manager
alongside the upload keystore.
---
## Useful commands
```bash
. mobile/scripts/env.sh # required before any Android command
cd mobile
npm run sync:web # rebuild www/ from ../server/public
npm run sync # …and push it into both native projects
npm run open:ios # open in Xcode
npm run open:android # open in Android Studio (if installed)
npm run assets # regenerate icons/splash from assets/*.png
cd mobile/android && ./gradlew :app:assembleDebug # debug APK, for a device or emulator
```
**If you regenerate assets**, re-apply the fix in
`android/app/src/main/res/mipmap-anydpi-v26/ic_launcher*.xml` — `@capacitor/assets` insets
both adaptive-icon layers by 16.7%, which stops the gradient background reaching the
launcher's mask. The files carry a comment explaining it.