@passflow/react 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +94 -12
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.es.js +133 -143
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/src/components/flow/index.d.cts +1 -1
  8. package/dist/src/components/flow/index.d.ts +1 -1
  9. package/dist/src/components/flow/index.d.ts.map +1 -1
  10. package/dist/src/components/flow/passflow/flow-router.d.cts +39 -0
  11. package/dist/src/components/flow/passflow/flow-router.d.ts +39 -0
  12. package/dist/src/components/flow/passflow/flow-router.d.ts.map +1 -0
  13. package/dist/src/components/flow/passflow/index.d.cts +25 -3
  14. package/dist/src/components/flow/passflow/index.d.ts +25 -3
  15. package/dist/src/components/flow/passflow/index.d.ts.map +1 -1
  16. package/dist/src/components/flow/two-factor-setup-magic-link/index.d.cts +6 -1
  17. package/dist/src/components/flow/two-factor-setup-magic-link/index.d.ts +6 -1
  18. package/dist/src/components/flow/two-factor-setup-magic-link/index.d.ts.map +1 -1
  19. package/dist/src/components/form/forgot-password/forgot-password.d.ts.map +1 -1
  20. package/dist/src/components/form/invitation-join/index.d.ts.map +1 -1
  21. package/dist/src/components/form/signin/index.d.ts.map +1 -1
  22. package/dist/src/components/form/signup/index.d.ts.map +1 -1
  23. package/dist/src/components/provider/passflow-provider.d.ts.map +1 -1
  24. package/dist/src/components/provider/use-dispose-on-replace.d.cts +17 -0
  25. package/dist/src/components/provider/use-dispose-on-replace.d.ts +17 -0
  26. package/dist/src/components/provider/use-dispose-on-replace.d.ts.map +1 -0
  27. package/dist/src/context/auth-context.d.cts +5 -4
  28. package/dist/src/context/auth-context.d.ts +5 -4
  29. package/dist/src/context/auth-context.d.ts.map +1 -1
  30. package/dist/src/context/index.d.cts +1 -0
  31. package/dist/src/context/index.d.ts +1 -0
  32. package/dist/src/context/index.d.ts.map +1 -1
  33. package/dist/src/context/search-context.d.cts +20 -0
  34. package/dist/src/context/search-context.d.ts +20 -0
  35. package/dist/src/context/search-context.d.ts.map +1 -0
  36. package/dist/src/hooks/index.d.cts +2 -1
  37. package/dist/src/hooks/index.d.ts +2 -1
  38. package/dist/src/hooks/index.d.ts.map +1 -1
  39. package/dist/src/hooks/use-refresh-status.d.cts +36 -0
  40. package/dist/src/hooks/use-refresh-status.d.ts +36 -0
  41. package/dist/src/hooks/use-refresh-status.d.ts.map +1 -0
  42. package/dist/src/hooks/use-session-expired.d.cts +8 -4
  43. package/dist/src/hooks/use-session-expired.d.ts +8 -4
  44. package/dist/src/hooks/use-session-expired.d.ts.map +1 -1
  45. package/dist/src/utils/url-params.d.cts +2 -4
  46. package/dist/src/utils/url-params.d.ts +2 -4
  47. package/dist/src/utils/url-params.d.ts.map +1 -1
  48. package/package.json +6 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to `@passflow/react` are documented in this file.
4
4
 
5
+ ## [0.17.0] - 2026-11-03
6
+
7
+ ### Changed
8
+
9
+ - Requires `@passflow/core` `^0.21.0`: one token refresh per browser across tabs, with transient failures kept apart from the end of a session. See core's `docs/token-refresh.md`, "Migrating from 0.20.1". What reaches React apps: a network error or 5xx during refresh no longer signs the user out; `SessionExpired` fires only at a real end; `SignOut` fires once per sign-out.
10
+ - `SessionExpiredReason` is core's type, re-exported from the package root as before. It adds `'successor_timeout'` and `'identity_changed'`, so `useSessionExpired` and `AuthProvider`'s `onSessionExpired` can receive them. An exhaustive `switch` over the reason needs the new cases.
11
+
12
+ ### Added
13
+
14
+ - `useRefreshStatus()` returns `{ isFailing, since, nextRetryAt }` while token refresh is failing transiently (`RefreshFailing` until `RefreshRecovered`), for a "reconnecting" notice. Call it in a component that lives as long as the app: a component that mounts mid-episode starts idle.
15
+
16
+ ### Fixed
17
+
18
+ - `PassflowProvider` disposes the `Passflow` instances it replaces (its state changes on app ID discovery and app settings) and the one it holds on unmount. From core 0.21 each instance holds refresh timers, a `BroadcastChannel` and Web Locks, so an undisposed one went on acting as another tab.
19
+
20
+ ## [0.16.0] - 2026-11-03
21
+
22
+ ### Breaking
23
+
24
+ - `react-router` is no longer a peer dependency, and the package does not import it. The only peers are `react` and `react-dom`, `^18 || ^19`. `PassflowFlow` keeps its own small router, so it works the same in an app that routes with React Router, TanStack Router, Wouter, another router, or none. A host that installed `react-router` only for this package can remove it.
25
+ - `PassflowFlow` no longer picks up a host React Router `<Router>`, and the console warning about `routerType` inside one is gone. Inside an app router in browser mode it shares the address bar with it: each side sees the other's navigations, and Back reaches both. Mount it on a catch-all route (`/auth/*` in React Router, `/auth/$` in TanStack Router, `<Route path="/auth" nest>` in Wouter).
26
+ - `pathPrefix` is an absolute path from the root of the URL. When it is not given, the flow takes it from the first URL it sees, so `<PassflowFlow />` on `/auth` or `/auth/signin` uses `/auth`. Before, inside a host router, it was relative to the parent route.
27
+ - `TwoFactorSetupMagicLinkFlow` no longer uses React Router's `useParams`. It reads the token from the URL segment after `/two-factor-setup-magic-link/`, or from a new `token` prop.
28
+
29
+ ### Added
30
+
31
+ - `<PassflowFlow />` with no props works: browser history, prefix from the URL, every screen at a fixed path under it (`/signin`, `/signup`, `/verify-challenge-otp`, `/two-factor-setup-magic-link/:token`, …).
32
+ - Full control: pass `location` and `navigate` together and the app's router owns the location, for example a TanStack Router memory history. Their types are exported as `PassflowFlowLocation` and `PassflowFlowNavigate`.
33
+ - `PassflowFlow` serves the 2FA setup magic link (`/two-factor-setup-magic-link/:token`) as one of its screens.
34
+
35
+ ### Fixed
36
+
37
+ - Forms inside `PassflowFlow` read search params from the flow's location. In memory or hash mode, or under `location` + `navigate`, they read the address bar, which never carried them; forgot-password then threw "Invalid default method param".
38
+
39
+ ### Internal
40
+
41
+ - Tests run the zero-config flow inside React Router, TanStack Router and Wouter (jsdom), and inside a TanStack Router app in Chromium. `@tanstack/react-router` and `wouter` are dev dependencies; `react-router` stays one, for those tests and the docs.
42
+ - The consumer type check installs no router.
43
+
5
44
  ## [0.15.1] - 2026-11-02
6
45
 
7
46
  ### Fixed
package/README.md CHANGED
@@ -46,6 +46,8 @@ This is a SDK for react application.
46
46
  - [useJoinInvite](#usejoininvite)
47
47
  - [useLogout](#uselogout)
48
48
  - [usePasswordlessComplete](#usepasswordlesscomplete)
49
+ - [useSessionExpired](#usesessionexpired)
50
+ - [useRefreshStatus](#userefreshstatus)
49
51
 
50
52
  to install just type:
51
53
 
@@ -137,17 +139,15 @@ Both scripts automatically start the demo app and Storybook. `pnpm test` uses th
137
139
  ## Installation
138
140
 
139
141
  ```bash
140
- pnpm add @passflow/react react-router
142
+ pnpm add @passflow/react
141
143
  ```
142
144
 
143
- `react-router` (`^7.0.0 || ^8.0.0`) is a required peer dependency and is not bundled, so install it even when the app routes with Wouter or TanStack Router. `react-router-dom` is not needed: from v7 on, `BrowserRouter` and the rest are exported by `react-router`.
145
+ No router is needed. `PassflowFlow` has its own small router, and works on its own or inside an app routed by React Router, TanStack Router, Wouter or anything else.
144
146
 
145
147
  ## Requirements
146
148
 
147
- - React 18+ (React 19.2.7+ with React Router 8)
148
- - React Router 7 or 8 (React Router 6 is not supported)
149
+ - React 18 or 19
149
150
  - Node `^22.12.0 || ^24.0.0 || >=26.0.0`
150
- - Your app may route with React Router, Wouter or TanStack Router
151
151
 
152
152
  ## Integration
153
153
 
@@ -246,21 +246,63 @@ authentication-context claims those tokens carry.
246
246
 
247
247
  ### PassflowFlow
248
248
 
249
+ The whole sign-in experience on its own pages: sign-in, sign-up, one-time codes, magic links, password reset, invitations and two-factor. It needs no router and no setup:
250
+
251
+ ```tsx
252
+ <PassflowProvider url="https://auth.example.com" appId="your-app-id">
253
+ <PassflowFlow />
254
+ </PassflowProvider>
255
+ ```
256
+
249
257
  | Prop | Type | Description |
250
258
  |------|------|-------------|
251
259
  | successAuthRedirect | string | URL to redirect after successful authentication |
252
- | federatedDisplayMode | "popup" \| "redirect" | Federated authentication display mode |
253
- | pathPrefix | string | Prefix for all routes (optional) |
260
+ | federatedDisplayMode | "modal" \| "redirect" | Federated authentication display mode |
261
+ | pathPrefix | string | Where the flow lives, such as `/auth`. Optional: worked out from the first URL |
262
+ | routerType | "browser" \| "hash" \| "memory" | The history the flow keeps. Default `browser` |
263
+ | location, navigate | see below | Full control by the app's router. Optional, always together |
264
+
265
+ **Paths.** The flow's screens are `<prefix>/signin`, `<prefix>/signup`, `<prefix>/verify-challenge-otp`, `<prefix>/verify-challenge-magic-link`, `<prefix>/forgot-password`, `<prefix>/forgot-password/success`, `<prefix>/password/reset`, `<prefix>/invitation`, `<prefix>/two-factor-verify`, `<prefix>/two-factor-setup` and `<prefix>/two-factor-setup-magic-link/:token`. Without `pathPrefix` the flow takes the prefix from the first URL it sees: `/auth/signin` and `/auth` both give `/auth`, `/` gives none. A path under the prefix that names no screen goes to sign-in.
254
266
 
255
- Inside an app that already has a React Router `<Router>`, `PassflowFlow` uses that router rather than its own (`routerType` is ignored). Mount it under a splat route so its own routes can match, and `pathPrefix` is relative to that route:
267
+ **Inside an app's router.** Mount the flow on a catch-all route; nothing else is needed. The flow moves the address bar and announces it with `popstate`, the event React Router, TanStack Router and Wouter listen to, so the app's router follows the flow and the flow follows the app, Back and Forward included:
256
268
 
257
269
  ```tsx
258
- <Routes>
259
- <Route path="/" element={<Home />} />
260
- <Route path="/auth/*" element={<PassflowFlow successAuthRedirect="/" />} />
261
- </Routes>
270
+ // TanStack Router
271
+ const authRoute = createRoute({ getParentRoute: () => rootRoute, path: '/auth/$', component: () => <PassflowFlow /> });
272
+
273
+ // React Router
274
+ <Route path="/auth/*" element={<PassflowFlow />} />
275
+
276
+ // Wouter
277
+ <Route path="/auth" nest><PassflowFlow /></Route>
262
278
  ```
263
279
 
280
+ **`routerType`.** `browser` (default) uses the address bar. `hash` keeps the flow's path after `#`. `memory` leaves the URL alone, for an embedded widget or tests.
281
+
282
+ **Full control.** When the app's router does not use the address bar (it runs in hash or memory mode), hand the flow its location and a way to move it. `routerType` is then not used:
283
+
284
+ ```tsx
285
+ // TanStack Router with a memory or hash history
286
+ function AuthPage() {
287
+ const router = useRouter();
288
+ const location = useRouterState({ select: (state) => state.location });
289
+ return (
290
+ <PassflowFlow
291
+ location={{ pathname: location.pathname, search: location.searchStr }}
292
+ navigate={(to, { replace }) => {
293
+ const href = `${to.pathname}${to.search}`;
294
+ if (replace) router.history.replace(href);
295
+ else router.history.push(href);
296
+ }}
297
+ />
298
+ );
299
+ }
300
+ ```
301
+
302
+ `TwoFactorSetupMagicLinkFlow` can also be mounted on its own page. It reads the token from the URL (the segment after `/two-factor-setup-magic-link/`), or takes it as a `token` prop.
303
+
304
+ The sections below wire the app's router into the **standalone form components** (`SignIn`, `SignUp`, …) through `PassflowProvider`'s `navigate` and `router` props. `PassflowFlow` does not need them.
305
+
264
306
  ### React Router
265
307
 
266
308
  Example of integration with React Router:
@@ -907,3 +949,43 @@ const { fetch, isLoading, isError, error } = usePasswordlessComplete();
907
949
  | isLoading | `boolean` | Loading state |
908
950
  | isError | `boolean` | Error state |
909
951
  | error | `string` | Error message |
952
+
953
+ ### useSessionExpired
954
+ Hook for reacting to the end of a session, for example to redirect to your login page.
955
+
956
+ ```typescript
957
+ const { isSessionExpired, expiredReason, resetSessionExpired } = useSessionExpired({
958
+ onSessionExpired: (reason) => navigate('/signin'),
959
+ });
960
+ ```
961
+
962
+ `SessionExpired` fires only when the session has really ended: the last auth strike (`'refresh_failed'`), no successor token pair in time (`'successor_timeout'`), or another user's tokens in storage (`'identity_changed'`). A transient refresh failure (network error, 5xx) keeps the session and shows up as `RefreshFailing` / `RefreshRecovered` instead; use [useRefreshStatus](#userefreshstatus) for that. The state resets on `SignIn` and `Register`.
963
+
964
+ | Parameter | Type | Description |
965
+ |-----------|------|-------------|
966
+ | onSessionExpired | `(reason: SessionExpiredReason) => void` (optional) | Called when the session ends |
967
+
968
+ **Returns:**
969
+
970
+ | Property | Type | Description |
971
+ |----------|------|-------------|
972
+ | isSessionExpired | `boolean` | True once the session has ended |
973
+ | expiredReason | `SessionExpiredReason \| null` | Why it ended (`SessionExpiredReason` is exported from `@passflow/core`) |
974
+ | resetSessionExpired | `() => void` | Clears the expired state |
975
+
976
+ ### useRefreshStatus
977
+ Hook for showing a "connection trouble" notice while token refresh is failing transiently. The session is kept during the episode; show the notice on `RefreshFailing` and hide it on `RefreshRecovered`.
978
+
979
+ ```typescript
980
+ const { isFailing, since, nextRetryAt } = useRefreshStatus();
981
+ ```
982
+
983
+ Set by `RefreshFailing`. Cleared by `RefreshRecovered`, `SessionExpired`, `SignOut`, `SignIn` and `Register`. The episode is known only from events, so a component that mounts after `RefreshFailing` starts idle: call the hook in a component that lives as long as the app, near `PassflowProvider`.
984
+
985
+ **Returns:**
986
+
987
+ | Property | Type | Description |
988
+ |----------|------|-------------|
989
+ | isFailing | `boolean` | True inside a transient-failure episode |
990
+ | since | `number \| null` | Epoch ms of the episode's first failure |
991
+ | nextRetryAt | `number \| null` | Epoch ms of the pending retry, when known |