Skip to content

macOS · iOS build environment

Bootstrapping the Mac so it can build, run and verify the merchant app on iOS. Companion to Windows Build Environment (the Android side) and Parity Verification (what to check once it runs).

Why this exists

iOS native moved from R2 into R1 on 2026-07-26 (ADR-0011 amendment). Every change is now verified on Android native, iOS native and the Web PWA before it is done, so the Mac is part of the normal loop, not a release-time afterthought.

No Apple Developer Program membership is needed to develop

This is the fact that made moving iOS into R1 free:

CapabilityFree (Personal Team)Needs $99/yr
iOS Simulator✅ no Apple account at all
Install on your own iPhone✅ Personal Team signing
Profile validity7 days, then the app refuses to launch — re-run to re-sign1 year
Apps per device3unlimited
TestFlight / App Store
Push notifications
Associated Domains (iOS universal links)✅ — this is why qrsetu:// is the iOS interim scheme

Free signing is fine for development and parity verification, and unusable for sustained parallel QA. Buy the membership when distribution is needed, not before.

Prerequisites

Per the CLAUDE.md toolchain rule — verify each is present at the right version before running any build step.

A clean macOS has no shell profile — do this first

~/.zshrc and ~/.zprofile do not exist on a fresh Mac. The nvm and Homebrew installers only append to a profile that already exists, so without these two lines they report "profile not found", wire themselves into nothing, and every following command fails with command not found. This costs an hour if you meet it blind.

bash
touch ~/.zshrc ~/.zprofile

1 · Xcode

Install from the App Store, then:

bash
sudo xcodebuild -license accept
xcode-select -p              # must print /Applications/Xcode.app/Contents/Developer
xcodebuild -version          # must be 16.1+ (RN 0.86's floor)

At Xcode's first-launch platform prompt, select iOS and macOS only. Deselect watchOS, tvOS and visionOS — you will never build for them and they cost 20–30 GB, which is real money on a 256 GB machine.

Re-check xcode-select -p after ANY Command Line Tools install

Installing the CLT — including as a side effect of installing Homebrew — often repoints the active developer directory at /Library/Developer/CommandLineTools. pod install and expo prebuild then fail with errors that look like Expo bugs and are not. Fix:

bash
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

2 · Node 22, via nvm

nvm does not ship with macOS:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.zshrc
command -v nvm               # must print "nvm"

nvm is a shell function, not a binarywhich nvm prints nothing even when it works. Always use command -v nvm.

Then, from the repo root (nvm install with no argument reads .nvmrc, which contains 22):

bash
nvm install && nvm use && node -v

3 · Homebrew, then the build tools

bash
command -v brew || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Apple Silicon installs to /opt/homebrew, which is NOT on PATH by default:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
brew --version

brew install gh cocoapods watchman

The Homebrew installer asks for your macOS password (sudo) and may pull the Command Line Tools — see the xcode-select warning above.

4 · GitHub access

The repo is private, so authentication is needed even to clone. GitHub has not accepted passwords for git over HTTPS since 2021 — use the CLI's browser flow, which stores and rotates nothing:

bash
gh auth login       # GitHub.com → HTTPS → Yes (authenticate git) → Login with a web browser
gh auth status

A fine-grained PAT scoped to this repo is the fallback. Never embed it in the remote URL — it lands in .git/config in plaintext and in your shell history; let the macOS Keychain credential helper hold it.

First build

Do not follow a half-remembered sequence here — the full walkthrough, including the iPhone X iOS-version floor, free code signing, and device install, is iOS Build & Device Testing. Start there once the prerequisites above are in place.

ios/ is generated — never edit it

apps/mobile/ios/ and apps/mobile/android/ are both gitignored. Anything you change inside them is destroyed by the next expo prebuild and never reaches another machine or CI.

So all native configuration lives in app.json (or a config plugin):

  • expo.ios.bundleIdentifierin.digious.qrsetu, permanent once published
  • expo.ios.buildNumber — the iOS counterpart of android.versionCode; both are explicit in app.json so the file stays the single source of truth and CI owns the bump
  • expo.plugins — splash, Sentry, and anything else needing native changes

This is the same lesson the Android gradle.properties incident taught: a native file edited by hand is a change that exists on exactly one machine.

No build-ios.mjs

Android has scripts/build-android.mjs because Windows has a real pathology to fight — C:-drive artefact spill and Gradle OOM, needing arm64-only builds and capped parallelism. macOS has none of those problems. expo run:ios and xcodebuild work as shipped, so a wrapper script would be pure maintenance debt. Use the Expo commands directly.

Versioning

app.json is the single source of truth for both platforms:

FieldPlatformRole
expo.versionbothMarketing version (1.0.0)
expo.android.versionCodeAndroidInteger, must increase per Play upload
expo.ios.buildNumberiOSString, must increase per App Store upload

CI owns the bump. Do not hand-edit these to make a local build work — see the "Versioning" section of apps/mobile/README.md.

Measuring IPA size

The app-size budget applies to iOS too. After a release build, record the IPA size alongside the APK figure in the mobile README's size table. The RN/Hermes native runtime dominates on both platforms, so the numbers should track each other; a sudden divergence means a native module landed on one platform only — which is itself a parity bug.

Troubleshooting (environment only)

Build- and device-level problems are troubleshooted in iOS Build & Device Testing. Only toolchain issues belong here.

Freeing disk space is an environment action with a device-level consequence

Reclaiming space on the Mac can make the iPhone vanish from Xcode's destination list, because the pairing records, signing certificate and device-support symbols all live under paths a size-sorted cleanup will happily delete. Clean only from the allowlist in Cleanup is allowlist-only, and if the phone has already disappeared, work the recovery ladder rather than reinstalling anything. QRS-232.

xcodebuild reports the CommandLineTools path — point it at the real Xcode: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer.

pod not found after brew install cocoapods — open a new shell so the Homebrew path is picked up, or eval "$(/opt/homebrew/bin/brew shellenv)".

Node version drifts between shells — this repo pins Node via .nvmrc; run nvm use in each new terminal, or enable nvm's automatic switching.