@versini/auth0-react-thin 2.22.1 → 2.24.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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as Auth0Provider, Auth0ProviderOptions, Auth0ProviderWithConfigOptions, Auth0ProviderWithClientOptions, AppState, ConnectedAccount } from './auth0-provider';
2
2
  export { default as useAuth0 } from './use-auth0';
3
+ export { default as useAuth0Suspense, Auth0SuspenseContextInterface, } from './use-auth0-suspense';
3
4
  export { default as withAuth0, WithAuth0Props } from './with-auth0';
4
5
  export { default as withAuthenticationRequired, WithAuthenticationRequiredOptions, } from './with-authentication-required';
5
6
  export { default as Auth0Context, Auth0ContextInterface, initialContext, LogoutOptions, RedirectLoginOptions, } from './auth0-context';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,IAAI,aAAa,EACxB,oBAAoB,EACpB,8BAA8B,EAC9B,8BAA8B,EAC9B,QAAQ,EACR,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EACL,OAAO,IAAI,0BAA0B,EACrC,iCAAiC,GAClC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,OAAO,IAAI,YAAY,EACvB,qBAAqB,EACrB,cAAc,EACd,aAAa,EACb,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,OAAO,EACP,IAAI,EACJ,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,YAAY,EACZ,iBAAiB,EACjB,6BAA6B,EAC7B,4BAA4B,EAC5B,YAAY,EACZ,YAAY,EACZ,0BAA0B,EAC1B,qBAAqB,EACrB,QAAQ,EACR,mBAAmB,EACnB,gBAAgB,EAEhB,QAAQ,EACR,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,yBAAyB,EAEzB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EAEpB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,yBAAyB,EACzB,aAAa,EACb,uBAAuB,EAEvB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,4BAA4B,EAC5B,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAEhB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EAEnB,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,MAAM,EACN,iCAAiC,EACjC,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,IAAI,aAAa,EACxB,oBAAoB,EACpB,8BAA8B,EAC9B,8BAA8B,EAC9B,QAAQ,EACR,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACL,OAAO,IAAI,gBAAgB,EAC3B,6BAA6B,GAC9B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EACL,OAAO,IAAI,0BAA0B,EACrC,iCAAiC,GAClC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,OAAO,IAAI,YAAY,EACvB,qBAAqB,EACrB,cAAc,EACd,aAAa,EACb,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,OAAO,EACP,IAAI,EACJ,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,YAAY,EACZ,iBAAiB,EACjB,6BAA6B,EAC7B,4BAA4B,EAC5B,YAAY,EACZ,YAAY,EACZ,0BAA0B,EAC1B,qBAAqB,EACrB,QAAQ,EACR,mBAAmB,EACnB,gBAAgB,EAEhB,QAAQ,EACR,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,yBAAyB,EAEzB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EAEpB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,yBAAyB,EACzB,aAAa,EACb,uBAAuB,EAEvB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,4BAA4B,EAC5B,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAEhB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EAEnB,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,MAAM,EACN,iCAAiC,EACjC,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { User } from '@auth0/auth0-spa-js';
