Skip to content

Release 0.1.0 alpha.5 - #6

Merged
monarcode merged 7 commits into
mainfrom
0.1.0-alpha.5
Nov 2, 2026
Merged

monarcode merged 7 commits into
mainfrom
0.1.0-alpha.5

Conversation

@monarcode

@monarcode monarcode commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

This branch is the 0.1.0-alpha.5 release. It adds Expo Router support for native iOS and Android apps (Expo SDK 53 and newer) through a new @monarcode/session-kit/expo entry point, and fixes a few React Native and adapter bugs found on the way. The CHANGELOG has the full notes.

What's new

/expo entry point

  • secureStorage() saves the session with expo-secure-store:
    • Keys are encoded for SecureStore.
    • Values over its 2048-byte limit are split across keys, and a write cut short never leaves a half-written value.
    • Reads are synchronous.
    • A session iOS kept from an earlier install is cleared by default (clearOnReinstall: false keeps it).
    • Data Android can't decrypt reads as signed out.
  • SessionGate wraps the root navigator and adds what Stack.Protected lacks:
    • It keeps the splash screen up while the session is restored.
    • It renders an unavailable({ error, retry }) fallback.
    • It re-checks the session when the app returns to the foreground.
    • It remounts screens when one account replaces another.
  • rememberDeepLink(auth, url), called from redirectSystemPath in +native-intent.tsx, keeps a deep link that Stack.Protected would drop while the user is signed out. SessionGate opens it after the sign-in.
  • New optional peers: expo-router, expo-secure-store, expo-file-system and react-native.
  • UnavailableProps is exported from /expo and /react-router.

Docs and examples

  • docs/expo.md is a type-checked Expo Router guide.
  • examples/expo-router is bundled in CI with expo export.
  • scripts/check-readme.mjs is now scripts/check-docs.mjs, which checks each document with its own resolution and lib settings.

Fixes

  • JWTs decode where there is no TextDecoder (older Hermes), or where one rejects fatal.
  • AuthProvider mounts in a layout effect on React Native.
  • React Router's SessionOutlet given only a client, without AuthProvider, now mounts that client. Before, it showed pending forever in declarative mode.

Breaking changes

  • A JWT payload that isn't valid UTF-8 is now rejected (USER_VALIDATION_FAILED) instead of being read with replacement characters.

Testing

  • pnpm run check passes: 206 tests, run with both synchronous and late-settling storage, plus type tests and docs examples.
  • Expo modules are mocked in the tests. The example app has not yet been run on a simulator or device.

JWT decoding used TextDecoder, which older Hermes builds lack. Without it,
decoding failed silently: expiry from exp and iat was ignored, and
fromAccessToken rejected every token. Decoding now falls back to
decodeURIComponent, and both paths reject malformed UTF-8, so every
platform reads a token the same way.

AuthProvider picked a passive effect whenever document was missing, which
includes React Native. It now uses the layout effect there too, so a
synchronous restore happens before the first frame. Servers still get the
passive effect.
- Add the `@monarcode/session-kit/expo` entry point with `secureStorage()`,
  which reads synchronously so a synchronous user schema restores before the
  first render
- Split values over SecureStore's 2048-byte limit across alternating chunk
  slots, so a write cut short keeps the previous value whole
- Remove a session an earlier iOS install left in the keychain, unless
  `clearOnReinstall: false`. Treat undecryptable Android data as signed out, and
  a locked keychain as `unavailable`
- Add mocked `expo-secure-store` and `expo-file-system` tests, plus a CHANGELOG
  entry
…apps

- Add `SessionGate` to the Expo entry point. It keeps the splash screen up while
  the session restores, renders an `unavailable({ error, retry })` fallback,
  re-checks the session when the app returns to the foreground, and remounts the
  navigator when one account replaces another
- Add `rememberDeepLink(auth, url)` for `+native-intent.tsx`, so a link
  `Stack.Protected` would drop while signed out opens after sign-in
- Add optional peers `expo-router`, `expo-secure-store`, `expo-file-system` and
  `react-native`, with CI testing the oldest SDK 53 versions
- Add `examples/expo-router`, an Expo SDK 58 app for iOS and Android that signs
  in against DummyJSON. It uses `secureStorage()`, `SessionGate`,
  `Stack.Protected` guards, `rememberDeepLink` in `+native-intent.tsx`, and a
  `/profile` screen that calls `auth/me`, so a deep link has somewhere to go
- Bundle the app in CI with `expo export` offline, and block `dummyjson.com` in
  the examples job so a build that tried to call it fails
- Update `examples/README.md` and `.gitignore` for the sixth app and generated
  native projects
- Add `docs/expo.md`, a step-by-step guide for Expo Router covering
  installation, `secureStorage()`, `SessionGate`, deep links, the sign-in and
  signed-in screens, and what is not supported
- Generalize `check-readme.mjs` into `check-docs.mjs`, which type-checks marked
  examples in both the README and the Expo guide, the latter under Bundler
  resolution
- Update the README for Expo with the peer dependency table, setup table and
  `secureStorage()` notes. Move the Expo CHANGELOG entries from Unreleased to
  `0.1.0-alpha.5` and bump `package.json` to match
…Outlet`,

and fix mount bugs

- Move the client lookup, mount-on-render, `unavailable` rendering and
  mount-effect logic into `react/context.ts`. `SessionGate` and React Router's
  `SessionOutlet` now mount a `client` prop themselves, so they restore the
  session without `AuthProvider`. Export `UnavailableProps` from `/react-router`
- Remount the navigator only when one account replaces another. Keep deep links
  in a per-client Nano Store, and compare routes with a new `sameRoute`
- Drop `secureStorage`'s own write queue, since auth already serializes storage
  operations. Remove chunks last to first, and have a read that finds unreadable
  data return the cleanup promise
- Make `decodeUtf8` fall back where `TextDecoder` is missing or rejects `fatal`
- Document that every `mount()` re-checks the session, and that an
  `ErrorBoundary` needs `auth.retry()` as well as its own `retry`
@monarcode
monarcode merged commit 5ebe65c into main Nov 2, 2026
3 checks passed
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