Skip to content

[pull] main from expo:main - #1193

Merged
pull[bot] merged 8 commits into
code:mainfrom
expo:main
Aug 28, 2026
Merged

[pull] main from expo:main#1193
pull[bot] merged 8 commits into
code:mainfrom
expo:main

Conversation

@pull

@pull pull Bot commented Aug 28, 2026

Copy link
Copy Markdown

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 : )

alanjhughes and others added 8 commits August 28, 2026 08:29
…aunch asset (#49470)

# Why
An update row can end up with status READY but no launch asset row.
Registration is now transactional (#49130) so new rows can no longer be
committed in this state, but rows corrupted on older SDKs persist
through library upgrades, and any future cause would land in the same
state. Today such a row is selected for launch, fails with "Launch asset
not found for update" on every cold start, and never heals: the loader
short-circuits on READY so it never re-registers the assets, and the
launcher keeps selecting the same broken row.

This brings back the changes in #48733 by @martintreurnicht, which was
closed in favor of #49130.

# How
- DatabaseLauncher.getLaunchableUpdate skips any non-DEVELOPMENT update
whose launch asset row is missing and logs a warning. Selection can then
fall back to an older complete update, the embedded update, or nothing,
instead of a row that can never launch.
- Loader.processUpdate's READY short-circuit now also requires the
launch asset to exist. A broken row falls through to downloadAllAssets,
which re-registers the assets and repairs it on the next load pass

The launcher guard is the safety net until repair happens, and the
loader guard is the repair path. DEVELOPMENT rows are exempt because
they legitimately have no asset rows.

# Test Plan
Added new tests
…n in Loader (#49471)

# Why
#43966 fixed lost asset writes by making Loader's bookkeeping
collections thread safe. That race silently dropped assets from updates
that were then marked READY, which bricks the app at launch, and nothing
in the test suite would catch a regression: the existing loader tests
run on a single-threaded test dispatcher, so the download completions
can never overlap.

# How
Adds LoaderStressTest, which loads a generated manifest of 100 assets
plus the launch asset through a real RemoteLoader on a Dispatchers.IO
scope, with small random delays in the mocked downloads so completions
genuinely interleave. It runs five iterations against fresh in-memory
databases and asserts the update is READY, has its launch asset, and has
every asset row registered.

# Test Plan
With #43966's synchronization temporarily reverted to plain collections,
the test fails on the first iteration with a
ConcurrentModificationException out of notifyAssetLoadProgress. With the
synchronization in place, all iterations pass.
#49234)

# Why

`File.readableStream()` returns zeroed bytes, and silently corrupts the
caller's buffer, when it is read with a BYOB reader whose view starts at
a non-zero offset.

`byobRequest.view` covers the region of the caller's buffer that the
stream still has to fill. `byteLength` is already that region's size,
and `byteOffset` is where it begins in the buffer.
[`FileSystemReadableStreamSource.pull`](https://github.com/expo/expo/blob/main/packages/expo-file-system/src/internal/streams.ts#L28-L41)
treated both as if they were measured from the start of the buffer:

```ts
const bytes = await this.handle.readBytes(theView.byteLength - theView.byteOffset);
...
if (theView instanceof Uint8Array) {
  theView.set(bytes, theView.byteOffset);
} else {
  const array = new Uint8Array(theView.buffer);
  for (let i = 0; i < bytes.length; i++) {
    array[i + (theView.byteOffset ?? 0)] = bytes[i]!;
  }
}
```

Two defects:

1. `readBytes(byteLength - byteOffset)` under-reads by `byteOffset`, and
asks for a non-positive length once `byteOffset >= byteLength`.
2. `theView.set(bytes, theView.byteOffset)` — `set` takes an offset
**relative to the view**, and the view already starts at `byteOffset`,
so the offset is applied twice.

The two branches of that `if` disagree with each other. The `else`
branch indexes a whole-buffer `Uint8Array` by `i + byteOffset`, which is
correct. Only the `Uint8Array` branch double-applies.

`byobRequest.view` has a non-zero `byteOffset` whenever the caller
passes an offset view, and also on any continuation of a
partially-filled BYOB read, since the spec builds the view as `(buffer,
byteOffset + bytesFilled, byteLength - bytesFilled)`.

# How

`byteLength` is the amount to read, and the write goes through a
`Uint8Array` bounded to the view's region:

```ts
const bytes = await this.handle.readBytes(theView.byteLength);
...
new Uint8Array(theView.buffer, theView.byteOffset, theView.byteLength).set(bytes);
```

That is correct for every view type, so the `instanceof` branch goes
away. The `TODO` above it still stands — a native method writing
straight into the view at an offset would avoid this copy.

Partial reads are unaffected: a short `bytes` writes only what it has,
and `respond(bytes.length)` is unchanged.

# Test Plan

New unit tests in `src/internal/__tests__/streams-test.ts`. They drive
`pull()` with a `byobRequest` stand-in, because jsdom has no
`ReadableStream` and the Web jest project would otherwise fail with
`ReferenceError: ReadableStream is not defined`.

Before, on `main`:

```
Tests:       12 failed, 12 passed, 24 total
Test Suites: 4 failed, 4 total
```

The three failing cases, in all four projects:

```
✕ fills a BYOB view that starts at a non-zero offset in its buffer
✕ requests as many bytes as the BYOB view can hold
✕ fills a BYOB view that is not a Uint8Array
```

After:

```
PASS Node | PASS Web | PASS Android | PASS iOS
Tests:       24 passed, 24 total
```

I also checked this end to end against Node's real `ReadableStream`,
reading a file of `1..64` through a BYOB reader with `new Uint8Array(new
ArrayBuffer(32), 8, 16)`.

`main`:

```
value bytes    : [0,0,0,0,0,0,0,0]
expected       : [1,2,3,4,5,6,7,8]
whole buffer   : [0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 1,2,3,4,5,6,7,8, 0,0,0,0,0,0,0,0]
```

The caller gets eight zero bytes, and the file data is written at offset
16 — past the range `respond()` reported as filled, so it also clobbers
whatever the caller had after the view.

This PR:

```
value bytes    : [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
expected       : [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
whole buffer   : [0,0,0,0,0,0,0,0, 1,2,...,16, 0,0,0,0,0,0,0,0]
```

`et check-packages expo-file-system` -> `🏁 All checks passed`.

# Checklist

- [x] Added a `CHANGELOG.md` entry.
- [x] Added tests that fail on `main` and pass here.
- [x] Conforms to the [documentation writing style
guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md).
…9481)

> [!WARNING]
> **Agent-authored and NOT human-reviewed.** An automated `/verify
--fix` run for #49468 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](https://github.com/expo/expo/actions/runs/33140294393) · refs
#49468

The Android code reads the manifest key
`expo.modules.notifications.large_notification_icon` and passes it to
`setLargeIcon`. The config plugin never writes that key. A project
therefore cannot set a notification large icon, and a `largeIcon` key in
`app.json` is dropped without a warning. A `TODO` marks the gap.

This adds a `largeIcon` property. When it is set, prebuild generates
`notification_large_icon.png` for five densities and adds the meta-data
entry.

With `largeIcon` unset, the prebuilt manifest and every generated
drawable are byte-identical to the current release. One case does
change: the plugin now clears that key when `largeIcon` is unset, so a
custom plugin that writes it must be listed before `expo-notifications`.
Both are measured below.

<details><summary>Cause</summary>

The Android runtime side is complete. `ExpoNotificationBuilder` defines
the key, reads it from `ApplicationInfo.metaData`, and applies it:

- key: [`ExpoNotificationBuilder.kt`
L402-L403](https://github.com/expo/expo/blob/f423e2ae75422c2860cee500b13cee695dbbe558/packages/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt#L402-L403)
- read: [`ExpoNotificationBuilder.kt`
L319-L340](https://github.com/expo/expo/blob/f423e2ae75422c2860cee500b13cee695dbbe558/packages/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt#L319-L340)
- use: [`ExpoNotificationBuilder.kt`
L150-L155](https://github.com/expo/expo/blob/f423e2ae75422c2860cee500b13cee695dbbe558/packages/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt#L150-L155)

The plugin side is not. `setNotificationConfig` writes only the small
icon, the color and the default channel: [`withNotificationsAndroid.ts`
L119-L173](https://github.com/expo/expo/blob/f423e2ae75422c2860cee500b13cee695dbbe558/packages/expo-notifications/plugin/src/withNotificationsAndroid.ts#L119-L173).
`withNotificationsAndroid` destructures a fixed set of properties, so
`largeIcon` in `app.json` reaches nothing:
[`withNotificationsAndroid.ts`
L261-L270](https://github.com/expo/expo/blob/f423e2ae75422c2860cee500b13cee695dbbe558/packages/expo-notifications/plugin/src/withNotificationsAndroid.ts#L261-L270).
The `TODO` sits at
[L48-L49](https://github.com/expo/expo/blob/f423e2ae75422c2860cee500b13cee695dbbe558/packages/expo-notifications/plugin/src/withNotificationsAndroid.ts#L48-L49).

[#10492](#10492) removed large icon
support from the managed workflow in 2020, in
`ScopedExpoNotificationBuilder.java`. The native reader stayed. The
config side was never added back.

The 64 dp baseline matches Android's own `notification_large_icon_width`
and `notification_large_icon_height`. The small icon keeps its 24 dp
baseline.
</details>

<details><summary>Verification</summary>

A fresh `create-expo-app` project on `expo@57.0.17`,
`expo-notifications@57.0.15`, `react-native@0.86.3`. Every arm is `npx
expo prebuild -p android --clean` with the image cache cleared first.
The "this change" arms use `plugin/build/withNotificationsAndroid.js`
from this repository's own `pnpm run build` (`sha256:24eb3041…`), copied
into the app's `node_modules`. The "published" arms use
`sha256:ae1bc74d…`, which matches a fresh `npm i
expo-notifications@57.0.15`.

| Arm | Plugin | `largeIcon` in app.json | meta-data | drawables |
|---|---|---|---|---|
| Before | published 57.0.15 | yes | absent | none |
| After | this change | yes | present | 5 densities |
| Guard | this change | no | absent | none |

The after arm adds exactly one line to the generated manifest:

```
<meta-data android:name="expo.modules.notifications.large_notification_icon" android:resource="@drawable/notification_large_icon"/>
```

Generated drawables in the after arm. The small icon is unchanged:

```
drawable-mdpi/notification_icon.png          24x24
drawable-hdpi/notification_icon.png          36x36
drawable-xhdpi/notification_icon.png         48x48
drawable-xxhdpi/notification_icon.png        72x72
drawable-xxxhdpi/notification_icon.png       96x96
drawable-mdpi/notification_large_icon.png    64x64
drawable-hdpi/notification_large_icon.png    96x96
drawable-xhdpi/notification_large_icon.png   128x128
drawable-xxhdpi/notification_large_icon.png  192x192
drawable-xxxhdpi/notification_large_icon.png 256x256
```

Guard arm, `largeIcon` unset, published against this change:

```
diff manifest-baseline.xml manifest-guard.xml   ->  MANIFEST IDENTICAL
diff res-baseline.txt res-guard.txt             ->  DRAWABLES IDENTICAL
```

`res-*.txt` are `sha256sum` lists of every generated `notification*`
drawable, so the bytes are identical, not only the file names.
</details>

<details><summary>The behaviour that does change</summary>

The guard arm only covers a project that never had the meta-data key.
This change adds an `else` branch that removes
`expo.modules.notifications.large_notification_icon` and deletes
`notification_large_icon.png` when `largeIcon` is unset. Today the
plugin never touches that key or that filename, so a project that writes
it from a custom plugin is a separate case. That is a real population,
because a custom plugin is currently the only way to set a large icon.

All four combinations, with `largeIcon` unset in `app.json` in every
row:

| Plugin | Custom plugin listed | meta-data entries |
`notification_large_icon.png` files |
|---|---|---|---|
| published 57.0.15 | before `expo-notifications` | 1 | 5 |
| published 57.0.15 | after `expo-notifications` | 1 | 5 |
| this change | before `expo-notifications` | 1 | 5 |
| this change | after `expo-notifications` | 0 | 0 |

A plugin listed later has its mods run earlier, so the custom plugin
writes first and the `expo-notifications` mod then clears the result.
Listing the custom plugin before `expo-notifications` is safe with both
versions.

This matches how the plugin already treats `icon`, `color` and
`defaultChannel`, which it also clears when unset. The difference is
that no third party writes those keys. If you would rather not clear
this key at all, option 2 in the block below is the smaller change.
</details>

<details><summary>Checks run</summary>

From `packages/expo-notifications` in a full `pnpm install` of this
repository at the checkout commit:

```
pnpm run typecheck   ->  exit 0
pnpm run lint        ->  Found 0 warnings and 0 errors (115 files)
pnpm test            ->  13 suites, 69 tests, 1 snapshot, all passing
pnpm run build       ->  exit 0
pnpm run depscheck   ->  exit 0
npx oxfmt --check    ->  All matched files use the correct format
```

The test count includes two new plugin tests: one for the five generated
large icon files, one for safe and idempotent removal.

This change also touches one documentation page, so the `docs-pr` checks
ran from `docs/`:

```
pnpm test         ->  58 suites, 623 tests, 32 snapshots, all passing
pnpm lint-prose   ->  0 errors, 0 warnings, 0 suggestions in 1600 files
pnpm lint         ->  oxfmt, tsc and eslint pass; oxlint crashed
```

The `oxlint` step crashed with a Rust panic in `oxc_allocator`. It
crashes the same way on the unmodified tree in the same environment, so
this change did not cause it.
</details>

<details><summary>Not covered</summary>

- No Android emulator or device run. This change only produces build
inputs. The native code that draws the large icon is unchanged.
- No bare project with a hand-edited `android/AndroidManifest.xml` that
prebuild does not regenerate. Every arm ran `prebuild --clean`.
- `largeIcon` is not added to the versioned SDK 57 documentation pages,
only to `unversioned`.
- A notification that carries its own image still wins over this icon,
because `ExpoNotificationBuilder` prefers
`notificationContent.getImage`. That behaviour is unchanged.
</details>

<!-- expo-bot:fix-options v1 -->
<details><summary>Options considered</summary>

1. **Do nothing and document that the large icon needs a custom config
plugin.** Costs nothing in code, but leaves a `TODO` that names this
exact work, and leaves every project writing the same plugin by hand.
Rejected: the native reader already exists, so only the config side is
missing.
2. **Add the property but never clear the key when `largeIcon` is
unset.** Drops the `else` branch, so the last row of the table above
would read 1 and 5, and no custom plugin could be broken by ordering.
Cost: it breaks symmetry with `icon`, `color` and `defaultChannel`, and
a bare project that removes `largeIcon` from `app.json` keeps a stale
entry and a stale drawable until a clean prebuild. Rejected on that
inconsistency, but it is the option to pick if the ordering interaction
is judged worse.
3. **Accept a drawable resource name instead of an image path, for
example `"largeIcon": "@drawable/my_icon"`.** Smaller, but the user must
place the drawable in `android/` by hand, and prebuild regenerates that
directory in both the managed and the bare workflow. Rejected: the file
would not survive the next prebuild.
4. **Add a `largeIcon` property that generates the drawables and writes
the meta-data, mirroring the existing `icon` path.** Chosen: it reuses
the small icon's own generate-and-write code, it is symmetric with
`icon` for users, and the prebuild output is byte-identical for projects
that do not set it and do not write the key themselves.

</details>
<!-- /expo-bot:fix-options -->

---------

Co-authored-by: expo-bot <expo-bot@users.noreply.github.com>
# Why

In many places we still read `getState` or `store.state` instead of
relying on the global state passed to navigators.

# How

1. Remove `getState` and `getStateForKey`, and use
`RootNavigationStateContext` and `NavigatorStateContext` instead
2. Replace usages of `store.state` by `RootNavigationStateContext`

# 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)
## Why

The `stackRef` is one of the blockers for concurrent react migration. It
is used only in react-navigation/devtools which will no longer work with
expo-router anyway.

We will add our own devtools as part of
https://linear.app/expo/issue/ENG-20826

## How

1. Remove `stackRef` and `withStack`
2. Remove `stack` param from `__unsafe_action__`

---------

Co-authored-by: Expo Bot <34669131+expo-bot@users.noreply.github.com>
# Why

Remove the global imperative `store`

# How

1. Replace usages of `store.linking` and `store.routeNode` with in-tree
RouterConfig context
2. Remove `store`
3. Fix tests
4. Replace `store.state` with `navigationRef.getRootState()`

# 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)
# Why

When refactoring router to use global state -
#49297 - a regression was introduced -
prevent remove and action listeners stopped working. This PR fixes that.

# How

1. When reducing in global state reducer return not only state but also
events - `{ state, events }`. Events can be of different types (three
right now - `action-dispatched`, `route-removed`, `remove-prevented`).
2. After next render (in `useLayoutEffect`)
`useNavigationTreeReportEvents` processes these events and emits the
react-navigation ones. After processing it dispatches `REPORT_CONSUMED`
action to reducer to remove the processed events (by id)
3. `usePreventRemove` is refactored to align better with current
architecture. It uses context to pass a set of prevented ids to the
reducer. This context stores id of the route with
`usePreventRemove(true)` and all of its parents. Since all the
synchronization between the context passed to reducer and
`usePreventRemove` is executed in effect phase, the hook returns a
function which disables prevention synchronously - for the next reducer
render, rather then waiting one commit.

# 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)
@pull pull Bot locked and limited conversation to collaborators Aug 28, 2026
@pull pull Bot added the ⤵️ pull label Aug 28, 2026
@pull
pull Bot merged commit b14abcc into code:main Aug 28, 2026
20 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants