Skip to content

Commit e04fe3b

Browse files
antonisclaude
andauthored
chore(e2e): Update Expo sample to SDK 58 (#6914)
Bump samples/expo to Expo SDK 58 (expo@58, expo-* and RN community libs aligned to the published bundledNativeModules manifest, React 19.3.0, React Native 0.88.0). Expo 58's prebuild template adopts the UIScene lifecycle itself (its own SceneDelegate: ExpoAppSceneDelegate), so withIosSceneLifecycle now only guards against template drift and injects the SENTRY_TEST_ABORT hook. The crash-screenshot verify script guards are updated to match. Fix Themed prop types for RN 0.88 function-component Text/View. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 24fbd7f commit e04fe3b

7 files changed

Lines changed: 755 additions & 1483 deletions

File tree

‎.github/workflows/sample-application-expo.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ jobs:
182182
- name: Build Android App
183183
working-directory: samples/expo/android
184184
env:
185-
# Increase memory for Expo SDK 57 lint tasks
185+
# Increase memory for Expo SDK 58 lint tasks
186186
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g"
187187
run: |
188188
[[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug'

‎samples/expo/AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ yarn android # Build & run the Android dev client (expo run:android)
1414

1515
## iOS UIScene lifecycle
1616

17-
The `plugins/withIosSceneLifecycle.js` config plugin makes this app adopt the iOS **UIScene lifecycle** with Expo's own `EXExpoAppSceneDelegate` (a Swift `UIWindowSceneDelegate`). This is where Apple/Expo are pushing every app ("Required by iOS 27"), and — importantly — it is the exact configuration behind the crash-handler freeze in [sentry-cocoa#9281](https://github.com/getsentry/sentry-cocoa/issues/9281). Keeping it as the sample's normal config means that bug class stays exercised by regular iOS CI. Expo 57 ships `ExpoAppSceneDelegate` but does not wire it up during prebuild yet, so the plugin adds the `UIApplicationSceneManifest` and adjusts the generated `AppDelegate`.
17+
This app runs on the iOS **UIScene lifecycle** with a Swift `UIWindowSceneDelegate` (the Expo template's `SceneDelegate: ExpoAppSceneDelegate`). This is where Apple/Expo are pushing every app ("Required by iOS 27"), and — importantly — it is the exact configuration behind the crash-handler freeze in [sentry-cocoa#9281](https://github.com/getsentry/sentry-cocoa/issues/9281). Keeping it as the sample's normal config means that bug class stays exercised by regular iOS CI. Since Expo 58 the prebuild template wires this up itself; the `plugins/withIosSceneLifecycle.js` config plugin only fails prebuild if the scene manifest disappears (template drift) and injects the test hook below.
1818

1919
### Crash-handler regression check
2020

21-
The same plugin injects a documented test hook: launching the app with the env var **`SENTRY_TEST_ABORT=1`** crashes it via `abort()` on a **background thread** ~8s after launch. That makes SentryCrash's inline signal handler run off the main thread, forcing the crash-time screenshot (`attachScreenshot: true`) to read the MainActor scene-delegate `window` getter off-main — which **hangs** on an unfixed sentry-cocoa and **terminates cleanly** on a fixed one. The hook is inert unless the env var is set.
21+
The plugin injects a documented test hook: launching the app with the env var **`SENTRY_TEST_ABORT=1`** crashes it via `abort()` on a **background thread** ~8s after launch. That makes SentryCrash's inline signal handler run off the main thread, forcing the crash-time screenshot (`attachScreenshot: true`) to read the MainActor scene-delegate `window` getter off-main — which **hangs** on an unfixed sentry-cocoa and **terminates cleanly** on a fixed one. The hook is inert unless the env var is set.
2222

2323
`scripts/verify-ios-crash-screenshot-no-freeze.sh` (repo root) drives this end to end against whatever cocoa the SDK currently bundles, asserting a clean terminate + a captured `screenshot.png` with no `dispatch_assert_queue_fail` re-entry. It needs **Xcode 27 / iOS 26+** (Swift 6 isolation enforcement). To validate a local cocoa build before it is released, stage it and pass `SENTRY_XCFRAMEWORK_CACHE_DIR=<cache>` (containing `<version>/Sentry.xcframework`). The `.github/workflows/ios-crash-screenshot-no-freeze.yml` workflow runs it on a cocoa version bump (and on manual dispatch) — the fix ships in sentry-cocoa 9.30.1, so a red run means a regression.

‎samples/expo/components/Themed.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* https://docs.expo.io/guides/color-schemes/
44
*/
55

6+
import type { TextProps as DefaultTextProps, ViewProps as DefaultViewProps } from 'react-native';
67
import { Text as DefaultText, View as DefaultView } from 'react-native';
78

89
import Colors from '@/constants/Colors';
@@ -14,8 +15,8 @@ type ThemeProps = {
1415
darkColor?: string;
1516
};
1617

17-
export type TextProps = ThemeProps & DefaultText['props'];
18-
export type ViewProps = ThemeProps & DefaultView['props'];
18+
export type TextProps = ThemeProps & DefaultTextProps;
19+
export type ViewProps = ThemeProps & DefaultViewProps;
1920

2021
export function useThemeColor(
2122
props: { light?: string; dark?: string },

‎samples/expo/package.json‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,23 @@
2525
"dependencies": {
2626
"@sentry/core": "10.76.2",
2727
"@sentry/react-native": "8.31.0",
28-
"@types/react": "~19.2.10",
29-
"expo": "^57.0.26",
30-
"expo-constants": "~57.0.2",
31-
"expo-dev-client": "~57.0.3",
32-
"expo-image-picker": "~57.0.1",
33-
"expo-linking": "~57.0.1",
34-
"expo-router": "~57.0.2",
35-
"expo-status-bar": "~57.0.0",
36-
"expo-updates": "~57.0.4",
37-
"expo-web-browser": "~57.0.0",
38-
"react": "19.2.3",
39-
"react-dom": "19.2.3",
40-
"react-native": "0.86.2",
41-
"react-native-gesture-handler": "~2.32.0",
42-
"react-native-safe-area-context": "~5.7.0",
43-
"react-native-screens": "~4.25.2",
44-
"react-native-web": "^0.21.0",
28+
"@types/react": "~19.3.0",
29+
"expo": "^58.0.14",
30+
"expo-constants": "~58.0.12",
31+
"expo-dev-client": "~58.0.14",
32+
"expo-image-picker": "~58.0.10",
33+
"expo-linking": "~58.0.13",
34+
"expo-router": "~58.0.23",
35+
"expo-status-bar": "~58.0.3",
36+
"expo-updates": "~58.0.17",
37+
"expo-web-browser": "~58.0.6",
38+
"react": "19.3.0",
39+
"react-dom": "19.3.0",
40+
"react-native": "0.88.0",
41+
"react-native-gesture-handler": "~3.3.0",
42+
"react-native-safe-area-context": "~5.10.1",
43+
"react-native-screens": "~4.30.0",
44+
"react-native-web": "~0.21.4",
4545
"typescript": "~6.0.3"
4646
},
4747
"devDependencies": {
Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
/**
2-
* Makes the Expo sample adopt the iOS UIScene lifecycle with Expo's own `EXExpoAppSceneDelegate`
3-
* (a Swift `UIWindowSceneDelegate`). This is the direction Apple/Expo are pushing every app
4-
* ("Required by iOS 27"), and it is the exact configuration behind the crash-handler freeze in
5-
* getsentry/sentry-cocoa#9281 — so by being the sample's normal config, it keeps that bug class
6-
* exercised by regular CI.
2+
* Keeps the Expo sample on the iOS UIScene lifecycle with a Swift `UIWindowSceneDelegate` (the
3+
* template's `SceneDelegate: ExpoAppSceneDelegate`). This is the direction Apple/Expo are pushing
4+
* every app ("Required by iOS 27"), and it is the exact configuration behind the crash-handler
5+
* freeze in getsentry/sentry-cocoa#9281 — so by being the sample's normal config, it keeps that bug
6+
* class exercised by regular CI.
77
*
8-
* Expo 57 ships `ExpoAppSceneDelegate` but does not yet wire it up during prebuild, so this plugin
9-
* does it:
10-
* 1. Adds a `UIApplicationSceneManifest` to Info.plist pointing at `EXExpoAppSceneDelegate`.
11-
* 2. Makes the generated `AppDelegate` conform to `ExpoReactNativeFactoryProvider` and defer the
12-
* window / React Native start to the scene delegate.
8+
* Since Expo 58 the prebuild template adopts the scene lifecycle itself, so this plugin only guards
9+
* against template drift (fails prebuild if the scene manifest is gone) and injects the test hook.
1310
*
14-
* It also injects a small, documented test hook used by the crash-handler regression check
11+
* The test hook is used by the crash-handler regression check
1512
* (scripts/verify-ios-crash-screenshot-no-freeze.sh): when the app is launched with the runtime
1613
* env var `SENTRY_TEST_ABORT=1`, it crashes via `abort()` on a background thread a few seconds
1714
* after launch. SentryCrash then handles the signal off the main thread, forcing the crash-time
@@ -21,62 +18,44 @@
2118
*/
2219
const { withInfoPlist, withAppDelegate } = require('@expo/config-plugins');
2320

24-
const SCENE_DELEGATE_CLASS = 'EXExpoAppSceneDelegate';
25-
26-
function withSceneManifest(config) {
21+
function withSceneManifestGuard(config) {
2722
return withInfoPlist(config, cfg => {
28-
cfg.modResults.UIApplicationSceneManifest = {
29-
UIApplicationSupportsMultipleScenes: false,
30-
UISceneConfigurations: {
31-
UIWindowSceneSessionRoleApplication: [
32-
{
33-
UISceneConfigurationName: 'Default Configuration',
34-
UISceneDelegateClassName: SCENE_DELEGATE_CLASS,
35-
},
36-
],
37-
},
38-
};
23+
const delegateClass =
24+
cfg.modResults.UIApplicationSceneManifest?.UISceneConfigurations?.UIWindowSceneSessionRoleApplication?.[0]
25+
?.UISceneDelegateClassName;
26+
if (!delegateClass) {
27+
throw new Error('[withIosSceneLifecycle] UIApplicationSceneManifest not found; Expo template changed.');
28+
}
3929
return cfg;
4030
});
4131
}
4232

4333
function patchAppDelegate(contents) {
44-
let out = contents;
45-
46-
// Conform to ExpoReactNativeFactoryProvider so the scene delegate can retrieve the factory the
47-
// app delegate creates in didFinishLaunching.
48-
const classDecl = 'class AppDelegate: ExpoAppDelegate {';
49-
if (!out.includes(classDecl)) {
50-
throw new Error('[withIosSceneLifecycle] AppDelegate class declaration not found; Expo template changed.');
34+
if (contents.includes('SENTRY_TEST_ABORT')) {
35+
return contents;
5136
}
52-
out = out.replace(classDecl, 'class AppDelegate: ExpoAppDelegate, ExpoReactNativeFactoryProvider {');
5337

54-
// Remove the app-delegate window creation + RN start; under the scene lifecycle the scene
55-
// delegate (ExpoAppSceneDelegate) owns the window and starts React Native.
56-
const windowBlock = /#if os\(iOS\) \|\| os\(tvOS\)[\s\S]*?factory\.startReactNative\([\s\S]*?\)\s*#endif/;
57-
if (!windowBlock.test(out)) {
58-
throw new Error('[withIosSceneLifecycle] window/startReactNative block not found; Expo template changed.');
38+
const anchor = /^([ \t]*)return super\.application\(application, didFinishLaunchingWithOptions: launchOptions\)/m;
39+
if (!anchor.test(contents)) {
40+
throw new Error('[withIosSceneLifecycle] didFinishLaunching return not found; Expo template changed.');
5941
}
60-
out = out.replace(
61-
windowBlock,
42+
return contents.replace(anchor, (match, indent) =>
6243
[
63-
'// [withIosSceneLifecycle] Scene lifecycle: ExpoAppSceneDelegate creates the window and',
64-
' // starts React Native in scene(_:willConnectTo:). Do NOT start it here.',
6544
'',
66-
' // Test hook for the crash-handler regression check (sentry-cocoa#9281): when launched',
67-
' // with SENTRY_TEST_ABORT=1, crash on a background thread so SentryCrash\'s inline handler',
68-
' // runs off the main thread and the crash-time screenshot reads the MainActor window',
69-
' // getter off-main. Inert unless the env var is set.',
70-
' if ProcessInfo.processInfo.environment["SENTRY_TEST_ABORT"] == "1" {',
71-
' DispatchQueue.global().asyncAfter(deadline: .now() + 8.0) { abort() }',
72-
' }',
45+
`${indent}// [withIosSceneLifecycle] Test hook for the crash-handler regression check (sentry-cocoa#9281):`,
46+
`${indent}// when launched with SENTRY_TEST_ABORT=1, crash on a background thread so SentryCrash's inline`,
47+
`${indent}// handler runs off the main thread and the crash-time screenshot reads the MainActor window`,
48+
`${indent}// getter off-main. Inert unless the env var is set.`,
49+
`${indent}if ProcessInfo.processInfo.environment["SENTRY_TEST_ABORT"] == "1" {`,
50+
`${indent} DispatchQueue.global().asyncAfter(deadline: .now() + 8.0) { abort() }`,
51+
`${indent}}`,
52+
'',
53+
match,
7354
].join('\n'),
7455
);
75-
76-
return out;
7756
}
7857

79-
function withSceneAppDelegate(config) {
58+
function withTestHookAppDelegate(config) {
8059
return withAppDelegate(config, cfg => {
8160
if (cfg.modResults.language !== 'swift') {
8261
throw new Error('[withIosSceneLifecycle] expected a Swift AppDelegate.');
@@ -87,7 +66,7 @@ function withSceneAppDelegate(config) {
8766
}
8867

8968
module.exports = function withIosSceneLifecycle(config) {
90-
config = withSceneManifest(config);
91-
config = withSceneAppDelegate(config);
69+
config = withSceneManifestGuard(config);
70+
config = withTestHookAppDelegate(config);
9271
return config;
9372
};

‎scripts/verify-ios-crash-screenshot-no-freeze.sh‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,17 @@ for runtime, devices in sorted(data.items()):
5656
echo "Simulator: $SIM_ID"
5757
xcrun simctl boot "$SIM_ID" 2>/dev/null || true
5858

59-
# ---- 2. Prebuild the sample (the withIosSceneLifecycle plugin adopts the UIScene lifecycle) -------
59+
# ---- 2. Prebuild the sample (the Expo template adopts the UIScene lifecycle) ----------------------
6060
log "Prebuilding Expo sample"
6161
( cd "$EXPO_DIR" && CI=1 npx expo prebuild --clean --no-install -p ios )
6262

6363
# Guard: fail loudly if the scene lifecycle did not get wired up (otherwise the check would pass
6464
# trivially against a non-scene app that cannot hit the bug).
6565
/usr/libexec/PlistBuddy -c "Print :UIApplicationSceneManifest:UISceneConfigurations:UIWindowSceneSessionRoleApplication:0:UISceneDelegateClassName" \
66-
"$IOS_DIR/sentryreactnativeexposample/Info.plist" 2>/dev/null | grep -q EXExpoAppSceneDelegate \
67-
|| fail "Scene manifest not injected — withIosSceneLifecycle did not apply (Expo template drift?)."
68-
grep -q "ExpoReactNativeFactoryProvider" "$IOS_DIR/sentryreactnativeexposample/AppDelegate.swift" \
69-
|| fail "AppDelegate not wired for the scene lifecycle — withIosSceneLifecycle did not apply."
66+
"$IOS_DIR/sentryreactnativeexposample/Info.plist" 2>/dev/null | grep -q SceneDelegate \
67+
|| fail "Scene manifest missing from Info.plist (Expo template drift?)."
68+
grep -q "ExpoAppSceneDelegate" "$IOS_DIR/sentryreactnativeexposample/SceneDelegate.swift" 2>/dev/null \
69+
|| fail "SceneDelegate is not an ExpoAppSceneDelegate (Expo template drift?)."
7070
grep -q 'SENTRY_TEST_ABORT' "$IOS_DIR/sentryreactnativeexposample/AppDelegate.swift" \
7171
|| fail "AppDelegate is missing the SENTRY_TEST_ABORT hook — withIosSceneLifecycle did not apply."
7272

0 commit comments

Comments
 (0)