Skip to content

Commit eaf8aec

Browse files
authored
feat!: bump Expo SDK 52 -> 55 (requires Reanimated 4) (#161)
## Summary Coordinated Expo SDK 52 → 55 upgrade for the kitchen-sink example, with the library's peer dependency floors raised to match Reanimated 4's ecosystem requirements. Replaces 10 stalled Renovate PRs (#128, #129, #130, #132, #134, #148, #150, #151, #152, #154) that were each blocked by SDK 52's pinned versions. ## ⚠️ Breaking change (library) Library minimum peer versions are now: - `react` `>=18.0.0` - `react-native` `>=0.81.0` - `react-native-gesture-handler` `>=2.20.0` - `react-native-reanimated` `>=4.0.0` - `react-native-worklets` `>=0.5.0` *(optional; ships with Reanimated 4)* The deprecated `runOnJS` from `react-native-reanimated` was replaced with `scheduleOnRN` from `react-native-worklets`, which only exists in Reanimated 4. Consumers on Reanimated 3 or earlier must upgrade. `packages/modal/CHANGELOG.md` has an Unreleased entry under "⚠ BREAKING CHANGES" with the same list. ## What changed ### `examples/kitchen-sink` (playground) - `expo` `^52.0.16` → `^55.0.24` - `react` / `react-dom` `18.3.1` → `19.2.0` - `react-native` `0.76.9` → `0.83.6` - `expo-constants`, `expo-linking`, `expo-router`, `expo-splash-screen`, `expo-status-bar` → SDK 55 line - `react-native-gesture-handler` `2.20.2` → `~2.30.0` - `react-native-reanimated` `~3.16.5` → `4.3.1` - `react-native-safe-area-context` `4.12.0` → `~5.6.2` - `react-native-screens` `~4.4.0` → `~4.23.0` - `react-native-web` `~0.19.13` → `^0.21.0` - New: `@expo/metro-runtime` `~55.0.11` direct dep + `pnpm.overrides` pin (was stuck on 4.0.0 via stale resolution, causing `getDevServer is not a function` at JS bootstrap) - New: `expo.install.exclude` for `react-native-reanimated` (Expo SDK 55 advises 4.2.1 but its bundled compatibility.json rejects worklets 0.8.x; 4.3.1 is the authoritative fix) - Drop `newArchEnabled` from `app.config.ts` (default in SDK 55) - Fix `metro.config.js` for SDK 55's tightened `Readonly` Metro config types - New Expo config plugin `plugins/withExpoModulesCoreSwiftStrictConcurrency.js`: lowers `SWIFT_STRICT_CONCURRENCY=targeted` for the ExpoModulesCore pod only, working around expo/expo#44141 (Xcode 16 Swift 6 strict mode) ### `packages/modal` (library) - Replace `runOnJS` with `scheduleOnRN` from `react-native-worklets` - Add `react-native-worklets` as optional peer - Raise peer floors (see Breaking change above) - devDeps bumped to match the workspace (`@types/react` 19, `react-native` 0.83.6, `jest-expo` 55, `react-test-renderer` 19, `@testing-library/react-native` 13.3) - Exclude `dist` from `tsconfig.json` (fixes typecheck/build race with bunchee) ### Tooling - Pin `metro@^0.83.3` via `pnpm.overrides` (was pulling 0.84.4 from `@react-native/community-cli-plugin`, failing expo-doctor) - ESLint shared config: drop `parserOptions.project` (superseded by `projectService` in typescript-eslint v8; fixes lint parse errors) - Drop `ScrollView` import from `react-native-gesture-handler` in kitchen-sink (use core RN to satisfy `no-restricted-syntax`) ### CI - New workflow `.github/workflows/e2e-ios.yml`: `macos-15` runner, full prebuild + Release iOS build + Maestro smoke flows, uploads Maestro debug output as artifact on failure - New `examples/kitchen-sink/.maestro/`: - `smoke-launch.yaml` and `smoke-modal-open-close.yaml` (run on CI; hardened with `extendedWaitUntil` + `waitForAnimationToEnd` for iOS 26 accessibility timing) - The existing `crash-test-dropdown.yaml`, `issue155-crash-test.yaml`, `stress-test-crash.yaml` are brought forward for local use (they reference screens that only exist on the `fix/issue-155-navigation-crash` branch, so not in CI matrix) - `README.md` documenting which flow runs where ## Test plan - [x] `pnpm install` clean (no `node_modules` patches required) - [x] `pnpm --filter @magic/kitchen-sink doctor` → 15/15 - [x] `pnpm typecheck` → 4/4 - [x] `pnpm lint` → 3/3 - [x] `pnpm --filter react-native-magic-modal build` → clean - [x] Local: `expo prebuild --platform ios --clean` + `expo run:ios` on iPhone 17 Pro (iOS 26.5) → BUILD SUCCEEDED, kitchen-sink home screen renders with all 9 modal buttons (screenshot verified) - [ ] CI: `🛠️ Branch Checkup` green (in progress) - [ ] CI: `📱 E2E iOS (Maestro)` green (in progress) - [ ] Manual modal interaction smoke (iOS 26 LogBox warnings still need to be investigated post-merge; UI renders, but the in-app debugger pill is not expandable in this env) - [ ] Android build smoke test
1 parent 83f4040 commit eaf8aec

19 files changed

Lines changed: 2679 additions & 3284 deletions

.github/workflows/e2e-ios.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: 📱 E2E iOS (Maestro)
2+
on:
3+
push:
4+
branches-ignore:
5+
- main
6+
pull_request:
7+
branches:
8+
- main
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
maestro-ios:
16+
name: 🧪 Maestro smoke tests (iOS sim)
17+
runs-on: macos-15
18+
timeout-minutes: 60
19+
env:
20+
EXAMPLE_DIR: examples/kitchen-sink
21+
APP_ID: com.gstj.reactnativemagicmodalexample
22+
23+
steps:
24+
- name: 🏗 Setup Repo
25+
uses: actions/checkout@v4
26+
27+
- name: 🏗 Setup PNPM
28+
uses: pnpm/action-setup@v4
29+
30+
- name: 🏗 Setup Node
31+
uses: actions/setup-node@v4
32+
with:
33+
node-version: 20.x
34+
cache: pnpm
35+
36+
- name: 🏗 Setup Ruby (for CocoaPods)
37+
uses: ruby/setup-ruby@v1
38+
with:
39+
ruby-version: "3.2"
40+
bundler-cache: false
41+
42+
- name: 🏗 Install CocoaPods
43+
run: gem install cocoapods --no-document
44+
45+
- name: 🏗 Get PNPM store directory
46+
id: pnpm-cache
47+
run: |
48+
echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT"
49+
50+
- name: 🏗 Setup PNPM cache
51+
uses: actions/cache@v4
52+
with:
53+
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
54+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
55+
restore-keys: |
56+
${{ runner.os }}-pnpm-store-
57+
58+
- name: 📦 Install Dependencies
59+
run: pnpm install --frozen-lockfile
60+
61+
- name: 🏗 Install Maestro CLI
62+
run: |
63+
curl -fsSL "https://get.maestro.mobile.dev" | bash
64+
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
65+
66+
- name: 🔎 Verify Maestro
67+
run: maestro --version
68+
69+
- name: 📱 Boot iOS Simulator
70+
id: boot-sim
71+
run: |
72+
# Use the latest iPhone simulator already available on the runner image.
73+
DEVICE_UDID=$(xcrun simctl list devices available -j \
74+
| python3 -c "import json,sys; d=json.load(sys.stdin)['devices']; \
75+
rt=sorted([k for k in d if 'iOS' in k]); \
76+
uuids=[x['udid'] for x in d[rt[-1]] if 'iPhone' in x['name']]; \
77+
print(uuids[-1])")
78+
echo "Using simulator UDID: $DEVICE_UDID"
79+
xcrun simctl boot "$DEVICE_UDID" || true
80+
xcrun simctl bootstatus "$DEVICE_UDID" -b
81+
echo "device_udid=$DEVICE_UDID" >> "$GITHUB_OUTPUT"
82+
83+
- name: 🛠 Expo prebuild (iOS)
84+
working-directory: ${{ env.EXAMPLE_DIR }}
85+
run: pnpm expo prebuild --platform ios --clean
86+
87+
- name: 🏗 Build & install iOS app on simulator
88+
working-directory: ${{ env.EXAMPLE_DIR }}
89+
run: pnpm expo run:ios --configuration Release --no-bundler --device "${{ steps.boot-sim.outputs.device_udid }}"
90+
91+
- name: 🧪 Run Maestro smoke flows
92+
working-directory: ${{ env.EXAMPLE_DIR }}
93+
run: |
94+
maestro test \
95+
.maestro/smoke-launch.yaml \
96+
.maestro/smoke-modal-open-close.yaml
97+
98+
- name: 📤 Upload Maestro debug output
99+
if: always()
100+
uses: actions/upload-artifact@v4
101+
with:
102+
name: maestro-debug-output
103+
path: |
104+
~/.maestro/tests/**
105+
${{ env.EXAMPLE_DIR }}/.maestro/**/*.png
106+
if-no-files-found: ignore
107+
retention-days: 7
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Maestro E2E Flows — kitchen-sink
2+
3+
End-to-end smoke tests for the `react-native-magic-modal` kitchen-sink example
4+
app, executed with [Maestro](https://maestro.mobile.dev).
5+
6+
## Flows
7+
8+
### Run on CI (`.github/workflows/e2e-ios.yml`)
9+
10+
| File | Purpose |
11+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
12+
| `smoke-launch.yaml` | App boots and the home screen renders the primary buttons. |
13+
| `smoke-modal-open-close.yaml` | The primary "Show Modal" example opens `ExampleModal` and can be dismissed via the in-modal **Close Modal** button. |
14+
15+
These flows only touch UI surfaces that exist on the `renovate-sweep` /
16+
`main` branches and should be stable across SDK upgrades.
17+
18+
### Present but NOT in the CI matrix
19+
20+
| File | Why it is skipped |
21+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
22+
| `crash-test-dropdown.yaml` | References `app/crash-test.tsx` ("Android Crash Test (Pure Reproduction)") which only exists on the issue-155 fix branch. |
23+
| `issue155-crash-test.yaml` | References `app/issue155.tsx` ("Test Issue #155 (Navigation Crash)") which only exists on the issue-155 fix branch. |
24+
| `stress-test-crash.yaml` | Same as above — depends on the crash-test screen. |
25+
26+
Once the issue-155 fix branch is merged (which adds the `crash-test` and
27+
`issue155` route files), add these flows to the `e2e-ios.yml` matrix.
28+
29+
## Run locally
30+
31+
Prereqs:
32+
33+
- macOS with Xcode + an iOS simulator booted.
34+
- The kitchen-sink app installed on the simulator:
35+
```sh
36+
cd examples/kitchen-sink
37+
pnpm expo prebuild --platform ios --clean
38+
pnpm expo run:ios
39+
```
40+
- Maestro CLI: <https://maestro.mobile.dev/getting-started/installing-maestro>.
41+
42+
Run a single flow:
43+
44+
```sh
45+
maestro test examples/kitchen-sink/.maestro/smoke-launch.yaml
46+
```
47+
48+
Run the CI smoke set:
49+
50+
```sh
51+
maestro test \
52+
examples/kitchen-sink/.maestro/smoke-launch.yaml \
53+
examples/kitchen-sink/.maestro/smoke-modal-open-close.yaml
54+
```
55+
56+
The app id (`com.gstj.reactnativemagicmodalexample`) is declared in
57+
`examples/kitchen-sink/app.config.ts` and matches the `appId` in each flow.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
appId: com.gstj.reactnativemagicmodalexample
2+
---
3+
- launchApp
4+
# Navigate to the crash test screen
5+
- tapOn: "Android Crash Test (Pure Reproduction)"
6+
# Show the dropdown modal using the exact crash pattern
7+
- tapOn: "Show Dropdown (Crash Pattern)"
8+
# Tap on the menu item that triggers immediate navigation (should crash)
9+
- tapOn: "Navigate Immediately (Crash)"
10+
# If it doesn't crash, we should be on the home screen
11+
- assertVisible: "Show Modal"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
appId: com.gstj.reactnativemagicmodalexample
2+
---
3+
- launchApp
4+
- tapOn: "Test Issue #155 (Navigation Crash)"
5+
- tapOn: "Navigate Immediately"
6+
- assertVisible: "Issue #155 Test Page"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
appId: com.gstj.reactnativemagicmodalexample
2+
---
3+
# Smoke test: verify the kitchen-sink app launches and the home screen renders.
4+
# The home screen (src/app/index.tsx) renders a ScrollView of Pressable buttons.
5+
- launchApp
6+
- waitForAnimationToEnd:
7+
timeout: 10000
8+
- extendedWaitUntil:
9+
visible: "Show Modal"
10+
timeout: 15000
11+
- assertVisible: "Show Undismissable Modal"
12+
- assertVisible: "Show Toast"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
appId: com.gstj.reactnativemagicmodalexample
2+
---
3+
# Smoke test: open the primary modal example and dismiss it via the in-modal
4+
# Close button. NOTE: src/app/index.tsx's showModal() also auto-closes via
5+
# setTimeout(2000), so this flow taps "Close Modal" promptly to avoid racing
6+
# the timer.
7+
- launchApp
8+
- waitForAnimationToEnd:
9+
timeout: 10000
10+
- extendedWaitUntil:
11+
visible: "Show Modal"
12+
timeout: 15000
13+
- tapOn: "Show Modal"
14+
- extendedWaitUntil:
15+
visible: "Example Modal"
16+
timeout: 5000
17+
- assertVisible: "This is an example to showcase the imperative Magic Modal!"
18+
- tapOn: "Close Modal"
19+
- extendedWaitUntil:
20+
visible: "Show Modal"
21+
timeout: 5000
22+
- assertNotVisible: "Example Modal"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
appId: com.gstj.reactnativemagicmodalexample
2+
---
3+
# Stress test to reproduce intermittent crash - StormeNet pattern
4+
- launchApp
5+
- scroll
6+
- tapOn: "Android Crash Test (Pure Reproduction)"
7+
- extendedWaitUntil:
8+
visible: "Show Dropdown (Crash Pattern)"
9+
timeout: 5000
10+
- tapOn: "Show Dropdown (Crash Pattern)"
11+
- extendedWaitUntil:
12+
visible: "Navigate Immediately (Crash)"
13+
timeout: 3000
14+
- tapOn: "Navigate Immediately (Crash)"
15+
- extendedWaitUntil:
16+
visible: "Show Modal"
17+
timeout: 5000

examples/kitchen-sink/app.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default {
1717
origin: "https://kitchen-sink.expo.dev",
1818
},
1919
],
20+
"./plugins/withExpoModulesCoreSwiftStrictConcurrency",
2021
],
2122
platforms: ["ios", "android", "web"],
2223
splash: {
@@ -32,6 +33,5 @@ export default {
3233
android: {
3334
package: "com.gstj.reactnativemagicmodalexample",
3435
},
35-
newArchEnabled: true,
3636
assetBundlePatterns: ["**/*"],
3737
} satisfies ExpoConfig;

examples/kitchen-sink/metro.config.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,24 @@ const projectRoot = __dirname;
88

99
const workspaceRoot = path.resolve(projectRoot, "../..");
1010

11+
/** @type {any} */
1112
const config = getDefaultConfig(__dirname);
1213

13-
// @ts-expect-error -- TODO: Convert to TS
14-
config.watcher = {
15-
// +73.3
16-
...config.watcher,
17-
healthCheck: {
18-
enabled: true,
19-
},
20-
};
14+
// +73.3
15+
config.watcher.healthCheck.enabled = true;
2116

2217
// 1. Watch all files within the monorepo
2318
config.watchFolders = [workspaceRoot];
2419
// 2. Let Metro know where to resolve packages and in what order
25-
// @ts-expect-error -- TODO: Convert to TS
2620
config.resolver.nodeModulesPaths = [
2721
path.resolve(projectRoot, "node_modules"),
2822
path.resolve(workspaceRoot, "node_modules"),
2923
];
3024

3125
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
32-
// @ts-expect-error -- TODO: Convert to TS
3326
config.resolver.disableHierarchicalLookup = true;
3427

3528
const { FileStore } = require("metro-cache");
36-
// @ts-expect-error -- TODO: Convert to TS
3729
config.cacheStores = [
3830
// Ensure the cache isn't shared between projects
3931
// this ensures the transform-time environment variables are changed to reflect

examples/kitchen-sink/package.json

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,30 @@
2323
"react-native-magic-modal"
2424
]
2525
}
26+
},
27+
"install": {
28+
"exclude": [
29+
"react-native-reanimated"
30+
]
2631
}
2732
},
2833
"dependencies": {
29-
"expo": "^52.0.16",
30-
"expo-constants": "~17.0.2",
31-
"expo-linking": "~7.0.2",
32-
"expo-router": "^4.0.2",
33-
"expo-splash-screen": "~0.29.7",
34+
"expo": "^55.0.24",
35+
"@expo/metro-runtime": "~55.0.11",
36+
"expo-constants": "~55.0.16",
37+
"expo-linking": "~55.0.15",
38+
"expo-router": "~55.0.14",
39+
"expo-splash-screen": "~55.0.21",
3440
"react-native-magic-modal": "workspace:*",
35-
"expo-status-bar": "~2.0.0",
36-
"react": "18.3.1",
37-
"react-dom": "18.3.1",
38-
"react-native": "0.76.9",
39-
"react-native-gesture-handler": "2.20.2",
40-
"react-native-reanimated": "~3.16.5",
41-
"react-native-safe-area-context": "4.12.0",
42-
"react-native-screens": "~4.4.0",
43-
"react-native-web": "~0.19.13"
41+
"expo-status-bar": "~55.0.6",
42+
"react": "19.2.0",
43+
"react-dom": "19.2.0",
44+
"react-native": "0.83.6",
45+
"react-native-gesture-handler": "~2.30.0",
46+
"react-native-reanimated": "4.3.1",
47+
"react-native-safe-area-context": "~5.6.2",
48+
"react-native-screens": "~4.23.0",
49+
"react-native-web": "^0.21.0"
4450
},
4551
"devDependencies": {
4652
"@babel/core": "^7.20.0",

0 commit comments

Comments
 (0)