Conversation
…th without read access (#49362) # Why Picking a video with `launchImageLibraryAsync` terminates apps that ship no `NSPhotoLibraryUsageDescription` in their `Info.plist`. `PHPickerViewController` requires **no** photo library authorization — that is its whole design purpose: the user picks out-of-process and the app receives only the chosen items. `expo-image-picker` builds its picker with `PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())`, which is fine on its own: it just makes the results Photos-backed so `assetIdentifier` is populated. However, the passthrough fast path added in #37569 calls `PHAsset.fetchAssets(withLocalIdentifiers:)`. That is a photo library **read**, so it trips a `kTCCServicePhotos` authorization check. For an app that ships no `NSPhotoLibraryUsageDescription` — perfectly legal, and the recommended configuration for an app that only uses the system picker — that check does not merely fail: iOS terminates the app. `tccd` logs: ``` Refusing authorization request for service kTCCServicePhotos ... without NSPhotoLibraryUsageDescription key ``` The termination happens after the user taps "Done" in the picker, so it looks like a picker bug rather than a permission bug, and no crash report is written to `DiagnosticReports`, which makes it hard to diagnose. This is a regression: before #37569, the video path used only `loadFileRepresentation` on the item provider, which needs no authorization. The effect today is that `expo-image-picker` forces every app that picks videos to request full read access to the photo library, purely to keep an optimization that only matters for *adjusted* assets — even though the config plugin exposes `photosPermission: false` for exactly the case of an app that doesn't want that permission. # How Only take the fast path when the app actually holds the read access that path requires: ```swift let photoLibraryReadStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) let hasPhotoLibraryReadAccess = photoLibraryReadStatus == .authorized || photoLibraryReadStatus == .limited if options.videoExportPreset == .passthrough, hasPhotoLibraryReadAccess, let assetId = selectedVideo.assetIdentifier { ``` - Apps holding read access keep the optimization unchanged. - Apps without it fall through to the pre-existing `loadVideoRepresentation` path — the behavior before the fast path was introduced. That path still preserves the original bytes under `.passthrough`, and still returns `selectedVideo.assetIdentifier` as `assetId`, so the resolved asset is unchanged apart from timing. - `PHPhotoLibrary.authorizationStatus(for:)` does not prompt and is safe to call without a usage-description key. `.limited` is included because in limited mode `fetchAssets` legitimately succeeds for assets in the user's granted set, and gracefully returns empty (falling through) for ones outside it — so the optimization is preserved there too. # Test Plan I do not have a self-contained repro app to attach; the following was verified downstream by the reporter in a real Expo/React Native app (NavigateAI), against `expo-image-picker@57.0.8` patched with exactly this change, on an iOS 18.6 simulator: - With all photo permissions revoked and no `NSPhotoLibraryUsageDescription` in the built `Info.plist`, picking a video from the library previously terminated the app on "Done". With this change the pick completes and resolves an asset whose `assetId` is populated. - With full read access granted, the fast path is still taken and behavior is unchanged. I have not run the expo repo's own test-suites for this package beyond confirming the change is a compile-level no-op for callers (no public API change, no JS/TS change). # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) — changelog entry added; there is no JS/TS source to rebuild, this is an iOS-only change. - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). — not applicable, no config plugin change. - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) — not applicable, no docs change. --------- Co-authored-by: Vojtech Novak <vonovak@gmail.com>
…s page (#49350) # Why <!-- Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests. --> Fix ENG-26175 # How <!-- How did you build this feature or fix this bug and why? --> Add a section and reference to audit logs command on audit logs page. # Test Plan <!-- Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction. --> <img width="2370" height="808" alt="CleanShot 2026-08-25 at 22 43 10@2x" src="https://github.com/user-attachments/assets/2eb2b7b2-a22e-419a-939d-9c2ac7be64e1" /> # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
# Why <!-- Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests. --> Fix ENG-26176 # How <!-- How did you build this feature or fix this bug and why? --> Remove unused EAS Hosting shoutout banner. # Test Plan <!-- Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction. --> N/A # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
# Why <!-- Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests. --> Fix ENG-26172 # How <!-- How did you build this feature or fix this bug and why? --> - Pick light and dark images in CSS instead of JavaScript in all image based components. When images are picked by JavaScript, they happen after the first hydration which is why, previously, there was a frame where light mode images were picked first before dark mode images when dark mode theme was selected. - Remove unused `prefersDarkTheme` code. # Test Plan <!-- Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction. --> **Examples after fix of the image loading:** https://github.com/user-attachments/assets/a2ed08fd-0e54-467b-81c5-261414f8184f https://github.com/user-attachments/assets/f14ada33-d192-4d85-a945-5430c4665912 **How to test manually:** - Go to https://pr-49347.expo-docs.pages.dev/eas/observe/introduction/ - Either switch to dark mode theme - Hard refresh the page and ensure that light mode image doesn't load # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
# Why Fixes #49365. closes #44276 On iOS, ScreenOrientationRegistry can enter a circular wait that permanently blocks the main thread: 1. A controller notification runs on expo.screenorientationregistry. 2. ScreenOrientationModule.screenOrientationDidChange reads currentOrientationMask, which synchronously waits for the main thread. 3. UIKit queries supportedInterfaceOrientations on the main thread. 4. This reaches requiredOrientationMask(), which synchronously waits for expo.screenorientationregistry. The registry queue and main thread then wait for each other, causing an app hang. We observed this frequently in production after upgrading from expo-screen-orientation 9.0.8 to 57.0.1. # How Controller notifications are now delivered on a dedicated notification queue rather than the registry's state-protection queue. screenOrientationDidChange snapshots the registered controllers while holding a barrier on the registry queue, then releases that queue before notifying them. This prevents controller callbacks that synchronously access the main thread from blocking registry state reads made by the main thread. Controller registration and removal now also use barrier writes, ensuring mutations cannot race with the controller snapshot. # Test Plan Test Plan The issue includes a minimal iOS development-build reproduction that deterministically arranges the production wait cycle. 1. Clone the reproduction linked in #49365. 2. Run npm install. 3. Run npm run ios. 4. Wait three seconds or press Trigger now. 5. Without this fix, the app remains on TESTING... and stops responding. 6. Pause the process in Xcode and inspect the main thread. It is blocked in ScreenOrientationRegistry.requiredOrientationMask(). 7. Apply this change to expo-screen-orientation and rebuild the native app. 8. The status changes from TESTING... to RESPONSIVE, and the app continues handling touches. The equivalent change was also tested locally as a patch-package patch against expo-screen-orientation@57.0.1. The reproduction remained responsive after the deterministic trigger. Expo Doctor output for the reproduction: Running 21 checks on your project... 21/21 checks passed. No issues detected! # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --------- Co-authored-by: Vojtech Novak <vonovak@gmail.com>
# Why <!-- Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests. --> Replaces the single `lodash/partition` call in `common/code-utilities.ts` with a six-line local helper. It was the only lodash import that reaches the browser bundle, and `_app` loads on every page. # How <!-- How did you build this feature or fix this bug and why? --> Saves ~7 KB transferred per page (measured on `/versions/latest/sdk/calendar/`: 1363 KB → 1356 KB, median of 5 local Lighthouse runs). # Test Plan <!-- Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction. --> N/A # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
…d of per 8 KiB segment (#49206) # Why On Android, uploading a file picked through the Storage Access Framework with `File.upload()` is around 100x slower than it should be. `CountingSink.write` calls `requestBody.contentLength()` on every write to report progress, and okio hands it one 8 KiB segment at a time. For a `content://` URI the request body's `contentLength()` is `SAFDocumentFile.length()`, which is a `ContentResolver` query into the providing app. So the cost of an upload is dominated by one binder round trip per 8 KiB, not by the transfer itself. Measured with `File.upload()` on a 294 MB document picked from the system picker, on an API 29 emulator: 94.4 s before, 0.86 s after (numbers and method below). `MULTIPART` was never affected — `MultipartBody` caches its own `contentLength()` — so this is `BINARY_CONTENT` only, which is the default. iOS is unaffected: it uploads via `URLSession.uploadTask(fromFile:)`. # How `contentLength()` is constant for the lifetime of a `RequestBody`, so there is no reason to re-query it: - `CountingRequestBody` resolves the delegate's length once into a `lazy` field, and passes that `Long` to `CountingSink` instead of passing the request body and letting the sink call back into it. `CountingSink` no longer holds a `RequestBody` at all, which makes its dependency on a constant length explicit rather than incidental. - The `UnifiedFileInterface.asRequestBody` body resolves `length()` once, also into a `lazy` field, for the same reason — that call is the SAF query itself. Keeping it lazy rather than eager matters: `ContentProviderFile.length()` and `AssetFile.length()` can fall back to reading the whole stream, and this way that work still happens on the OkHttp thread when OkHttp asks for the header, not on the caller's queue while the request is being built. No public API changes. Progress callbacks report the same values. # Test Plan Measured on device, on this branch versus its merge base, with the `expo-file-system` Android sources built from source so the Kotlin under test is the one in this diff. **Correction to the original description:** the numbers in the first version of this PR were produced by `File.upload()`, not by `FileSystem.uploadAsync`. Thanks to @expo-bot for catching the wrong API name — the legacy call converts its URI to a `java.io.File` and never reaches this file, so it could not have exercised the change. Everything below is a fresh measurement through `File.upload()`. **Setup.** Pixel 7a AVD, API 29, arm64. `apps/minimal-tester`, built with `npx expo run:android --variant release` on each arm. A 294 MB file (293,940,775 bytes) pushed to `/sdcard/Download` and picked with `File.pickFileAsync()`, which yields `content://com.android.providers.downloads.documents/document/msf%3A26` and so resolves to `SAFDocumentFile`. A Node HTTP sink on the host counts the bytes it receives and discards them, reached over `adb reverse tcp:8099 tcp:8099`. The app calls: ```ts const result = await file.upload('http://127.0.0.1:8099/upload', { httpMethod: 'PUT', uploadType, // 0 = BINARY_CONTENT, 1 = MULTIPART headers: { 'Content-Type': 'application/octet-stream' }, onProgress: ({ bytesSent, totalBytes }) => { /* count events, collect distinct totals */ }, }); ``` **Results.** Each row is the wall clock of `file.upload()` measured in JS, three consecutive runs where three are given. | source | upload type | before | after | |---|---|---|---| | SAF `content://` | `BINARY_CONTENT` | 91.8 / 94.4 / 105.5 s | 1.02 / 0.85 / 0.86 s | | SAF `content://` | `MULTIPART` | 1.02 s | 0.64 s | | `file://` (same file copied into the cache dir) | `BINARY_CONTENT` | 1.24 / 0.98 s | 1.04 / 0.78 s | **Correctness, identical in both arms and in every run:** the sink received exactly 293,940,775 bytes and the request declared the same `Content-Length`; the last progress event was `(293940775, 293940775)`; and the progress stream carried exactly one distinct `totalBytes` value. The event *count* drops from ~900–1000 to ~10 only because `emitProgress` throttles to one event per 100 ms and the upload is now two orders of magnitude shorter. **Cause, confirmed on device.** Eight `debuggerd -j` dumps taken about a second apart during a "before" upload caught the OkHttp thread in the same place 8 times out of 8, and never in a socket write: ``` at android.os.BinderProxy.transactNative(Native method) at android.content.ContentProviderProxy.query(ContentProviderNative.java:421) at android.content.ContentResolver.query(ContentResolver.java:944) at androidx.documentfile.provider.DocumentsContractApi19.queryForLong(DocumentsContractApi19.java:181) at androidx.documentfile.provider.SingleDocumentFile.length(SingleDocumentFile.java:83) at expo.modules.filesystem.unifiedfile.SAFDocumentFile.length(SAFDocumentFile.kt:90) at expo.modules.filesystem.FileSystemUploadTaskKt$asRequestBody$1.contentLength(FileSystemUploadTask.kt:261) at expo.modules.filesystem.CountingRequestBody.contentLength(FileSystemUploadTask.kt:231) at expo.modules.filesystem.CountingSink.write(FileSystemUploadTask.kt:254) at okio.RealBufferedSink.emitCompleteSegments(RealBufferedSink.kt:256) at okio.RealBufferedSink.writeAll(RealBufferedSink.kt:195) at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:62) ``` After the change the same upload finishes in under a second, and a SAF-backed upload costs about the same as a `file://` one — which is what it should cost, since `JavaFile.length()` was only ever a `stat`. This was first found in a production app (CoMapeo), where importing a custom offline map picked from the document picker ran at 1.8 MB/s and looked like a hang on low-end phones; the same import with this change lands in ~3 s. # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --------- Co-authored-by: Bartłomiej Klocek <barthap10@gmail.com>
…#48914) # Why Creating a `VideoPlayer` via `useVideoPlayer()` on Android throws when the current `Activity` is briefly unavailable — for example during a cold start with an immediate background/recreation of the Activity: ``` Call to function 'VideoPlayer.constructor' has been rejected. → Caused by: The current activity is no longer available ``` The constructor obtained the application context through `appContext.throwingActivity.applicationContext`, so it required an `Activity` it doesn't actually need — the application context does not depend on any Activity. This is the same failure family as #42939 (player creation while the app is backgrounded). # How Replaced `appContext.throwingActivity.applicationContext` with `appContext.reactContext?.applicationContext` in the `VideoPlayer` constructor (`VideoModule.kt`), following the pattern already used elsewhere in the repo (e.g. `VideoManager.onModuleCreated`). If the react context is gone, it now throws the typed `Exceptions.ReactContextLost()` instead of an NPE. Other `throwingActivity` usages were left untouched (`isPictureInPictureSupported`, fullscreen entry in `VideoView`) since those genuinely require an Activity. # Test Plan - `./gradlew :expo-video:compileDebugKotlin` — builds successfully. - `./gradlew :expo-video:spotlessCheck` — passes. - Manual: player creation no longer throws `MissingActivity` when the Activity is destroyed/recreating; playback works as before when the Activity is available. # Checklist - [x] Documentation is up to date to reflect these changes (eg: https://docs.expo.dev and README.md). - [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) - [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). --------- Co-authored-by: Vojtech Novak <vonovak@gmail.com>
# Why In order to remove `useSyncExternalStoreWithSelector` from `useNavigationState`. # How 1. Add `NavigatorStateContext` and use it to pass current navigator slice down in the tree 2. Remove `NavigationStateListenerProvider` and `useSyncExternalStoreWithSelector` usage # Test Plan CI # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --------- Co-authored-by: expo-tuft[bot] <288127324+expo-tuft[bot]@users.noreply.github.com>
# Why In order to remove `useSyncExternalStore` from `useIsFocused` # How Remove `useSyncExternalStore` fallback from `useIsFocused` # Test Plan CI # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --------- Co-authored-by: expo-tuft[bot] <288127324+expo-tuft[bot]@users.noreply.github.com>
> [!WARNING] > **Agent-authored and NOT human-reviewed.** An automated `/verify --fix` run for #49058 wrote this change and checked it in a sandbox; the reasoning and evidence are in the outcome comment on that issue. Review it as you would any external contribution. Requested by @brentvatne · [investigation run]((no run url)) · refs #49058 Two sibling route groups that hold the same child path share one URL, because a group segment is not part of the URL. In-app navigation keeps the group you are in. A cold link (page reload, new tab, bookmark, deep link) has no current group, so it renders the first alphabetical match. The shared routes page covered this in one sentence about page reloads, so users read the difference as a routing defect (#49058). This change rewrites that sentence in `docs/pages/router/advanced/shared-routes.mdx`: every shared route matches the same URL, in-app navigation keeps the current group, a cold link falls back to the first alphabetical match, and one URL can therefore render two different screens. It also warns against using shared routes for role-based screens. No runtime behaviour changes. I measured that behaviour with the router's own code at this commit, in the Web jest project. Vale, `oxfmt --check` and `pnpm test` in `docs/` all pass (620 tests, 58 suites). <details><summary>Cause</summary> A group segment compiles to an optional regex group, so one URL matches every group that holds the path: [`getStateFromPath-forks.ts#L173-L178`](https://github.com/expo/expo/blob/2edd383762349718ffb2b570e3b9cc248cf93a85/packages/expo-router/src/fork/getStateFromPath-forks.ts#L173-L178). `getStateFromPath` takes the current segments as a third argument ([`getStateFromPath.ts#L70-L82`](https://github.com/expo/expo/blob/2edd383762349718ffb2b570e3b9cc248cf93a85/packages/expo-router/src/fork/getStateFromPath.ts#L70-L82)), and the config sorter prefers the candidate that shares more group segments with the current route ([`getStateFromPath-forks.ts#L329-L346`](https://github.com/expo/expo/blob/2edd383762349718ffb2b570e3b9cc248cf93a85/packages/expo-router/src/fork/getStateFromPath-forks.ts#L329-L346)). In-app navigation passes those segments ([`getNavigationAction.ts#L43-L49`](https://github.com/expo/expo/blob/2edd383762349718ffb2b570e3b9cc248cf93a85/packages/expo-router/src/global-state/getNavigationAction.ts#L43-L49)). A cold link passes none ([`useLinking.ts#L86`](https://github.com/expo/expo/blob/2edd383762349718ffb2b570e3b9cc248cf93a85/packages/expo-router/src/fork/useLinking.ts#L86), [`useStore.ts#L141`](https://github.com/expo/expo/blob/2edd383762349718ffb2b570e3b9cc248cf93a85/packages/expo-router/src/global-state/useStore.ts#L141)). The sorter then ties, the sort is stable, and file order decides. File order is `require.context` key order. Metro sorts the file list before it builds the context module, so the first group in alphabetical order wins: `files.slice().sort()` in `createFileMap`, `metro/src/lib/contextModuleTemplates.js` (line 36 in `metro@0.83.3`, which `@expo/metro@54.2.0` re-exports; the same line in `metro@0.84.5`, which this checkout resolves). That last step is a source read, not an end-to-end measurement. Protected routes do not take part in this. Guards are render-time only, so a URL that lands in a guarded group is redirected to the navigator anchor, not to the same path in the sibling group ([`useScreens.tsx#L364-L373`](https://github.com/expo/expo/blob/2edd383762349718ffb2b570e3b9cc248cf93a85/packages/expo-router/src/useScreens.tsx#L364-L373)). </details> <details><summary>Verification</summary> Route tree used for every arm: ``` app/index.tsx app/(creator)/_layout.tsx app/(creator)/dashboard.tsx app/(creator)/campaign-details/[id].tsx app/(guest)/_layout.tsx app/(guest)/browse.tsx app/(guest)/campaign-details/[id].tsx ``` I called the real `getStateFromPath(path, config, previousSegments)` with a config built by the router's own `getMockConfig`, and read the result back with `getRouteInfoFromState`. | Case | Current group | File order | URL | Resolved group | | --- | --- | --- | --- | --- | | A | none (cold link) | creator first | `/campaign-details/123` | `(creator)` | | D | none (cold link) | guest first | `/campaign-details/123` | `(guest)` | | B | `(guest)/browse` | creator first | `/campaign-details/123` | `(guest)` | | C | `(creator)/dashboard` | creator first | `/campaign-details/123` | `(creator)` | | G | `(guest)/browse` | guest first | `/campaign-details/123` | `(guest)` | | H | `(creator)/dashboard` | guest first | `/campaign-details/123` | `(creator)` | | E | none (cold link) | creator first | `/(guest)/campaign-details/123` | `(guest)` | A against D shows that a cold link follows file order. B, C, G and H show that in-app navigation keeps the current group, whatever the file order is. E shows that naming the group in the link pins the group. I then rendered the same tree with the router's own `renderRouter`, under the Web jest project in a `jsdom` environment. `initialUrl` is the cold link, and `router.push` is in-app navigation: ``` PASS Web src/__tests__/issue-49058.test.web.tsx ✓ cold link renders the first group in file order (147 ms) ✓ in-app navigation from (guest) keeps the guest group at the same url (83 ms) ✓ in-app navigation from (creator) keeps the creator group at the same url (44 ms) ✓ naming the group in the href pins the group on a cold link (27 ms) PASS Node src/__tests__/issue-49058.test.web.tsx ✓ cold link renders the first group in file order (100 ms) ✓ in-app navigation from (guest) keeps the guest group at the same url (70 ms) ✓ in-app navigation from (creator) keeps the creator group at the same url (42 ms) ✓ naming the group in the href pins the group on a cold link (21 ms) Tests: 8 passed, 8 total ``` The same four cases also pass in the iOS jest project, so this is not Web-specific. `getPathname()` stays `/campaign-details/123` in every case, while `getSegments()` changes group. These test files were measurement scaffolding and are not part of this change. </details> <details><summary>Checks run</summary> From `docs/`: - `.vale/bin/vale --config='.vale.ini' pages/router/advanced/shared-routes.mdx` — `0 errors, 0 warnings and 0 suggestions in 1 file`. - `oxfmt --check pages/router/advanced/shared-routes.mdx` — `All matched files use the correct format.` - `pnpm test` — `Test Suites: 58 passed, 58 total`, `Tests: 620 passed, 620 total`, `Snapshots: 32 passed, 32 total`. - `pnpm test:worker` — `All tests passed!`. - `tsc --noEmit -p .` — clean. `pnpm lint` also runs `oxlint`, which crashed in my sandbox with a Rust allocator panic (`oxc_allocator/src/pool/fixed_size.rs:112`) under memory pressure. `oxlint` lints only JS and TS files; this change touches neither. I did not run `expo-router` package checks, because this change does not touch that package. </details> <details><summary>Not covered</summary> - This change does not alter routing behaviour, so no app behaviour is affected. - The array syntax `(home,search)` builds the same two routes in memory and should behave the same way, but I did not measure it. - I did not drive a real browser. I ran the router in the Web jest project with `jsdom` instead. - I did not measure `require.context` key order end to end; the alphabetical step is a source read of Metro. - No test in the repository covers a cold link to an ambiguous shared path. Every current "stay in the group" test navigates into a group first. That gap remains. </details> <!-- expo-bot:fix-options v1 --> <details><summary>Options considered</summary> 1. **Warn in development when two groups expose the same URL.** Shared routes are a supported feature, and the array syntax `(home,search)` creates this shape on purpose, so the warning would fire on apps that are correct today. Rejected: it would add noise to every shared-routes app and to the repository's own `apps/router-e2e` projects. 2. **Make URL resolution guard-aware, so a cold link prefers an unguarded sibling group.** Guards live only in React render context (`layouts/GuardContext.tsx`), while resolution runs before render in `fork/getStateFromPath.ts`. Rejected: it needs a new data path from render state into the linking layer, it changes which screen existing cold links open, and that is a design decision for a maintainer. 3. **Sort sibling groups explicitly instead of relying on `require.context` order.** Metro already sorts the file list before it builds the context module, so no app would resolve a URL differently. Rejected: it only removes a dependency on Metro's ordering, and it does not address the reported confusion. 4. **Do nothing and document the behaviour.** Chosen: the behaviour is intentional and already partly documented; the gap is that the page never states that in-app navigation and a cold link resolve differently, which is exactly what the reporter hit. </details> <!-- /expo-bot:fix-options --> --------- Co-authored-by: expo-bot <expo-bot@users.noreply.github.com> Co-authored-by: Aman Mittal <amandeepmittal@live.com>
…session interruption (#49239) # Why On iOS, a recording armed with `record({ forDuration })` loses its duration limit if an audio session interruption occurs. `handleInterruptionBegan` pauses the recorder, and the `.shouldResume` path reaches `startRecording()`, which resumes with a **bare `ref.record()`** — carrying no duration. The recording then continues indefinitely. An incoming call, an alarm, or Siri is enough to trigger it, and nothing in the app is called, so there is no opportunity to re-arm from JS. Measured on a physical iPhone 17 Pro Max (iOS 26.5), `expo-audio@57.0.3`: a recorder armed `forDuration: 6.0`, paused, then resumed via the bare call captured **14.06 s and was still going**. # What this changes `AudioRecorder` remembers the duration a recording was armed with, and `startRecording()` re-applies it instead of resuming unbounded. Two details that matter: **The limit is re-applied absolutely, not as a remainder.** `record(forDuration:)` stops when the recorder's own `currentTime` *reaches* the value, and `currentTime` is cumulative across pause/resume — so the original limit is correct. Re-arming with a computed remainder was measured to end the recording early: 3.98 s against a 6.0 s arm. **`resetDurationTracking()` deliberately does not clear the limit.** `updateStateForDirectRecording()` calls that helper *after* `record(forDuration:)` has armed the limit, so clearing it there would discard the value the caller just set — and the first interruption would resume unbounded again. The limit is cleared at capture boundaries instead: `prepare`, `stopRecording`, `didFinish`, `encodeErrorDidOccur`, and `handleMediaServicesReset`. If the allowance is already spent when the resume arrives, the recorder stops rather than resuming, which routes through the existing `didFinish` path and emits `isFinished` as usual. All three arming paths (`record({ forDuration })`, `record({ atTime, forDuration })`, and the deprecated `recordForDuration`) now go through one method, so none of them can bypass the limit. # Test Plan Verified on a physical device against `expo-audio@57.0.3` with the equivalent change applied: - armed 6.0 s, paused, resumed bare → **before:** 14.06 s and climbing; **after:** stops at 6.000 s - a real Clock-alarm interruption during a 120 s-capped recording → resumes and ends at **exactly 120.0000 s** of audio, verified by parsing the CAF `data` chunk, across 4 trials - one canonical file throughout; the recorder's `didFinish` fires normally at the limit I could not run the repo's own iOS test suite for `expo-audio`; the verification above is device-level against the published package. --------- Co-authored-by: Wojciech Dróżdż <behenate@gmail.com>
# Why <!-- Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests. --> Add missing EAS dashboard entries (for example, Observe, Runtimes, etc.) in the Search UI box and fix the broken one. # Test Plan <!-- Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction. --> <img width="2004" height="2000" alt="CleanShot 2026-08-26 at 17 10 51@2x" src="https://github.com/user-attachments/assets/8c74b812-ce01-4e55-8095-31aefa8290d1" /> <img width="1726" height="1860" alt="CleanShot 2026-08-26 at 17 10 59@2x" src="https://github.com/user-attachments/assets/7c174990-5dac-4762-be7f-a814f0bfdffa" /> # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
# Why <!-- Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests. --> `Search.tsx` already lazy-loads the command menu. `CommandMenu` sits behind `React.lazy`, and the trigger comes from the package's separate lightweight `/trigger` entry. The intent was right and the code looked right but it wasn't working as expected. `ExpoDashboardItem.tsx` imports `addHighlight` and `CommandItemBaseWithCopy` from the main `@expo/styleguide-search-ui` entry, and `Search.tsx` imported that component at the top level. One static import is enough for webpack to pull the whole package into the sync graph, so it all landed in `_app` anyway and the `lazy()` ended up resolving a module that was already loaded. The `_app` chunk goes from 617,870 to 406,383 bytes (gzip -9), so about 207 KB off every page, and 16 packages leave the chunk entirely. Measured on `/versions/latest/sdk/calendar/` with local Lighthouse at median of 5: page weight 1363 KB to 1138 KB, Total Blocking Time 818 ms to 715 ms. The TBT ranges don't overlap (799-832 before, 700-729 after). # Test Plan <!-- Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction. --> N/A # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
…(address missed computed property exclusion) (#49278) # Why Resolves #49232 Related #45337 Previously, the split of `babel-preset-expo` into separate sub-presets missed the `@babel/transform-object-rest-spread` plugin. I've likely left it alone since I was unable to confirm that moving it wouldn't cause the order-dependent issues the bug details. This plugin should instead be only exercised for Hermes v0 and Webviews, not for the Hermes v1 and Modern Web presets. It should be safely movable assuming that it's added after `@babel/plugin-transform-destructuring` (to avoid the ordering dependent bug). This is safe as long as we trust that Hermes does not have any bugs/quirks in its implementation. # How - Move `transform-object-rest-spread` to individual `hermes-v0` and `webview` presets - Note added to capture order dependence - Update noxcturnal transformer config to mirror changes (main-only, no changelog entry) # Test Plan - Unit tests added to capture transform bug related to #49232 - Unit tests added to capture plugin change in configs - `test-suite` updated `JSDestructuring` with all relevant cases for `transform-object-rest-spread` - **Note:** This is fully LLM-derived but looks comprehensive to me - These tests have passed on a test run of `E2E_FORCE_BABEL=1 expo start --clear` against a clean iOS simulator build - Agent confirmed that the iOS bundle does not contain `transform-object-rest-spread` transform outputs and instead contains raw rest-spread patterns (Asked to validate with `pnpx 2g` and fetch the raw iOS bundle identically to the simulator) # Checklist <!-- Please check the appropriate items below if they apply to your diff. --> - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
# Why Adds `expo-app-intents module`, which provides a framework for adding App Intent support to Expo apps # How We can't really provide this functionality fully from JS. App Intents are compiled from static swift classes at build time. Therefore our best shot at adding App Intent and Apple Intelligence support with minimal hassle for the users is allowing them to define the Intents via inline modules. We have to define the intents natively, but the data used by the intents can be provided from JS. For example: In a restaurant order use case - the Entities, Shortcuts and Intents "shapes" have to be defined natively, but the individual items can be provided at runtime from JS via `setEntityCatalogAsync`. The general flow of a received AppIntent: - An App Intent runs in the app target and calls `AppIntentDispatcher.shared.dispatch(name:params:)`. - `AppIntentDispatcher` wraps the call as an `AppIntentInvocation` with an id, name, params, and timestamp. - The dispatcher saves it through `AppIntentInvocationStore` first, so the invocation survives even if JS is not running. - If JS is alive, `ExpoAppIntentsModule` receives the invocation from the dispatcher’s async stream and emits an onIntent event. - JS handles live events via `addAppIntentListener()` or `useAppIntents()`, or later reads stored ones with `getPendingInvocationsAsync()`. - After handling, JS calls `removePendingInvocationAsync(id)` or `clearPendingInvocationsAsync()` to prevent reprocessing. - For parameterized intents, JS manages searchable values with `setEntityCatalogAsync()`, which stores entities in `AppIntentEntityStore` and refreshes App Shortcuts. The module provides functions for communication between the native and js side: - addAppIntentListener(listener) - subscribes to live App Intent invocations while JS is running. - useAppIntents(handler) - reads pending invocations on mount and listens for new invocations afterward. - getPendingInvocationsAsync() - returns queued intent invocations that have not been removed yet. - removePendingInvocationAsync(id) - marks one handled invocation as removed from the pending queue. - clearPendingInvocationsAsync() clears all queued pending invocations. - setEntityCatalogAsync(kind, entities) - replaces an entity catalog used by App Intents parameter queries and refreshes shortcuts. - getEntityCatalogAsync(kind) reads the current entity catalog for a given kind. - refreshShortcutsAsync() asks iOS to re-evaluate App Shortcut phrases and parameter values. # Things coming in subsequent PRs - cli tool for easy setup in an existing project. It will also allow setup with a few examples for learning. - Docs - Examples for NCL # Things we may add in the future - Apple on-screen intelligence support for ExpoUI via a modifier - I think this should be possible, but I didn't have a chance to try implement it because I'm still on the waitlist for the new Siri, so I would have no way of testing it. - Non main-target App Intents - For some quick actions that don't require an immediate action of the app it's not worth it to spin up the main app target # Test Plan Tested on iPhone 13 and iPhone 17 in BareExpo and a separate test app --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )