Skip to content

Upgrade to Expo SDK 58 (React Native 0.88) - #135

Merged
koniz-dev merged 46 commits into
mainfrom
sdk-58
Nov 6, 2026
Merged

koniz-dev merged 46 commits into
mainfrom
sdk-58

Conversation

@koniz-dev

Copy link
Copy Markdown
Owner

Draft for #134: CI runs on the PR. Not to be merged until the maintainer decides (when the store Expo Go supports SDK 58, or at the deadline, about 2026-10-27).

  • Dependencies: the versions SDK 58 expects, plus React Native Testing Library 14 (test-renderer) and Jest 30, which expo-router 58 needs.
  • App: unstable_settings.anchor; back buttons decided when pressed; "Go home" tries replace even when dismissTo throws; strict React Native types (password ref, tab icon colors); web.output: 'single'.
  • Tests: async render APIs, no UNSAFE_ queries; a DocumentTitle stand-in for screens' titles; an asset-registry mock for expo-asset 58.
  • E2E: Expo Go 58.0.4 for Android.

Refs #134

🤖 Generated with Claude Code

koniz-dev and others added 18 commits October 19, 2026 23:02
The versions npx expo install --check expects for SDK 58, plus what
expo-router 58 needs for its testing peer: @testing-library/react-native
14, which renders with test-renderer instead of react-test-renderer, and
Jest 30 with jest-expo 58. Dependencies outside the SDK keep their
versions. The tests move to the async render APIs in the next commits.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ypes

- The root stack is anchored with unstable_settings.anchor;
  initialRouteName is deprecated.
- The first screen of (app) always has its back button, and it decides
  when pressed: Back, or Home when there is nothing to go back to. Its
  options are read once, before the stack below has settled, so the old
  check left a pushed Profile without a back button. (auth) uses the same
  router as the rest of the app.
- "Go home" tries replace even when dismissTo throws, which it does
  before the navigator has rendered.
- The password field's ref is a TextInputInstance, and the tab icons take
  the tab bar's tint colors: React Native's strict types reject the old
  ref type and React Navigation's ColorValue.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
render, renderHook, renderRouter, fireEvent and act return promises
and are awaited. The UNSAFE_ queries are gone, and the test renderer
keeps only host elements: screens' titles are read through a DocumentTitle
stand-in (testing/documentTitle.tsx), and the StatusBar and
KeyboardAvoidingView props from the components themselves. An awaited
render has already settled what was ready, so tests that check a loading
state hold the request or the storage read. The tab error-boundary tests
replace the declared Home tab, since a Tabs layout only routes the
screens it declares. jest.setup mocks the asset registry expo-asset now
reads. Testing docs describe the async APIs.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
web.output was unset, and Expo SDK 58 changes what an unset value
exports. Set it to 'single', which the hosting docs describe, and test
it. Getting Started notes that the JavaScript is now split per route.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android e2e downloads Expo Go 58.0.4 (SHA256 from the release's
asset digest). The README, AGENTS.md, Getting Started and Testing name
SDK 58, React Native 0.88 and React 19.3; Xcode 26.4+ is still the
requirement.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expo Router 58 renders a guarded screen as a Redirect, which calls
replace('/'): the root stack swaps the guarded group for a new Home
instead of returning to the Home below. Each sign-in and sign-out added
one more (3, 5, 7 after each round), a refused cold-start deep link left
two, and Back on Android or the iOS edge swipe showed Home again. The
root navigator now dismisses back to the bottom Home when the root stack
holds only Homes. Tests cover repeated sign-in and sign-out, the refused
deep links, and Profile's back button returning to a screen other than
Home.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Expo Router 58 a file in (tabs) without a Tabs.Screen is not a tab:
no button, and links to it stay on Home (Conventions said it showed its
file name). The web export now loads each route's JavaScript on demand,
so Getting Started says to keep the previous deploy's _expo/static files
and not to answer missing /_expo/ paths with index.html. Testing lists
testing/documentTitle.tsx and rerender/unmount among the async APIs; the
web.output comment no longer claims a server default.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser Home

51d8e04 collapsed a root stack of only Homes, but Login or Profile opened
over another screen still left a new Home on top: signing in from Login
opened over Settings landed on Home, not Settings, and Back showed
Settings and then Home again; signing out, or a session that ended, on
Profile over Settings did the same. Expo Router 58 puts that new Home in
the guarded group's place with replace('/'). The root navigator now pops
a Home that is on top of the root stack above its bottom, which returns
the user to the screen below, as on SDK 57. Tests cover sign-in, sign-out
and an expired session over another screen.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60e0c35 popped any Home on top of the root stack above its bottom, but
Expo Router 58 also puts one there for router.navigate, router.push and
<Link> to a tab from a screen above the tabs: those did nothing at all.
The root navigator now pops a Home only where (auth) or (app) just was,
with the stack no longer: the guards' replace('/'). Tests cover
navigate and push to Home from Settings, push to Home from Profile, and
replace with a tab.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83b66e5 recognised the guards' replace('/') by the stack alone, and the
app's own replace from Login or Profile has the same shape: a Profile or
Login button that replaced the screen with Explore landed on Home, and a
replace with Home from Profile over Settings went back to Settings. The
root navigator now pops the new Home only when the session changed while
a guarded group was on top, and the Home is on its first tab. Guarded
groups are listed in GUARDED_GROUPS. Tests cover Profile's own replace
with Home, and Profile's and Login's own replace with a tab.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt case

R5's mutation testing found conditions no test needed: the same-top
return (sign-out or expiry on a second (app) screen above Profile left
two Homes without it), the (tabs) and length checks (signing in and at
once replacing with a root screen, or pushing Home), the group check
(another screen's own replace with Home after a sign-out), and the Home
tab check (signing in and at once replacing Login with Explore, a demo
test). Each now has a test. The first-state exception was equivalent to
clearing the flag, so it goes.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ace after signIn

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o it at once

R6: the hook can't tell the app's own replace('/') right after a session
change from the guards' redirect, so it undid it, and a user who signed
out on Profile opened from Settings stayed on Settings. Conventions now
says to go Home with dismissTo('/'), and to navigate before awaiting
anything else, since the guards' redirect replaces what is on top then.
Tests cover dismissTo after signing in and out over Settings. The hook's
comment says so too, and a flag reset that the next state always repeats
goes.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…other

R7: the flag reset dropped in 5fb2731 was needed. Signing in on Login
and at once replacing it with Profile (as Conventions advises) moves
from one guarded group to the other, where no other reset runs, so a
later replace('/') on Profile was taken for the guards' redirect and the
user landed on the screen below instead of Home. The reset is back with
a test. Conventions now says to go to a tab with dismissTo('/<tab>'), and
to use replace only for screens outside (tabs): a replace with a tab
adds a second Home.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arded groups

R8: the advice held on Login and Profile, but not for signIn/signOut on a
tab such as Home's Sign out: there is no redirect there, dismissTo
doesn't switch tabs, and replace drops Home. The paragraph is now its own,
names where the guards' redirect happens, and says to navigate as usual
elsewhere.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the tabs

R9: "navigate as usual" after a sign-out held on a tab, but on a screen
pushed over the tabs (an app/settings.tsx with Sign out) navigate('/')
and <Link> push a second Home, whatever the session does: Expo Router
58's behaviour. Conventions now states the general rule (dismissTo, or
back when the tab is just below) and points the sign-in paragraph to it;
"the screen below" is the screen below the group. The (app) layout's
comment said a user is sent Home when the session ends; it is the screen
below the group.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… LOWs)

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@koniz-dev

Copy link
Copy Markdown
Owner Author

Ready for review; not to be merged until the maintainer decides (#134 criterion 8). Evidence: https://github.com/koniz-dev/react-native-starter/tree/evidence/docs/evidence/issue-134

koniz-dev and others added 10 commits October 20, 2026 21:51
)

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A restore that resolves after sign-in or expiry must not overwrite the
session; without a test, dropping `!changedRef.current` left the whole
suite green. The new tests also take every guard arm in SessionProvider
(branch coverage 83% -> 100%), including refreshUser after expiry.

Refs #136

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zod before 4.6.6 can exhaust memory validating a large array of invalid
items; the app parses backend arrays with z.array (demo todos, and the
pattern Connect Your Backend teaches). The floor is now ^4.6.7, so the
direct dependency can't resolve to an affected version.

The advisory stays reported on zod 3 under @expo/cli (dev server only,
no zod 3 fix): reviewed in the allowlist. The braces entry is removed:
npm no longer reports it.

Refs #142

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sdk-58 already carries the same change as e5c4243; its package.json and lockfile are kept.

Refs #142

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eas.json has build and submit profiles, but no workflow used them. The
EAS Build (manual) workflow takes a profile and platform, builds on EAS,
and can auto-submit a production build. Without an EXPO_TOKEN secret it
succeeds with a notice, so the template ships no credentialed service.
App Config documents the secret, inputs, credentials and OTA updates.

Refs #137

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adding a feature is four manual steps (screen, route file, strings,
test) that are easy to get half right. npm run new-feature -- <name>
[--route public|signed-in|tab] creates the screen as a named export, the
one-line route in the matching group (declaring a tab in the tabs
layout), its title string, and a test that opens the route. It checks
the name and every target first, so a refused name changes nothing.

Refs #138

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adopters in regulated domains need pinning, and getting it wrong (one
pin, a leaf key that rotates) locks users out. The recipe pins the API
hosts with react-native-ssl-public-key-pinning: backup pins, how to get
the hashes, a module that holds the app's HTTP clients until pinning is
on and fails closed, a check that it fails closed, key rotation, and the
web limitation. The library is pinned in docs/recipes for docs:check.

init-project's prompt and docs now speak of "the recipes", which it
removes or keeps together.

Refs #139

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The i18n seam takes more dictionaries, but nothing showed how to add a
language end to end. The recipe adds an Arabic dictionary (partial, with
English fallback per key), picks the language from the device with
expo-localization, sets the layout direction with I18nManager (and the
page's lang and dir on the web), explains when the direction applies and
which config plugin options to leave unset, and gives a test that fails
on a missing key or a dropped placeholder.

init-project now strips the @recipes block in Plug In a Provider, which
links the recipe.

Refs #140

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Maestro workflows ran only when dispatched, so a change outside the
lockfile (runner image, emulator, a download) could break the flows
unnoticed between releases. E2E (Android) now also runs weekly in Expo
Go, gated on a repository variable: a repository made from the template
doesn't have it, so its weekly run is skipped at no cost. A scheduled
failure opens or updates an issue from a separate job, the only one that
can write issues. No ref input and no new caches (#112).

Refs #141

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit d1eb09f. The starter is in maintenance mode, and the
workflow was a new feature no user asked for. It duplicated Expo's own CI
options (EAS Workflows, the GitHub integration), pinned an eas-cli version
to keep raising, shipped inert until configured, and its real build could
never run in this repository's CI. App Config now points to Expo's guides
for building on CI instead.

Refs #137

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
koniz-dev and others added 18 commits November 1, 2026 13:23
The revert of the EAS Build workflow (2333840) said App Config points to
these guides, but the line was left out of that commit.

Refs #137

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README.md conflicted: main rewrote it as a landing page, sdk-58 had only
changed the SDK in the old intro. Kept main's README with the Expo SDK
badge and the Tech stack row on SDK 58, React Native 0.88.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings follow-redirects 1.16.2 and node-forge 1.4.2. The
decode-uri-component allowlist entry from main is dropped here: Expo
Router 58 no longer depends on query-string, so the advisory isn't
reported on this branch.

Refs #146

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ULE is set"

This reverts commit ab4d519. A weekly scheduled run adds a standing job to watch and keep green. The starter is in maintenance mode
(security fixes, regressions, compatibility, documentation fixes), and
this was a new addition no user asked for: it came from comparing with
koniz-dev/flutter-starter. It can come back if a user asks for it.

Refs #141

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit fc671e2. A new recipe adds content and a pinned library that CI must keep compiling on every SDK. The starter is in maintenance mode
(security fixes, regressions, compatibility, documentation fixes), and
this was a new addition no user asked for: it came from comparing with
koniz-dev/flutter-starter. It can come back if a user asks for it.

Refs #140

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit 396b833. A new recipe adds content and a pinned library that CI must keep compiling on every SDK. The starter is in maintenance mode
(security fixes, regressions, compatibility, documentation fixes), and
this was a new addition no user asked for: it came from comparing with
koniz-dev/flutter-starter. It can come back if a user asks for it.

Refs #139

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit 2c81883. A 300-line generator adds a script and tests to keep in step with the conventions it encodes; AGENTS.md and Conventions already describe the steps. The starter is in maintenance mode
(security fixes, regressions, compatibility, documentation fixes), and
this was a new addition no user asked for: it came from comparing with
koniz-dev/flutter-starter. It can come back if a user asks for it.

Refs #138

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #146

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npx expo install expo@~58.0.22, then --fix: expo-router 58.0.29,
react-native 0.88.1, react-native-screens 4.30.1, jest-expo 58.0.13 and
the other patch versions Expo expects. @react-native/jest-preset moves
to 0.88.1 with react-native (jest-expo 58.0.13 needs ^0.88.1), still
pinned exactly. @expo/cli 58.1.18 no longer depends on zod 3, which
clears GHSA-mrhx-c7xc-m3w5 from the tree.

Refs #146

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm audit no longer reports GHSA-mrhx-c7xc-m3w5 (audit:check noted the
stale entry), so the allowlist is empty on this branch. Getting Started
no longer assumes the allowlist starts with entries.

npm had offered expo@51 (a downgrade) as the fix, so audit:check treated
the advisory as unfixable even though ~58.0.16 allowed the fixed expo.

Refs #146

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android client the store and api.expo.dev report for SDK 58 on
2026-11-06; SHA256 from the release asset's digest.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npx expo install --fix: expo 58.0.23, expo-router 58.0.30, expo-linking
58.0.16, expo-splash-screen 58.0.13, react-native-safe-area-context
5.11.1, jest-expo 58.0.14 (and node-forge 1.4.3 through the lockfile).

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A link opened while the app runs, to a group its guard refuses (/login
signed in, /profile signed out), left a second Home under Expo Router
58: it opens the group, then the guard replaces it with Home. Router 57
dropped such a link. useNoExtraHome now undoes the replacement whenever
the group that just left the top is one the guard refuses, instead of
only right after a session change, which also covers sign-in and
sign-out. Reported upstream in expo/expo#51916 (comment of 2026-10-26).

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lace

A replace from Home into a group the guard refuses ends with Home alone
in the root stack, so the dismiss had nothing to pop and React
Navigation logged an unhandled POP in development. Found in the
confirmation round R11.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expo Router 58 gives stack headers the native appearance on iOS 26
(expo/expo#51450): the header is translucent and the screen runs under
it, so Profile's intro and Name row sat under the header in the iOS
native e2e (07-profile.png, since the first SDK 58 run). The automatic
content inset starts Profile, Login and the showcase below the header;
iOS before 26 and Android lay the screen out below an opaque header and
are unchanged.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@koniz-dev
koniz-dev marked this pull request as ready for review November 6, 2026 19:49
@koniz-dev
koniz-dev merged commit 28fac1a into main Nov 6, 2026
8 checks passed
@koniz-dev
koniz-dev deleted the sdk-58 branch November 7, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant