fix(mobile): adopt the UIKit scene lifecycle so iOS 27 can launch #151

Merged
ullrich merged 1 commit from ios-27-scene-lifecycle into main 2026-09-12 09:29:05 +00:00
Owner

On Xcode 27, the app cannot be run on an iOS 27 simulator or device at all — it is killed before any JS executes:

Application failed to launch: UIScene life cycle is required for apps built with this SDK.

Apple made scene adoption mandatory for anything linked against the iOS 27 SDK (TN3187). Expo's prebuild template still emits an AppDelegate/window lifecycle with no UIApplicationSceneManifest, so a freshly prebuilt app has no scene support.

This is not caused by any change of ours and is not SDK-specific — SDK 56 (this branch) and SDK 57 fail identically. Expo tracks it in expo/expo#46664 (accepted, unreleased) and #46663; the fix depends on React Native 0.87+, so it lands no earlier than SDK 58.

The fix

A config plugin (apps/mobile/plugins/with-ios-scene-lifecycle) that injects UIApplicationSceneManifest into Info.plist and patches the generated AppDelegate.swift with a SceneDelegate.

Taken from narrowstacks/dorkroom#205, itself derived from YesterdaysLemon/expo-ios-scene-lifecycle-plugin. It does more than the minimum manifest, which matters for us:

  • forwards deep links to the Expo app-delegate subscribers — our OAuth sign-in returns through trailscool://auth/callback, which would otherwise break
  • forwards universal links and quick actions
  • re-dispatches scene activation callbacks, because under the scene lifecycle UIKit stops calling the UIApplicationDelegate life-cycle methods that Expo modules still rely on
  • handles cold-start quick actions arriving via connectionOptions

Every anchor in the AppDelegate patch throws when it stops matching, so a future Expo template change fails prebuild loudly instead of silently producing a non-scene, crashing app.

Two adjustments this repo needed

