mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
160 lines
6.8 KiB
Markdown
160 lines
6.8 KiB
Markdown
# iOS builds and TestFlight
|
|
|
|
The iOS app is built by [.github/workflows/release-ios.yml](../.github/workflows/release-ios.yml)
|
|
on a GitHub-hosted macOS runner and uploaded straight to TestFlight. macOS runner
|
|
minutes are free for this repository because it is public.
|
|
|
|
Unlike `android/`, which is committed and hand-maintained, **`ios/` is gitignored
|
|
and regenerated by `expo prebuild` on every run.** Never commit it, and never fix
|
|
anything by editing it - native iOS changes belong in `app.config.js` or a config
|
|
plugin, or they will be silently thrown away on the next build.
|
|
|
|
Without the secrets below the workflow still runs, but only as an unsigned
|
|
compile check; no IPA is produced.
|
|
|
|
## One-time Apple setup
|
|
|
|
You need an active Apple Developer Program membership ($99/yr). Everything here
|
|
can be done from Windows - no Mac required.
|
|
|
|
### 1. Team ID
|
|
|
|
developer.apple.com → Account → Membership details. A 10-character string like
|
|
`A1B2C3D4E5`.
|
|
|
|
### 2. Distribution certificate
|
|
|
|
Apple issues certificates against a certificate signing request. Keychain Access
|
|
is the usual way to make one, but OpenSSL works and does not need a Mac:
|
|
|
|
```bash
|
|
openssl genrsa -out dist.key 2048
|
|
openssl req -new -key dist.key -out dist.csr \
|
|
-subj "/emailAddress=you@bulwarkmail.org/CN=Bulwark Mail/C=DE"
|
|
```
|
|
|
|
Upload `dist.csr` at developer.apple.com → Certificates → **+** → **Apple
|
|
Distribution**, download the resulting `distribution.cer`, then bundle the cert
|
|
and its private key into a `.p12`:
|
|
|
|
```bash
|
|
openssl x509 -inform DER -in distribution.cer -out dist.pem
|
|
openssl pkcs12 -export -inkey dist.key -in dist.pem -out dist.p12 \
|
|
-passout pass:CHOOSE_A_PASSWORD
|
|
```
|
|
|
|
**On OpenSSL 3.x, add `-legacy` to that last command.** OpenSSL 3 defaults to
|
|
AES-256-CBC, which macOS `security import` refuses to read, and the workflow then
|
|
fails at the import step. OpenSSL 1.x has no `-legacy` flag and already writes
|
|
the format macOS wants - passing it there is an error. Check with `openssl
|
|
version` first; Git Bash on Windows currently ships 1.1.1.
|
|
|
|
Keep `dist.key` somewhere safe - you cannot re-download it, and losing it means
|
|
revoking the certificate and starting over.
|
|
|
|
### 3. App Store Connect API key
|
|
|
|
App Store Connect → Users and Access → Integrations → App Store Connect API →
|
|
**+**, with the **App Manager** role. Download the `.p8` (**one download only**)
|
|
and note the **Key ID** and the **Issuer ID** shown above the key list.
|
|
|
|
The key authenticates the TestFlight upload.
|
|
|
|
### 4. App ID, then app record
|
|
|
|
These live on two different Apple sites and have to be done in this order.
|
|
|
|
**developer.apple.com** → Certificates, Identifiers & Profiles → Identifiers →
|
|
**+** → App IDs → App. Explicit bundle ID `org.bulwarkmail.mobile`, matching
|
|
`ios.bundleIdentifier` in [app.config.js](../app.config.js). Leave every
|
|
capability off: push is not implemented on iOS, and sign-in uses the custom
|
|
`bulwarkmobile://` scheme rather than universal links, so no Associated Domains.
|
|
|
|
**appstoreconnect.apple.com** → Apps → **+** → New App. Platform iOS, any SKU.
|
|
The bundle ID here is a dropdown populated from the identifiers above, which is
|
|
why the App ID has to be registered first. The app name must be unique across
|
|
the entire App Store.
|
|
|
|
The TestFlight upload is rejected if the app record does not exist - and that
|
|
failure lands at the very end of a ~30 minute build.
|
|
|
|
### 5. App Store provisioning profile
|
|
|
|
developer.apple.com → Profiles → **+** → Distribution → **App Store Connect**.
|
|
Pick the App ID from step 4 and the distribution certificate from step 2, name it
|
|
`Bulwark Mobile App Store`, and download the `.mobileprovision`.
|
|
|
|
The build signs **manually** and this profile is not optional. Automatic signing
|
|
would be less to maintain, but `xcodebuild archive` under automatic signing always
|
|
signs with a *development* identity and leans on `-exportArchive` to re-sign for
|
|
distribution afterwards. That needs a development certificate and at least one
|
|
registered device - neither of which a CI-only TestFlight pipeline has any reason
|
|
to own, and Apple will not issue a development profile without a device. Naming
|
|
the App Store profile up front avoids the whole problem.
|
|
|
|
The profile expires when the certificate does (2027-08-01). Regenerate it here and
|
|
update the secret when that happens.
|
|
|
|
## Repository secrets
|
|
|
|
Settings → Secrets and variables → Actions:
|
|
|
|
| Secret | Value |
|
|
| --- | --- |
|
|
| `APPLE_TEAM_ID` | The 10-character team ID from step 1 |
|
|
| `IOS_DIST_CERT_BASE64` | `dist.p12` from step 2, base64-encoded |
|
|
| `IOS_DIST_CERT_PASSWORD` | The password you chose for `dist.p12` |
|
|
| `APP_STORE_CONNECT_KEY_ID` | Key ID from step 3 |
|
|
| `APP_STORE_CONNECT_ISSUER_ID` | Issuer ID from step 3 |
|
|
| `APP_STORE_CONNECT_PRIVATE_KEY` | The `.p8` from step 3, base64-encoded |
|
|
| `IOS_PROVISIONING_PROFILE_BASE64` | The `.mobileprovision` from step 5, base64-encoded |
|
|
|
|
To base64-encode a file:
|
|
|
|
```powershell
|
|
# PowerShell
|
|
[Convert]::ToBase64String([IO.File]::ReadAllBytes('dist.p12')) | Set-Clipboard
|
|
```
|
|
|
|
```bash
|
|
# bash
|
|
base64 -w0 dist.p12
|
|
```
|
|
|
|
## Running a build
|
|
|
|
Publishing a GitHub release triggers the iOS workflow alongside the Android one.
|
|
You can also run it by hand from the Actions tab, with an
|
|
`upload_to_testflight` toggle if you only want the IPA as a workflow artifact.
|
|
|
|
The IPA is deliberately not attached to the GitHub release: unlike the APK, it is
|
|
not installable outside TestFlight.
|
|
|
|
`CFBundleVersion` comes from the workflow run number via the `IOS_BUILD_NUMBER`
|
|
env var. App Store Connect rejects a build number it has already seen for a given
|
|
version, so never re-run a workflow expecting a re-upload to succeed - the run
|
|
number advances on its own, so just start a new run.
|
|
|
|
## After the upload
|
|
|
|
Processing takes roughly 5-30 minutes before the build shows up in TestFlight.
|
|
Internal testers (up to 100, must be on your App Store Connect team) get it
|
|
immediately. External testers (up to 10,000) need a Beta App Review, which takes
|
|
about a day the first time and is usually waved through afterwards.
|
|
|
|
## What does not work on iOS yet
|
|
|
|
- **Push notifications.** The push path is a hand-written Android module talking
|
|
to FCM (`BulwarkFcmModule`, `BulwarkMessagingService`). iOS needs an APNs key
|
|
and an equivalent native module; until then `push-notifications.ts` short-
|
|
circuits and the app polls like a device with push disabled.
|
|
- **Client certificates.** `BulwarkClientCertModule` is Android-only.
|
|
`secureFetch` falls back to plain `fetch`, so cert-authenticated servers will
|
|
not connect.
|
|
- **In-app updates.** Hidden on purpose - see `supportsSideloadUpdates` in
|
|
[src/lib/platform-capabilities.ts](../src/lib/platform-capabilities.ts).
|
|
Downloading and installing a binary is forbidden by App Store Review guideline
|
|
2.5.2, so iOS updates come through TestFlight instead. The Updates settings
|
|
pane is removed from the list on iOS; the version and commit are still shown
|
|
under About & Data.
|