Skip to content

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

Draft
koniz-dev wants to merge 27 commits into
mainfrom
sdk-58
Draft

koniz-dev wants to merge 27 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 9 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>
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