3
+ import { Auth0ContextInterface } from './auth0-context';
4
+ /**
5
+ * The value returned by `useAuth0Suspense`: the full `useAuth0` interface minus
6
+ * `isLoading` and the internal `_initPromise`. `error` is
7
+ * retained for post-init failures such as `loginWithPopup`.
8
+ */
9
+ export type Auth0SuspenseContextInterface<TUser extends User = User> = Omit<Auth0ContextInterface<TUser>, 'isLoading' | '_initPromise'>;
10
+ /**
11
+ * ```jsx
12
+ * <Suspense fallback={<Spinner />}>
13
+ * <Profile />
14
+ * </Suspense>
15
+ *
16
+ * function Profile() {
17
+ * const { user, isAuthenticated } = useAuth0Suspense();
18
+ * return isAuthenticated ? <p>Hello {user.name}</p> : <p>Please log in</p>;
19
+ * }
20
+ * ```
21
+ *
22
+ * Suspense-enabled variant of `useAuth0`. Suspends the component until Auth0
23
+ * initialization completes (letting the nearest `<Suspense>` fallback render),
24
+ * and throws initialization errors so the nearest Error Boundary can handle
25
+ * them. Requires React 19 or later.
26
+ *
27
+ * If initialization fails and the app later becomes authenticated by other
28
+ * means, the session is re-checked once; retrying the Error Boundary then
29
+ * renders if that check succeeded, or throws again if it did not.
30
+ *
31
+ * TUser is an optional type param to provide a type to the `user` field.
32
+ */
33
+ declare const useAuth0Suspense: <TUser extends User = User>(context?: React.Context<Auth0ContextInterface<User>>) => Auth0SuspenseContextInterface<TUser>;
34
+ export default useAuth0Suspense;
35
+ //# sourceMappingURL=use-auth0-suspense.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-auth0-suspense.d.ts","sourceRoot":"","sources":["../src/use-auth0-suspense.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC3C,OAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAEtE;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,CAAC,KAAK,SAAS,IAAI,GAAG,IAAI,IAAI,IAAI,CACzE,qBAAqB,CAAC,KAAK,CAAC,EAC5B,WAAW,GAAG,cAAc,CAC7B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,QAAA,MAAM,gBAAgB,GAAI,KAAK,SAAS,IAAI,GAAG,IAAI,EACjD,oDAAsB,KACrB,6BAA6B,CAAC,KAAK,CA0BrC,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"with-authentication-required.d.ts","sourceRoot":"","sources":["../src/with-authentication-required.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,aAAa,EAAa,EAAE,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAqB,EACnB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,iBAAiB,CAAC;AAkBzB;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;IACnC;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;IACxC;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,QAAA,MAAM,0BAA0B,GAAI,CAAC,SAAS,MAAM,EAClD,WAAW,aAAa,CAAC,CAAC,CAAC,EAC3B,UAAS,iCAAsC,KAC9C,EAAE,CAAC,CAAC,CAuCN,CAAC;AAEF,eAAe,0BAA0B,CAAC"}
1
+ {"version":3,"file":"with-authentication-required.d.ts","sourceRoot":"","sources":["../src/with-authentication-required.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,aAAa,EAAa,EAAE,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAqB,EACnB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,iBAAiB,CAAC;AAwBzB;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;IACnC;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;IACxC;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,QAAA,MAAM,0BAA0B,GAAI,CAAC,SAAS,MAAM,EAClD,WAAW,aAAa,CAAC,CAAC,CAAC,EAC3B,UAAS,iCAAsC,KAC9C,EAAE,CAAC,CAAC,CAuCN,CAAC;AAEF,eAAe,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "author": "Auth0",
3
3
  "name": "@versini/auth0-react-thin",
4
- "version": "2.22.1",
5
- "description": "Unofficial slim fork of @auth0/auth0-react@2.22.1 — bundle-size build, not affiliated with Auth0/Okta. See @versini/auth0-thin.",
4
+ "version": "2.24.0",
5
+ "description": "Unofficial slim fork of @auth0/auth0-react@2.24.0 — bundle-size build, not affiliated with Auth0/Okta. See @versini/auth0-thin.",
6
6
  "keywords": [
7
7
  "auth0",
8
8
  "fork",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@auth0/auth0-spa-js": "^2.24.1"
36
36
  },
37
- "forkedFrom": "@auth0/auth0-react@2.22.1",
37
+ "forkedFrom": "@auth0/auth0-react@2.24.0",
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  }
@@ -474,6 +474,16 @@ export interface Auth0ContextInterface<TUser extends User = User>
474
474
  * ```
475
475
  */
476
476
  myAccount: MyAccountApiClient;
477
+
478
+ /**
479
+ * Internal. A promise that resolves when Auth0 initialization completes and
480
+ * rejects with the initialization error if it fails. Consumed by
481
+ * `useAuth0Suspense`, which also treats its absence as "no provider" — do not
482
+ * add a stub value for this field to `initialContext`. Not part of the
483
+ * supported public API.
484
+ * @ignore
485
+ */
486
+ _initPromise?: Promise<void>;
477
487
  }
478
488
 
479
489
  /**
@@ -157,6 +157,32 @@ const defaultOnRedirectCallback = (appState?: AppState): void => {
157
157
  );
158
158
  };
159
159
 
160
+ /**
161
+ * @ignore
162
+ */
163
+ interface InitDeferred {
164
+ promise: Promise<void>;
165
+ resolve: () => void;
166
+ reject: (error: Error) => void;
167
+ }
168
+
169
+ /**
170
+ * @ignore
171
+ */
172
+ const createInitDeferred = (): InitDeferred => {
173
+ let resolve!: () => void;
174
+ let reject!: (error: Error) => void;
175
+ const promise = new Promise<void>((res, rej) => {
176
+ resolve = res;
177
+ reject = rej;
178
+ });
179
+ // Avoid unhandled-rejection warnings when no one is consuming the promise
180
+ // (i.e. useAuth0Suspense is not used). useAuth0Suspense attaches its own
181
+ // handler via use().
182
+ promise.catch(() => undefined);
183
+ return { promise, resolve, reject };
184
+ };
185
+
160
186
  /**
161
187
  * ```jsx
162
188
  * <Auth0Provider
@@ -168,6 +194,11 @@ const defaultOnRedirectCallback = (appState?: AppState): void => {
168
194
  * ```
169
195
  *
170
196
  * Provides the Auth0Context to its child components.
197
+ *
198
+ * The SDK is a client-only module (its published ESM and CommonJS bundles ship the
199
+ * `'use client'` directive), so in a Next.js App Router app you can import and render
200
+ * `Auth0Provider` directly from a Server Component such as `app/layout.tsx`.
201
+ * Components that call hooks like `useAuth0` must still be Client Components.
171
202
  */
172
203
  const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUser>) => {
173
204
  const {
@@ -189,6 +220,17 @@ const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUs
189
220
  () => providedClient ?? new Auth0Client(toAuth0ClientOptions(clientOpts))
190
221
  );
191
222
  const [state, dispatch] = useReducer(reducer<TUser>, initialAuthState as AuthState<TUser>);
223
+ // In state so the init retry below can swap in a fresh, non-rejected promise.
224
+ // Without that swap the first rejection would re-throw on every later render,
225
+ // making Error Boundary retries useless.
226
+ const [initDeferred, setInitDeferred] = useState(createInitDeferred);
227
+ // Set only once initialization has genuinely rejected. State, not a ref, so
228
+ // the retry effect re-evaluates when init fails *after* the app has become
229
+ // authenticated by some other means.
230
+ const [initFailed, setInitFailed] = useState(false);
231
+ // Guards against retrying more than once: a retry that also fails would
232
+ // otherwise re-trigger the effect for as long as the user stays signed in.
233
+ const initRetried = useRef(false);
192
234
  const didInitialise = useRef(false);
193
235
 
194
236
  const handleError = useCallback((error: Error) => {
@@ -196,15 +238,15 @@ const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUs
196
238
  return error;
197
239
  }, []);
198
240
 
199
- useEffect(() => {
200
- if (didInitialise.current) {
201
- return;
202
- }
203
- didInitialise.current = true;
204
- (async (): Promise<void> => {
241
+ // Settles `deferred` according to what initialization actually did, so
242
+ // `_initPromise` never claims success that did not happen. `allowRedirect`
243
+ // is false on a retry: the redirect callback is single-use, so a retry can
244
+ // only re-verify the session.
245
+ const runInit = useCallback(
246
+ async (deferred: InitDeferred, allowRedirect: boolean): Promise<void> => {
205
247
  try {
206
248
  let user: TUser | undefined;
207
- if (hasAuthParams() && !skipRedirectCallback) {
249
+ if (allowRedirect && hasAuthParams() && !skipRedirectCallback) {
208
250
  const { appState = {}, response_type, ...result } = await client.handleRedirectCallback();
209
251
  user = await client.getUser();
210
252
  appState.response_type = response_type;
@@ -217,11 +259,44 @@ const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUs
217
259
  user = await client.getUser();
218
260
  }
219
261
  dispatch({ type: 'INITIALISED', user });
262
+ deferred.resolve();
220
263
  } catch (error) {
221
- handleError(loginError(error));
264
+ const err = loginError(error);
265
+ handleError(err);
266
+ setInitFailed(true);
267
+ deferred.reject(err);
222
268
  }
223
- })();
224
- }, [client, onRedirectCallback, skipRedirectCallback, handleError]);
269
+ },
270
+ [client, onRedirectCallback, skipRedirectCallback, handleError]
271
+ );
272
+
273
+ useEffect(() => {
274
+ if (didInitialise.current) {
275
+ return;
276
+ }
277
+ didInitialise.current = true;
278
+ // Always the first deferred: `didInitialise` means this only ever runs once,
279
+ // so a later swap by the retry effect re-runs this effect but returns above.
280
+ void runInit(initDeferred, true);
281
+ }, [runInit, initDeferred]);
282
+
283
+ // A rejected promise stays rejected forever, so React.use() would re-throw on
284
+ // every later render and an Error Boundary retry could never recover. Once
285
+ // init has failed but the app has since become authenticated by some other
286
+ // means (loginWithPopup, a silent token, a passkey login), re-run the session
287
+ // check against a fresh deferred. The promise then reflects a real
288
+ // checkSession() outcome rather than an assumption that things are fine --
289
+ // if the session is still bad it rejects again and the boundary keeps showing
290
+ // the error.
291
+ useEffect(() => {
292
+ if (!initFailed || initRetried.current || !state.isAuthenticated) {
293
+ return;
294
+ }
295
+ initRetried.current = true;
296
+ const retry = createInitDeferred();
297
+ setInitDeferred(retry);
298
+ void runInit(retry, false);
299
+ }, [initFailed, state.isAuthenticated, runInit]);
225
300
 
226
301
  const loginWithRedirect = useCallback(
227
302
  (opts?: RedirectLoginOptions): Promise<void> => {
@@ -480,6 +555,7 @@ const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUs
480
555
  mfa,
481
556
  passkey,
482
557
  myAccount,
558
+ _initPromise: initDeferred.promise,
483
559
  };
484
560
  }, [
485
561
  state,
@@ -503,6 +579,7 @@ const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUs
503
579
  mfa,
504
580
  passkey,
505
581
  myAccount,
582
+ initDeferred,
506
583
  ]);
507
584
 
508
585
  return <context.Provider value={contextValue}>{children}</context.Provider>;
package/src/index.tsx CHANGED
@@ -7,6 +7,10 @@ export {
7
7
  ConnectedAccount
8
8
  } from './auth0-provider';
9
9
  export { default as useAuth0 } from './use-auth0';
10
+ export {
11
+ default as useAuth0Suspense,
12
+ Auth0SuspenseContextInterface,
13
+ } from './use-auth0-suspense';
10
14
  export { default as withAuth0, WithAuth0Props } from './with-auth0';
11
15
  export {
12
16
  default as withAuthenticationRequired,
@@ -0,0 +1,71 @@
1
+ // Namespace import: `use` only exists as a named export from React 19, so
2
+ // `import { use }` fails at link time for React 16-18 consumers even if they
3
+ // never call this hook. Property access stays late-bound.
4
+ import * as React from 'react';
5
+ import { User } from '@auth0/auth0-spa-js';
6
+ import Auth0Context, { Auth0ContextInterface } from './auth0-context';
7
+
8
+ /**
9
+ * The value returned by `useAuth0Suspense`: the full `useAuth0` interface minus
10
+ * `isLoading` and the internal `_initPromise`. `error` is
11
+ * retained for post-init failures such as `loginWithPopup`.
12
+ */
13
+ export type Auth0SuspenseContextInterface<TUser extends User = User> = Omit<
14
+ Auth0ContextInterface<TUser>,
15
+ 'isLoading' | '_initPromise'
16
+ >;
17
+
18
+ /**
19
+ * ```jsx
20
+ * <Suspense fallback={<Spinner />}>
21
+ * <Profile />
22
+ * </Suspense>
23
+ *
24
+ * function Profile() {
25
+ * const { user, isAuthenticated } = useAuth0Suspense();
26
+ * return isAuthenticated ? <p>Hello {user.name}</p> : <p>Please log in</p>;
27
+ * }
28
+ * ```
29
+ *
30
+ * Suspense-enabled variant of `useAuth0`. Suspends the component until Auth0
31
+ * initialization completes (letting the nearest `<Suspense>` fallback render),
32
+ * and throws initialization errors so the nearest Error Boundary can handle
33
+ * them. Requires React 19 or later.
34
+ *
35
+ * If initialization fails and the app later becomes authenticated by other
36
+ * means, the session is re-checked once; retrying the Error Boundary then
37
+ * renders if that check succeeded, or throws again if it did not.
38
+ *
39
+ * TUser is an optional type param to provide a type to the `user` field.
40
+ */
41
+ const useAuth0Suspense = <TUser extends User = User>(
42
+ context = Auth0Context
43
+ ): Auth0SuspenseContextInterface<TUser> => {
44
+ if (typeof React.use !== 'function') {
45
+ throw new Error(
46
+ 'useAuth0Suspense requires React 19 or later (React.use is unavailable).'
47
+ );
48
+ }
49
+
50
+ const ctx = React.useContext(context) as Auth0ContextInterface<TUser>;
51
+
52
+ if (!ctx._initPromise) {
53
+ throw new Error(
54
+ 'useAuth0Suspense must be used within an <Auth0Provider>.'
55
+ );
56
+ }
57
+
58
+ // Suspends until the init promise resolves; re-throws if it rejected.
59
+ React.use(ctx._initPromise);
60
+
61
+ // Memoized so the returned object is referentially stable across renders,
62
+ // matching useAuth0, which hands back the provider's memoized context.
63
+ // Without this, `useEffect(..., [auth])` in a consumer re-runs every render.
64
+ return React.useMemo(() => {
65
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
66
+ const { isLoading, _initPromise, ...rest } = ctx;
67
+ return rest;
68
+ }, [ctx]);
69
+ };
70
+
71
+ export default useAuth0Suspense;
@@ -18,8 +18,14 @@ const defaultOnBeforeAuthentication = async (): Promise<void> => {/* noop */ };
18
18
  /**
19
19
  * @ignore
20
20
  */
21
- const defaultReturnTo = (): string =>
22
- `${window.location.pathname}${window.location.search}`;
21
+ const defaultReturnTo = (): string => {
22
+ // Normalize the pathname to prevent protocol-relative open redirects.
23
+ // A URL like https://app.example.com//evil.com produces a pathname of
24
+ // //evil.com, which routers (react-router, next.js, gatsby) interpret as a
25
+ // protocol-relative URL and redirect the user to an external host.
26
+ const pathname = window.location.pathname.replace(/^\/\/+/, '/');
27
+ return `${pathname}${window.location.search}`;
28
+ };
23
29
 
24
30
  /**
25
31
  * Options for the withAuthenticationRequired Higher Order Component