---
name: ios-webview-app
description: Turn a Next.js static export into a native, fully offline iPad app and get it through App Store review (Kids Category). Use for build, simulator testing, screenshots, archive, upload and review prep.
---

# Next.js -> offline iPad app

## Architecture that passed review
- `ios/project.yml` (xcodegen) -> SwiftUI app with one `WKWebView`. iPad only, iPadOS 17+, all orientations, no `UIRequiresFullScreen`.
- The web bundle lives in the app at `ios/<App>/Web/` and is served through a **custom URL scheme handler** (`myapp://`), not `file://`. `file://` breaks fetch, localStorage scoping and audio autoplay. Block every `http(s)` navigation and request in the scheme handler/navigation delegate so the app is provably offline (reviewers and the Kids Category care).
- Build flag `NEXT_PUBLIC_APP_TARGET=ios` at export time; a tiny `app.ts` module exposes `IS_APP`, feature switches (`APP_HAS_<GAME>`) and app-only ordering (`APP_SWAPS`). Web-only UI (analytics, share links, newsletter, external links) is stripped by those switches, never by hand edits of the export.
- `ios/scripts/build-web.mjs`: `next build` with output export, copy to `Web/`, then crawl the bundle for `http` references and 404s. Fail the build on any.
- Scripts: `build-app.sh` (xcodebuild test on one simulator), `release.sh` (archive, export App Store-signed .ipa with a fresh build number), `screenshots.sh` (UI test that saves store screenshots).

## Kids Category / 4+ rating gotchas
- Any "magic used to harm", bonk, dizzy stars = cartoon violence = 9+. Soften or cut such games (`APP_HAS_X = false`) rather than argue.
- Links out of the app (web, a book, a store) go behind a grown-up gate with no way out for a kid (e.g. hold-to-open plus a simple sum).
- Privacy policy and credits/licenses (OFL fonts, MIT libs) must be readable inside the app, offline.
- `PrivacyInfo.xcprivacy` with no required-reason APIs; `ITSAppUsesNonExemptEncryption = NO` in Info.plist.
- App Privacy answer: Data Not Collected. Saves in localStorage only; say so on the parents' page.
- Privacy and support URLs must be live on the website **before** submitting; deploy those pages separately from unfinished app work.
- Apple shows the legal entity (LLC) as seller; DBAs are not accepted. Decide the team once and don't reopen it.
- Screenshots: up to 10 real-gameplay shots at 2752 x 2064 landscape (13-inch), RGB, no alpha. Icon 1024 px, no alpha.
- Review notes under 4000 bytes: how to pass the grown-up gate, that the app is offline, what each game does.

## Simulator and testing rules
- **One simulator at a time.** Three iPads in parallel ran a 16 GB Mac out of memory. Before any run: `xcrun simctl list devices booted`; if the owner has one open, reuse it or ask. Never pass several `-destination`s to one `xcodebuild test`.
- UI test audio plays through the Mac speakers. Mute or lower volume in tests; the owner will hear your runs.
- UI test suite that paid off: every page opens; hub shows all games; grown-up gate has no exit for a child; a tap in any game reaches every other game; portrait and landscape; network blocked; intro plays and clears; store screenshots saved. Make the run fail on any missing bundled file.
- Internal disk fills quickly with export stages and screenshots; point build stages at an external disk via an env var.

## Release
- Upload with **Transporter** (drag the .ipa, Deliver) or `release.sh --upload`. Don't fight altool.
- A child's Apple ID can't use TestFlight; install on the kid's iPad directly from Xcode.
- Keep the app branch separate from the website's deploy branch; merging to the deploy branch is the owner's call.

## Dev loop
- Dev server for the app target: `NODE_ENV=development NEXT_PUBLIC_APP_TARGET=ios npx next dev -p 3107`, then run the screen check (see the "iPad Screen Check" skill) against it before rebuilding the bundle.
