diff --git a/README.md b/README.md index 151ecf67..37e7b99a 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ just ask us on [Discord](https://discord.gg/8Vd7jzqjun). - [x] Set up domain name (compassmeet.com) - [ ] Cover more than 90% with tests (unit, integration, e2e) - [x] Add Android mobile app -- [ ] Add iOS mobile app +- [x] Add iOS mobile app - [x] Add better onboarding (tooltips, modals, etc.) - [ ] Add modules to learn more about each other (personality test, conflict style, love languages, etc.) - [ ] Add modules to improve interpersonal skills (active listening, nonviolent communication, etc.) diff --git a/common/src/constants.ts b/common/src/constants.ts index 6b693bfd..79097161 100644 --- a/common/src/constants.ts +++ b/common/src/constants.ts @@ -24,10 +24,12 @@ export const ANDROID_APP_URL = 'https://play.google.com/store/apps/details?id=com.compassconnections.app' /** * App Store listing. The numeric id is assigned by App Store Connect when the app record is - * created, so this is a placeholder until the first submission — see `docs/ios.md` §7. Anything - * linking here should be gated on `IOS_APP_URL` no longer containing `APPLE_ID`. + * created — see `docs/ios.md` §7. Left without a country segment on purpose: Apple redirects a + * bare `/app/…` link to the visitor's own storefront, which is what the localised app wants. + * Anything linking here should still be gated on `IOS_APP_URL` no longer containing `APPLE_ID`, + * so the placeholder can never reach a user if this is ever reset. */ -export const IOS_APP_URL = 'https://apps.apple.com/app/compass/idAPPLE_ID' +export const IOS_APP_URL = 'https://apps.apple.com/app/compass-social-connections/id6804429364' export const IS_IOS_APP_PUBLISHED = !IOS_APP_URL.includes('APPLE_ID') /** diff --git a/docs/ios.md b/docs/ios.md index ccf67723..00008978 100644 --- a/docs/ios.md +++ b/docs/ios.md @@ -13,9 +13,16 @@ React Native, no second UI codebase. `web/` stays the single source of the produ ## 0. Status Everything that is code — the web-side platform branching, the backend payload, the Xcode project, the -release pipeline — is written and in the repo, and the whole Linux-side setup is done. The Apple -Developer Program membership is **active** (team `HFZVH8XR59`), so the console work is unblocked. What -is left is that console work, one Mac session, and the on-device checklist. +release pipeline — is written and in the repo, the Apple Developer Program membership is **active** +(team `HFZVH8XR59`), and the app is **live on the App Store** (id `6804429364`). What is left is the +on-device checklist. + +**None of it needed a Mac.** Every local step ran on an ordinary Linux box; the only macOS anywhere in +the story is the GitHub Actions `macos-15` runner that seeds the certificates (`ios-certs.yml`) and +archives the build (`cd-ios.yml`). No Mac was bought, borrowed or rented — the EC2 Mac day sketched in +[§2.1](#21-working-without-a-mac) was planned and never used, and everything that looked like it needed +Xcode's GUI turned out to be either a portal page or a text file. §2.1 has the full account of what ran +where. Operational detail for all of it is in [`../ios/README.md`](../ios/README.md). @@ -88,7 +95,7 @@ remains is console configuration and the first build, not values to paste into t fine for TestFlight, not for a build anyone can install. Nothing fails if it is left on, which is exactly why it needs to be on a checklist. -### To do — needs the Apple consoles, a Mac, or the phone +### To do — needs the Apple consoles or the phone - [x] Register the App ID `com.compassconnections.app` with Push Notifications, Associated Domains and Sign in with Apple enabled, and create the app record in App Store Connect. @@ -121,13 +128,16 @@ remains is console configuration and the first build, not values to paste into t - [ ] Work through [On-device verification](#on-device-verification-first-testflight-build) below. Internal TestFlight needs no Beta App Review, so builds are installable minutes after processing. - [x] App Store Connect app record and listing metadata — everything but the build. -- [ ] Once live: put the real App Store id in `IOS_APP_URL` (`common/src/constants.ts`) and add the - App Store row next to "Get it on Google Play" in `web/components/nav/sidebar.tsx`. +- [x] Live: `IOS_APP_URL` (`common/src/constants.ts`) carries the real App Store id `6804429364`, + which flips `IS_IOS_APP_PUBLISHED` and with it the /download badge, the about-page copy and the + Smart App Banner. The sidebar needs no separate App Store row — it resolves per device through + `useAppDownload`. -Everything left is a web console or a GitHub Actions run. **The EC2 Mac day described in §2.1 is no -longer on the critical path**: certificate seeding moved to the `iOS Certificates (one-off)` workflow, -the archive is `cd-ios.yml`, and the App ID capability toggles are portal pages rather than Xcode. Rent -a Mac only if something needs interactive Xcode debugging. +Everything left is a web console or a GitHub Actions run. **The EC2 Mac day described in §2.1 was +never needed**: certificate seeding is the `iOS Certificates (one-off)` workflow, the archive is +`cd-ios.yml`, and the App ID capability toggles turned out to be portal pages rather than Xcode. Rent a +Mac only if something ever needs interactive Xcode debugging — through submission and release, nothing +has. ### Still open in the codebase @@ -138,7 +148,8 @@ Neither blocks TestFlight, both should land before a public release: them. This is the last place the block toast's promise is not kept — see §8.4. - [ ] **No workflow invokes the `symbolicate` lane.** `Fastfile:105` says to run it on the CI runner, but `cd-ios.yml`'s `workflow_dispatch` runs `beta`. The dSYMs are archived (`cd-ios.yml:113`); - there is just no way to reach a Mac to use them. + what is missing is a dispatch path that runs `symbolicate` on the `macos-15` runner. That runner + is the only macOS we have, and it is enough — this does not need a Mac, just a workflow input. ### On-device verification (first TestFlight build) @@ -243,15 +254,17 @@ default: the product is meant to look the same on both. `isNativeMobile()` was a ## 2. Prerequisites (hard blockers) -- **macOS, but not necessarily a Mac you own.** Xcode is macOS-only and there is no supported way to build - or sign an iOS app without it — but that macOS can be a `macos-latest` GitHub Actions runner (billed at - 10× Linux minutes) or a rented remote Mac. See [§2.1](#21-working-without-a-mac) — the whole - build → sign → TestFlight loop is drivable from Linux/Windows. +- **macOS, but only as a CI runner.** Xcode is macOS-only and there is no supported way to build or sign + an iOS app without it — but for the whole of this project that macOS was a `macos-15` GitHub Actions + runner (billed at 10× Linux minutes), never a machine anyone sat in front of. See + [§2.1](#21-working-without-a-mac): the entire build → sign → TestFlight → App Store loop is drivable + from Linux. - **Apple Developer Program membership** — $99/year. Not just for shipping: the Push Notifications and Sign in with Apple **entitlements are unavailable on a free account**, so the two features that make this more than a wrapped website can't even be built without it. - **A physical iPhone** — see below. The Simulator is not sufficient. -- Xcode 16+, CocoaPods (`sudo gem install cocoapods`), Node 22+, Java is _not_ needed. +- Node 22+ locally; Java is _not_ needed. Xcode 16+ and CocoaPods are only ever installed on the CI + runner — on Linux `pod install` and `xcodebuild` no-op and everything else in the sync still runs. ### Test device @@ -285,10 +298,24 @@ top of the profile page and the filter sheet on whatever you get. ### 2.1 Working without a Mac -We develop on Linux and Windows. Buying a Mac is not a prerequisite — the plan below assumes a physical -iPhone (above) plus cloud/rented macOS for the steps that genuinely need Xcode. +We develop on Linux. This section was written as a plan; it is now a record. **The app was built, signed, +submitted and published without a Mac** — none bought, none rented, none borrowed. The only macOS involved +was the GitHub Actions `macos-15` runner. What ran where, in the end: + +| Step | Where it ran | +| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| Web build, `npx cap sync ios`, icon generation (`yarn build-sync-ios`) | Linux, locally | +| Editing `project.pbxproj`, `Info.plist`, `App.entitlements`, `Podfile` | Linux, in a normal editor | +| App ID + capabilities, Services ID, APNs `.p8`, Firebase, App Store Connect record and metadata | developer.apple.com / appstoreconnect.apple.com / Firebase, in a browser | +| Certificate + provisioning profile creation (`match`) | `macos-15` runner, once — `ios-certs.yml` | +| `pod install`, `xcodebuild archive`, TestFlight and App Store upload | `macos-15` runner — `cd-ios.yml` | +| Getting builds onto the phone | TestFlight app, over the air | +| Device logs, JS console, DOM, network, crash reports | Linux, over USB (`libimobiledevice`, `ios-webkit-debug-proxy`) | + +The two steps that genuinely require macOS — creating certificates and running `xcodebuild` — are each one +lane in a workflow file. "Needing a Mac" reduces to "needing a runner", and a runner is a YAML line. -**Build and ship from CI.** The `macos-latest` runner in §7 does `npx cap sync ios`, CocoaPods, `xcodebuild`, +**Build and ship from CI.** The `macos-15` runner in §7 does `npx cap sync ios`, CocoaPods, `xcodebuild`, fastlane `match` (certs/profiles) and `pilot` (TestFlight upload). Triggering it is just pushing a commit and setting secrets, so the whole release path is Mac-free. Install builds on the iPhone over the air via the **TestFlight app** — no cable, no Xcode, and the resulting build is a real signed one, so the APNs work in §6 @@ -314,21 +341,31 @@ phone, run the proxy over USB, and attach Chrome DevTools to the WKWebView. That Web Inspector — JS console, DOM, network — which matters a lot for a webview app. It is a community project and can be picky about iOS/tool version pairings, so budget some setup time. -**What still needs a real interactive macOS session:** +**What we expected to need an interactive macOS session for — and didn't:** -- First-time Apple Developer setup done through Xcode's UI — Associated Domains (§4.1), Sign in with Apple - (§5), push entitlement (§6). All scriptable/hand-editable, but error-prone blind; - expect to burn CI cycles on provisioning-profile mismatches Xcode would have shown visually. -- The first App Store Connect submission and its metadata (§7). +- _First-time Apple Developer setup, assumed to be Xcode's UI_ — Associated Domains (§4.1), Sign in with + Apple (§5), push entitlement (§6). All three are toggles on the App ID in the developer portal, and the + matching `App.entitlements` is text. Xcode's capability editor is a convenience that writes those two + things for you; doing it directly is fine. The provisioning-profile mismatch cycles budgeted for here did + not materialise — `match` regenerates the profile from the App ID, so the entitlements and the profile + cannot drift the way they do when a human keeps both in sync by hand. +- _The first App Store Connect submission and its metadata (§7)_ — also entirely a web console. The build + came from `cd-ios.yml` like every one since; the listing, screenshots and review notes are forms. - **Crash symbolication** — turning a `.ips` from `idevicecrashreport` into a readable stack trace needs the - `.dSYM` and Xcode's `symbolicatecrash`. Do it on the CI runner as a step, or on a rented Mac. + `.dSYM` and Xcode's `symbolicatecrash`. This is the one item still open, and the fix is a step on the CI + runner (the `symbolicate` lane already in `Fastfile`), not a Mac. -**Getting that macOS time — AWS EC2 Mac.** This is what we use for the one-time setup. Be aware of the -shape of the bill before starting: EC2 Mac runs on **dedicated hosts with a 24-hour minimum allocation**, -so the smallest possible session costs a full day (`mac2.metal`, Apple silicon, ~$0.65/hr on-demand ≈ **$16 -for that minimum**), and the host keeps billing until it is _released_, not merely until the instance is -stopped. Releasing is also rate-limited by the same 24-hour floor. Budget one day, do all of §4.1, §5, §6, -§7 in it, then release the host. +The one thing a Mac would still buy is interactive Xcode: Simulator, Instruments, breakpoints in Swift. We +have not wanted it. The native layer is `AppDelegate.swift` and nothing else, everything above it is a +WebView that Chrome DevTools attaches to over USB from Linux, and the Simulator cannot do the one test that +mattered (§6 push) anyway. + +**The EC2 Mac day we planned and never used.** Kept here as the fallback if interactive Xcode ever does +become necessary — and because the shape of the bill is worth knowing before starting. EC2 Mac runs on +**dedicated hosts with a 24-hour minimum allocation**, so the smallest possible session costs a full day +(`mac2.metal`, Apple silicon, ~$0.65/hr on-demand ≈ **$16 for that minimum**), and the host keeps billing +until it is _released_, not merely until the instance is stopped. Releasing is also rate-limited by the same +24-hour floor. Budget one day and batch everything into it. Setup, from Linux: @@ -366,8 +403,9 @@ sudo gem install cocoapods For the GUI parts (capability toggles, App Store Connect), enable Screen Sharing and tunnel VNC over SSH — `ssh -L 5900:localhost:5900 ec2-user@` — rather than exposing 5900. -Recommendation: EC2 Mac for the one-time setup and first submission, CI (`macos-15` runner) for every -build after that, cable + `ios-webkit-debug-proxy` for day-to-day debugging. +Recommendation, in hindsight: skip it. CI (`macos-15` runner) for the certificate seeding and every build, +a browser for the Apple and Firebase consoles, cable + `ios-webkit-debug-proxy` for day-to-day debugging. +That covered the whole project, from `npx cap add ios` to a live App Store listing. --- @@ -653,17 +691,17 @@ Notes: ## 7. Build, sign, ship -Local (on macOS): +Locally, on Linux — this is the whole local half: ```bash -yarn build-web-view -npx cap sync ios -npx cap open ios # then Product → Archive +yarn build-sync-ios # web export + `npx cap sync ios` + icons. No archive step exists here. +git push # a bumped CURRENT_PROJECT_VERSION on `main` triggers cd-ios.yml ``` -From Linux/Windows there is no local archive step — push and let the `macos-latest` job below do it -([§2.1](#21-working-without-a-mac)). Everything up to `npx cap sync ios` still runs fine locally; it's only -`xcodebuild`/Archive that needs macOS. +There is no local archive and never was: push and let the `macos-15` job below do it +([§2.1](#21-working-without-a-mac)). Everything up to `npx cap sync ios` runs fine on Linux; only +`pod install` and `xcodebuild`/Archive need macOS, and both live in CI. On a Mac the equivalent would be +`npx cap open ios` → Product → Archive — we have never run it. `yarn build-sync-ios` (mirroring `scripts/build_sync_android.sh`) does the first two, plus `npx capacitor-assets generate --ios` — the generated icons are gitignored, exactly as on Android, so they @@ -704,10 +742,12 @@ One thing the lane does that is easy to miss: it rewrites `aps-environment` from `development` registers against the _sandbox_ APNs environment, and pushes then silently never arrive — there is no error anywhere to notice. -Do the first submission by hand from Xcode to shake out the metadata, then automate. This is the step worth -renting a Mac for — App Store Connect metadata, screenshots and capability toggles are fiddly enough that -doing them blind through CI on the first pass invites a rejection round. Add a `symbolicatecrash` step to the -same runner so crash reports pulled off the device from Linux can be symbolicated. +The first submission went through the App Store Connect **web console**, not Xcode: the build came from +`cd-ios.yml` exactly like every one since, and the metadata, screenshots and review notes are browser forms. +The rejection round warned about here did happen — `ITMS-90129`, a `CFBundleDisplayName` collision (§8.8) — +but it is a name clash Apple's upload check reports by email, and sitting at a Mac would not have surfaced it +any earlier. Still worth doing: add a `symbolicatecrash` step to the same runner so crash reports pulled off +the device from Linux can be symbolicated. --- @@ -792,7 +832,8 @@ Steps 1–4, 7 and the code half of 6 and 9 are done — see [§0](#0-status). W 2. ~~Move deep-link and notification-tap handling onto `@capacitor/app` + `pushNotificationActionPerformed`.~~ Done. 3. ~~Add the `apns` block to `sendPushToToken`.~~ Done — harmless on Android, prerequisite for iOS. -4. ~~`npx cap add ios`.~~ Done. Getting it running in the Simulator needs the Mac session. +4. ~~`npx cap add ios`.~~ Done. The Simulator was never used — TestFlight over the air was the way builds + reached the phone, and the Simulator cannot obtain an APNs token anyway (step 9). 5. Apple Developer Program, App ID with the three capabilities, App Store Connect app record. 6. ~~Firebase iOS app; Google iOS OAuth client.~~ Done. Left: the APNs `.p8` key and the Apple provider in Firebase Auth (§5.2), both of which need the membership from step 5. @@ -802,14 +843,15 @@ Steps 1–4, 7 and the code half of 6 and 9 are done — see [§0](#0-status). W first TestFlight build; internal testing on the iPhone 11. 9. Verify push end-to-end on the **physical device** — the Simulator has no APNs token, so this step cannot be faked — then the rest of the on-device checklist - in [§0](#to-do--needs-an-apple-account-a-mac-or-the-phone). + in [§0](#to-do--needs-the-apple-consoles-or-the-phone). 10. App Store Connect metadata and first submission. -Since we develop on Linux ([§2.1](#21-working-without-a-mac)), CI replaces the Simulator as the way to get a +Since we develop on Linux ([§2.1](#21-working-without-a-mac)), CI replaced the Simulator as the way to get a build onto the phone: get the fastlane half of step 8 standing early, then do 9 against TestFlight builds -with `ios-webkit-debug-proxy` attached over cable. Batch every Xcode-GUI-only item — the capability toggles -for §4.1, §5 and §6 — into the one EC2 Mac day. Seeding the match repo is no longer among them: the -`iOS Certificates (one-off)` workflow does it on a GitHub `macos-15` runner. +with `ios-webkit-debug-proxy` attached over cable. The Xcode-GUI-only items that were supposed to force an +EC2 Mac day — the capability toggles for §4.1, §5 and §6 — were portal pages all along, and seeding the match +repo is the `iOS Certificates (one-off)` workflow on a GitHub `macos-15` runner. The Mac day never happened, +and steps 1–10 completed without one. --- diff --git a/docs/releases.md b/docs/releases.md index 5247561c..08d3c8ce 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -59,7 +59,8 @@ instant. - **Android** — bump `versionCode` (and `versionName`) in `android/app/build.gradle`, push to `main`; `cd-android.yml` builds the signed AAB and uploads to Play. - **iOS** — bump `CURRENT_PROJECT_VERSION` (and `MARKETING_VERSION`) in `project.pbxproj`, push to - `main`; `cd-ios.yml` builds on a `macos-15` runner and uploads to TestFlight. See + `main`; `cd-ios.yml` builds on a `macos-15` runner and uploads to TestFlight. That runner is the only + macOS in the pipeline — releasing iOS from a Linux box is just a commit. See [`../ios/README.md`](../ios/README.md) §4. Every `CHANGELOG.md` entry must start with `## ` matching root `package.json` exactly — the diff --git a/ios/CLAUDE.md b/ios/CLAUDE.md index 53c54975..36a5dd49 100644 --- a/ios/CLAUDE.md +++ b/ios/CLAUDE.md @@ -7,6 +7,12 @@ See [README.md](README.md) for build, signing, TestFlight and debugging, and [`../docs/ios.md`](../docs/ios.md) for the plan and the remaining manual steps. Cross-package context is in the [root CLAUDE.md](../CLAUDE.md). +**Assume Linux, not a Mac.** This app was developed, signed, submitted and published without one. Work +locally on Linux — `project.pbxproj`, `Info.plist` and `App.entitlements` are text, `yarn build-sync-ios` +runs fine (`pod install` and `xcodebuild` just no-op), and the phone is debuggable over USB. The only +macOS is the `macos-15` runner in `ios-certs.yml` (certificates, run once) and `cd-ios.yml` (archive + +upload). Don't propose "open it in Xcode" as a step; there is no Xcode here. + ## What's here vs not here - Native scaffolding (`App/`, `Podfile`, `Info.plist`, `App.entitlements`, `AppDelegate.swift`). diff --git a/ios/README.md b/ios/README.md index 83ed94ff..3fc54595 100644 --- a/ios/README.md +++ b/ios/README.md @@ -4,7 +4,12 @@ Capacitor wrapper that loads the Next.js static export into a WKWebView — the [`../android`](../android/README.md), the same web bundle, no second UI codebase. The plan and the reasoning behind it live in [`../docs/ios.md`](../docs/ios.md). This file is the -operational half: how to build, sign and ship, and what still has to be done by hand on a Mac. +operational half: how to build, sign and ship. + +**No Mac is involved.** Everything local runs on Linux; the only macOS is the `macos-15` GitHub Actions +runner that seeds the certificates and archives the build. The app went from `npx cap add ios` to a live +App Store listing without anyone touching a Mac — see [`../docs/ios.md`](../docs/ios.md) §2.1 for the +breakdown of what ran where. --- @@ -50,13 +55,15 @@ Firebase console → Project settings → iOS app and drop it in `ios/App/App/`. ## 3. Building -From the repo root — works on Linux for everything except the archive: +From the repo root — this is the whole local build, and it runs on Linux: ```bash yarn build-sync-ios # build the web export, cap sync ios, regenerate icons ``` -Then on macOS: +`pod install` and `xcodebuild` no-op on Linux; the sync still does everything that matters. The archive +happens in CI (§4) — there is no local equivalent and we have never needed one. On a Mac you would follow +with: ```bash cd ios/App && bundle exec pod install @@ -84,7 +91,8 @@ Only the *marketing* version is worth syncing. `CURRENT_PROJECT_VERSION` and And are per-store upload counters with separate histories — iOS is at 1 while Android is past 160 — and both must strictly increase per upload. Forcing those to match would be actively harmful. -Locally (on a Mac with the secrets in the environment): +The `beta` lane can also be run by hand, but only on a Mac with the secrets in the environment — it ends +in `xcodebuild`. We have never had to; `workflow_dispatch` on `cd-ios.yml` re-runs it on the runner: ```bash cd ios && bundle exec fastlane beta @@ -134,9 +142,11 @@ distinguishes "the certificate exists and is usable" from the failure mode descr Run it once, ever. Apple issues a team three distribution certificates; re-running where match cannot see the existing one creates another and eventually exhausts them. -#### Why not from a laptop +#### Why not from Linux -Creating certificates needs macOS, and the way it fails on Linux is expensive rather than obvious. +This is the one step where Linux does not merely no-op. Creating certificates needs macOS, and the way +it fails on Linux is expensive rather than obvious — which is why it lives in a workflow rather than in +anyone's shell. `match` runs fine on Linux right up to the end: it clones the repo, decrypts it, asks App Store Connect for a certificate, and creates a provisioning profile. It then tries to *install* that profile and dies @@ -249,17 +259,29 @@ iOS/tool version pairings. Ubuntu ships libimobiledevice 1.3.0 (2020), which pre libimobiledevice from git master too rather than debugging the packaged one. Budget an afternoon the first time; this is the least reliable link in the Linux workflow. -## 6. What still needs a Mac +## 6. Where macOS is actually needed + +Two commands, both already in workflow files: + +| Needs macOS | Runs in | +| ---------------------------------------- | -------------------------------------------------------- | +| `match` creating certs/profiles (once) | [`ios-certs.yml`](../.github/workflows/ios-certs.yml) | +| `pod install` + `xcodebuild archive` | [`cd-ios.yml`](../.github/workflows/cd-ios.yml) | -Everything above except `xcodebuild`/Archive runs on Linux, and the release path is CI. A real -interactive macOS session is needed for: +Everything else — the web build, `cap sync`, editing `project.pbxproj` / `Info.plist` / +`App.entitlements`, the Apple and Firebase consoles, installing builds via TestFlight, device logs, the JS +console, crash-report retrieval — is Linux or a browser. The things this file used to list as needing an +interactive Mac did not: -- the one-time Apple Developer setup done through Xcode's UI (capability toggles that must match - `App.entitlements`), -- the first App Store Connect submission and its metadata, -- crash symbolication, if you'd rather not use the `symbolicate` fastlane lane on the runner. +- **Apple Developer capability setup** is toggles on the App ID in the portal plus a text entitlements + file; Xcode's capability editor only writes those two things for you. +- **The first App Store Connect submission and its metadata** is a web console; the build came from CI. +- **Crash symbolication** is the `symbolicate` fastlane lane, which belongs on the runner. Nothing invokes + it yet — the only genuinely open item ([`../docs/ios.md`](../docs/ios.md) §0). -See [`../docs/ios.md`](../docs/ios.md) §2.1 for how to get that time on an AWS EC2 Mac instance. +A Mac would still buy interactive Xcode — Simulator, Instruments, Swift breakpoints. The native layer is +one file (`AppDelegate.swift`) and the Simulator cannot obtain an APNs token, so it has not been worth it. +[`../docs/ios.md`](../docs/ios.md) §2.1 keeps the AWS EC2 Mac recipe as a fallback, unused. ## 7. Caveats