feat: iOS build config for TestFlight + Expo SDK 57 upgrade #8

Open
john wants to merge 4 commits from feat/ios-testflight-build into dev
Owner

First native build config for this app. eas build:list was empty — nothing here has been exercised on a real device yet.

What is in here

Expo SDK 56 → 57 (commit 1) — expo-doctor 20/20, tsc clean. Three SDK-57 API adaptations: StyleSheet.absoluteFillObjectStyleSheet.absoluteFill, tighter expo-router Link href typing, narrowed StatusChip state prop.

iOS build config (commit 2):

  • app.json — register the expo-camera and expo-image-picker config plugins. Without them the production build ships no NSCameraUsageDescription and camera access crashes on device. Permission strings are specific; App Review rejects vague ones. The unused NSMicrophoneUsageDescription and NSFaceIDUsageDescription that expo-camera and expo-secure-store add by default are suppressed — the app records no audio and uses no biometrics. usesNonExemptEncryption: false — HTTPS-only, skips the export-compliance prompt on every TestFlight upload.
  • eas.jsonappVersionSource: remote plus autoIncrement on production, so build numbers live in EAS rather than being committed. EXPO_PUBLIC_API_URL set per profile.
  • .easignore — new file. EAS reads this instead of .gitignore. api/ and openapi.json are gitignored build artifacts, but Metro cannot resolve @/api/* without them, so a cloud build would have failed at bundling.

Bugs found on the way

Broken today, not only in the native build. The orval-generated paths already start with /api/, so baseURL must not repeat it.

Site Was Effect
lib/api-instance.ts fallback https://superclean.haskytech.com/api doubles to /api/api/... whenever .env is absent — which is exactly the EAS build case, since .env is gitignored and never reaches the build servers
app/job/photo-list.tsx photoUrl ${base}/shifts/... missing /api, job photos 404
app/payslip/[id].tsx PDF URL ${baseURL}/staff/me/... missing /api, payslip download 404

Verification

  • npx expo-doctor — 20/20 checks passed
  • npx tsc --noEmit — clean
  • npx expo config --type introspect — resolved Info.plist carries exactly NSCameraUsageDescription, NSPhotoLibraryUsageDescription and ITSAppUsesNonExemptEncryption=false; bundle identifier sg.superclean.staff

Not verified: no build has run. The iOS build is the next step and needs Apple Developer credentials.

Follow-up flagged, not done

expo config warns android: userInterfaceStyle: Install expo-system-ui in your project to enable this feature. The app is designed light-mode-only; without expo-system-ui that lock does not apply on Android. Out of scope for the iOS TestFlight build, worth a separate PR before Android rollout.

First native build config for this app. `eas build:list` was empty — nothing here has been exercised on a real device yet. ## What is in here **Expo SDK 56 → 57** (commit 1) — `expo-doctor` 20/20, `tsc` clean. Three SDK-57 API adaptations: `StyleSheet.absoluteFillObject` → `StyleSheet.absoluteFill`, tighter expo-router `Link href` typing, narrowed `StatusChip` state prop. **iOS build config** (commit 2): - `app.json` — register the `expo-camera` and `expo-image-picker` config plugins. Without them the production build ships no `NSCameraUsageDescription` and camera access crashes on device. Permission strings are specific; App Review rejects vague ones. The unused `NSMicrophoneUsageDescription` and `NSFaceIDUsageDescription` that expo-camera and expo-secure-store add by default are suppressed — the app records no audio and uses no biometrics. `usesNonExemptEncryption: false` — HTTPS-only, skips the export-compliance prompt on every TestFlight upload. - `eas.json` — `appVersionSource: remote` plus `autoIncrement` on production, so build numbers live in EAS rather than being committed. `EXPO_PUBLIC_API_URL` set per profile. - `.easignore` — new file. EAS reads this **instead of** `.gitignore`. `api/` and `openapi.json` are gitignored build artifacts, but Metro cannot resolve `@/api/*` without them, so a cloud build would have failed at bundling. ## Bugs found on the way Broken today, not only in the native build. The orval-generated paths already start with `/api/`, so `baseURL` must not repeat it. | Site | Was | Effect | |---|---|---| | `lib/api-instance.ts` fallback | `https://superclean.haskytech.com/api` | doubles to `/api/api/...` whenever `.env` is absent — which is exactly the EAS build case, since `.env` is gitignored and never reaches the build servers | | `app/job/photo-list.tsx` `photoUrl` | `${base}/shifts/...` | missing `/api`, job photos 404 | | `app/payslip/[id].tsx` PDF URL | `${baseURL}/staff/me/...` | missing `/api`, payslip download 404 | ## Verification - `npx expo-doctor` — 20/20 checks passed - `npx tsc --noEmit` — clean - `npx expo config --type introspect` — resolved Info.plist carries exactly `NSCameraUsageDescription`, `NSPhotoLibraryUsageDescription` and `ITSAppUsesNonExemptEncryption=false`; bundle identifier `sg.superclean.staff` **Not verified:** no build has run. The iOS build is the next step and needs Apple Developer credentials. ## Follow-up flagged, not done `expo config` warns `android: userInterfaceStyle: Install expo-system-ui in your project to enable this feature`. The app is designed light-mode-only; without `expo-system-ui` that lock does not apply on Android. Out of scope for the iOS TestFlight build, worth a separate PR before Android rollout.
expo-doctor 20/20 and tsc clean on the upgraded tree.

API adaptations required by SDK 57:
- StyleSheet.absoluteFillObject renamed to StyleSheet.absoluteFill
- expo-router Link href typing tightened (cast in ExternalLink)
- StatusChip state prop narrowed in DayTimeline

The expo-file-system/legacy import in app/payslip/[id].tsx is also an
SDK 57 adaptation but rides in the next commit, which touches the same file.
First native build of this app — eas build:list was empty, so nothing here
was previously exercised on a real device.

app.json:
- register expo-camera and expo-image-picker config plugins so the iOS
  usage-description strings are generated; without them the App Store
  build has no NSCameraUsageDescription and camera access crashes
- permission strings are specific (App Review rejects vague ones)
- suppress the unused NSMicrophoneUsageDescription and
  NSFaceIDUsageDescription that expo-camera and expo-secure-store add by
  default; the app records no audio and uses no biometrics
- usesNonExemptEncryption false — HTTPS-only, avoids the export
  compliance prompt on every TestFlight upload

eas.json:
- appVersionSource remote + autoIncrement on production, so build numbers
  are managed by EAS rather than committed
- EXPO_PUBLIC_API_URL per profile: .env is gitignored and therefore never
  reaches the EAS build servers, so without this every cloud build fell
  through to the hardcoded fallback

.easignore:
- EAS reads this instead of .gitignore. api/ and openapi.json are
  gitignored build artifacts, but Metro cannot resolve @/api/* without
  them, so the build would fail at bundling.

Base URL fixes (broken today, not just in the native build):
- orval paths already start with /api/, so baseURL must not repeat it.
  The api-instance fallback did, giving /api/api/... whenever .env was
  absent — which is exactly the EAS build case.
- photo-list photoUrl and the payslip PDF URL built paths off that
  baseURL without /api, so job photos and payslip downloads 404.

AGENTS.md: point at the v57 docs, note this is a managed CNG project.
EAS build c6f1f74b failed in INSTALL_DEPENDENCIES:

  [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1
  Run "pnpm approve-builds" to pick which dependencies should be allowed
  to run scripts.

pnpm-workspace.yaml still carried pnpm's generated placeholder,
'esbuild: set this to true or false'. An undecided build script makes
'pnpm install --frozen-lockfile' exit 1. It never surfaced locally
because node_modules was already populated, so the decision path was
skipped.

Reproduced in isolation with only package.json, pnpm-lock.yaml and
pnpm-workspace.yaml: placeholder exits 1 with the same error, 'esbuild:
true' exits 0. esbuild's postinstall fetches its platform-native binary.
Author
Owner

iOS build is green. f3c045cd — version 0.1.0, build 3, 4m33s, signed IPA produced. Apple team RU2UHH6PAR, bundle sg.superclean.staff.

The first build failed, and it was not the iOS config

Build c6f1f74b died in INSTALL_DEPENDENCIES, 21s in, reporting only "Unknown error". The log said:

[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.

pnpm-workspace.yaml still carried pnpm's own generated placeholder, esbuild: set this to true or false. An undecided build script makes pnpm install --frozen-lockfile exit 1. It never showed up locally because node_modules was already populated, so pnpm skipped the decision path entirely — pnpm install reported success on this machine while failing on every clean checkout.

Reproduced in isolation (a scratch directory holding only package.json, pnpm-lock.yaml and pnpm-workspace.yaml):

allowBuilds.esbuild CI=1 pnpm install --frozen-lockfile
set this to true or false (placeholder) exit 1, ERR_PNPM_IGNORED_BUILDS
true exit 0

Worth noting the placeholder predates this branch — it was committed with the original scaffold, so any clean CI checkout of this repo would have failed the same way.

Still outstanding

eas submit needs ascAppId, which does not exist until the app record is created in App Store Connect. That step requires an interactive Apple login (2FA), so it has to be run by hand:

eas submit --platform ios --latest

submit.production.ios in eas.json stays empty until that returns an ascAppId; filling it in is a follow-up commit.

Holding this PR unmerged until the build is confirmed running on a physical device via TestFlight — the SDK 57 upgrade has still never executed outside a simulator-less cloud builder.

**iOS build is green.** [`f3c045cd`](https://expo.dev/accounts/johnangrs/projects/superclean-staff-app/builds/f3c045cd-ae74-4988-a041-2e8e0ebedbf2) — version 0.1.0, build 3, 4m33s, signed IPA produced. Apple team `RU2UHH6PAR`, bundle `sg.superclean.staff`. ### The first build failed, and it was not the iOS config Build [`c6f1f74b`](https://expo.dev/accounts/johnangrs/projects/superclean-staff-app/builds/c6f1f74b-a666-4c8b-9740-1e9075459f3e) died in `INSTALL_DEPENDENCIES`, 21s in, reporting only "Unknown error". The log said: ``` [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1 Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. ``` `pnpm-workspace.yaml` still carried pnpm's own generated placeholder, `esbuild: set this to true or false`. An undecided build script makes `pnpm install --frozen-lockfile` exit 1. It never showed up locally because `node_modules` was already populated, so pnpm skipped the decision path entirely — `pnpm install` reported success on this machine while failing on every clean checkout. Reproduced in isolation (a scratch directory holding only `package.json`, `pnpm-lock.yaml` and `pnpm-workspace.yaml`): | `allowBuilds.esbuild` | `CI=1 pnpm install --frozen-lockfile` | |---|---| | `set this to true or false` (placeholder) | exit 1, `ERR_PNPM_IGNORED_BUILDS` | | `true` | exit 0 | Worth noting the placeholder predates this branch — it was committed with the original scaffold, so any clean CI checkout of this repo would have failed the same way. ### Still outstanding `eas submit` needs `ascAppId`, which does not exist until the app record is created in App Store Connect. That step requires an interactive Apple login (2FA), so it has to be run by hand: ``` eas submit --platform ios --latest ``` `submit.production.ios` in `eas.json` stays empty until that returns an `ascAppId`; filling it in is a follow-up commit. Holding this PR unmerged until the build is confirmed running on a physical device via TestFlight — the SDK 57 upgrade has still never executed outside a simulator-less cloud builder.
App Store Connect record now exists for sg.superclean.staff (ASC app
6795781195, team RU2UHH6PAR). With these two values in the submit profile
EAS skips the 'ensuring your app exists' lookup, which is what forced the
interactive Apple login on the first submit.

Neither value is a secret — the ASC app id appears in App Store URLs.
appleId is deliberately omitted: EAS holds an App Store Connect API key
server-side (G745PN7QZ2), and naming an appleId here would push some
paths back to interactive login.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/ios-testflight-build:feat/ios-testflight-build
git switch feat/ios-testflight-build

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch dev
git merge --no-ff feat/ios-testflight-build
git switch feat/ios-testflight-build
git rebase dev
git switch dev
git merge --ff-only feat/ios-testflight-build
git switch feat/ios-testflight-build
git rebase dev
git switch dev
git merge --no-ff feat/ios-testflight-build
git switch dev
git merge --squash feat/ios-testflight-build
git switch dev
git merge --ff-only feat/ios-testflight-build
git switch dev
git merge feat/ios-testflight-build
git push origin dev
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
haskytech/superclean-staff-app!8
No description provided.