Change Why
@expo/config-plugins as a direct devDependency (~56.0.16) pnpm's isolated linker does not hoist it, so a local plugin cannot resolve it. The upstream PR uses bun, where it happens to be hoisted.
eslint override for apps/mobile/plugins/** Config plugins run in Node during prebuild and must be CommonJS, which the repo's ESM rules reject (3 errors).

Verification

  • On this branch (SDK 56): expo prebuild --clean --platform ios injects the manifest and SceneDelegate; pnpm typecheck 14/14, lint 14/14, test 12/12.
  • The same plugin verified end to end on an iOS 27 simulator (iPhone 17 Pro, Xcode 27): builds, launches, loads 2492 modules from Metro and renders. The identical build without the plugin is killed at launch.

Notes

Remove this plugin once Expo ships a scene-based template — the header comment says so and names the issues to re-check on each SDK upgrade.

The same commit is also on #150 (Expo SDK 57). Whichever lands first, the other should drop it — it is deliberately independent of the SDK bump.

🤖 Generated with Claude Code

On Xcode 27, the app cannot be run on an iOS 27 simulator or device **at all** — it is killed before any JS executes: ``` Application failed to launch: UIScene life cycle is required for apps built with this SDK. ``` Apple made scene adoption mandatory for anything linked against the iOS 27 SDK ([TN3187](https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle)). Expo's prebuild template still emits an AppDelegate/window lifecycle with no `UIApplicationSceneManifest`, so a freshly prebuilt app has no scene support. This is **not** caused by any change of ours and is not SDK-specific — SDK 56 (this branch) and SDK 57 fail identically. Expo tracks it in [expo/expo#46664](https://github.com/expo/expo/issues/46664) (accepted, unreleased) and [#46663](https://github.com/expo/expo/issues/46663); the fix depends on React Native 0.87+, so it lands no earlier than SDK 58. ### The fix A config plugin (`apps/mobile/plugins/with-ios-scene-lifecycle`) that injects `UIApplicationSceneManifest` into `Info.plist` and patches the generated `AppDelegate.swift` with a `SceneDelegate`. Taken from [narrowstacks/dorkroom#205](https://github.com/narrowstacks/dorkroom/pull/205), itself derived from [YesterdaysLemon/expo-ios-scene-lifecycle-plugin](https://github.com/YesterdaysLemon/expo-ios-scene-lifecycle-plugin). It does more than the minimum manifest, which matters for us: - **forwards deep links** to the Expo app-delegate subscribers — our OAuth sign-in returns through `trailscool://auth/callback`, which would otherwise break - forwards universal links and quick actions - **re-dispatches scene activation callbacks**, because under the scene lifecycle UIKit stops calling the `UIApplicationDelegate` life-cycle methods that Expo modules still rely on - handles cold-start quick actions arriving via `connectionOptions` Every anchor in the AppDelegate patch **throws** when it stops matching, so a future Expo template change fails `prebuild` loudly instead of silently producing a non-scene, crashing app. ### Two adjustments this repo needed | Change | Why | |---|---| | `@expo/config-plugins` as a direct devDependency (`~56.0.16`) | pnpm's isolated linker does not hoist it, so a local plugin cannot resolve it. The upstream PR uses bun, where it happens to be hoisted. | | eslint override for `apps/mobile/plugins/**` | Config plugins run in Node during prebuild and must be CommonJS, which the repo's ESM rules reject (3 errors). | ### Verification - On this branch (SDK 56): `expo prebuild --clean --platform ios` injects the manifest and `SceneDelegate`; `pnpm typecheck` 14/14, `lint` 14/14, `test` 12/12. - The same plugin verified end to end on an **iOS 27 simulator** (iPhone 17 Pro, Xcode 27): builds, launches, loads 2492 modules from Metro and renders. The identical build **without** the plugin is killed at launch. ### Notes Remove this plugin once Expo ships a scene-based template — the header comment says so and names the issues to re-check on each SDK upgrade. The same commit is also on #150 (Expo SDK 57). Whichever lands first, the other should drop it — it is deliberately independent of the SDK bump. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(mobile): adopt the UIKit scene lifecycle so iOS 27 can launch
All checks were successful
CD Staging / Build & Push Docker Images (pull_request) Has been skipped
CD Staging / Build & Push Docker Images-1 (pull_request) Has been skipped
CD Staging / Deploy Staging (pull_request) Has been skipped
CD Staging / Deploy PR Preview (pull_request) Has been skipped
CI / Security Scan (pull_request) Successful in 36s
CI / Dockerfile Package Check (pull_request) Successful in 13s
CI / Visual Tests (pull_request) Successful in 1m13s
CI / Checks (pull_request) Successful in 5m3s
CI / E2E Tests (pull_request) Successful in 4m34s
CI / Journal Image Smoke Test (pull_request) Successful in 17m41s
Cancel superseded CI / Cancel in-flight CI (pull_request) Successful in 19s
CD Staging / Tear Down PR Preview (pull_request) Successful in 46s
c2213aacfc
Apps linked against the iOS 27 SDK are killed at launch unless they adopt
UIScene (Apple TN3187). Expo's prebuild template still emits an
AppDelegate/window lifecycle with no UIApplicationSceneManifest, so a
freshly prebuilt app dies before any JS runs:

    Application failed to launch: UIScene life cycle is required for apps
    built with this SDK.

Anyone on Xcode 27 currently cannot run the app on an iOS 27 simulator or
device at all. Expo tracks this in expo/expo#46664 (accepted, unreleased);
the fix is tied to React Native 0.87+, so it lands no earlier than SDK 58.

Adds a config plugin that injects UIApplicationSceneManifest into
Info.plist and patches the generated AppDelegate.swift with a
SceneDelegate. Taken from narrowstacks/dorkroom#205, itself derived from
YesterdaysLemon/expo-ios-scene-lifecycle-plugin. Beyond the minimum
manifest it forwards deep links, universal links and quick actions to the
Expo app-delegate subscribers, and re-dispatches the scene activation
callbacks, because UIKit stops calling the UIApplicationDelegate
life-cycle methods that Expo modules still rely on. Deep-link forwarding
matters here specifically: OAuth sign-in returns through
trailscool://auth/callback.

Two adjustments were needed for this repo:

- @expo/config-plugins becomes a direct devDependency. pnpm's isolated
  linker does not hoist it, so a local plugin cannot resolve it (the
  upstream PR uses bun, where it happens to be hoisted).
- eslint gets an override for apps/mobile/plugins. Config plugins run in
  Node during prebuild and must be CommonJS, which the repo's ESM rules
  otherwise reject.

Every anchor in the AppDelegate patch throws when it stops matching, so a
future template change fails prebuild loudly rather than silently
producing a non-scene, crashing app. Remove the plugin once Expo ships a
scene-based template.

Verified against SDK 56 on this branch: prebuild injects the manifest and
the SceneDelegate. The same plugin was verified end to end on an iOS 27
simulator (iPhone 17 Pro, Xcode 27), where the app builds, launches and
loads its bundle from Metro — without it the identical build is killed at
launch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ullrich deleted branch ios-27-scene-lifecycle 2026-09-12 09:29:06 +00:00
Sign in to join this conversation.
No reviewers
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
trails-cool/trails!151
No description provided.