Bring the checklist app under version control
The project has lived only as a folder on the Mac Studio and, briefly, as a transfer tarball. This is the first commit: the Express server and its web front end, the Capacitor iOS and Android wrappers, and the release and backup scripts. The repo is the source, not a backup of the machine. Left out on purpose, by the .gitignore files that were already in the tree: node_modules, build output, mobile/www (regenerated from server/public on every build), and server/data, which holds the live checklists and belongs on the NAS rather than in git. Every credential is excluded too — server/.env, the Play upload keystore and its properties file, the App Store Connect key and ios-signing.config. The one secret that was not in an ignored file was the NAS SSH password, hard-coded in build-deploy.sh and grepped back out of it by backup-to-drive.sh. Both now read it from the login keychain (service checklist-nas, account Christian), falling back to NAS_PASS in the environment, so rotating it is one keychain entry rather than a history rewrite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user